DEV Community

Jeff Lindsay
Jeff Lindsay

Posted on

Start of an Objective-C bridge in Go

I've been struggling with a problem that I've been working on mostly off stream this last week: getting an Objective-C bridge working in Go. Last time I talked about this I found an abandoned incomplete library made half a decade ago that had the start of a usable Objective-C bridge. It just didn't work now becaue Go has gotten stricter about passing pointers with cgo.

It all has to do with a function of the libobjc library called objc_msgSend. This is used for every method call on an Objective-C object, so it might be the most used function of libobjc. The first problem is that because its used for any possible method call, it's a variadic function. This isn't supported by cgo, and the original author made a library to fix this that makes it work with some assembly code. Does it work? Maybe, but the pointer problem is manifesting there, and I can't really reason about this clever solution on top of not knowing if it even still works.

First order of business was to replace this with a dumber alternative just making wrapper functions for different numbers of arguments and switching between them based on arguments passed to objc_msgSend. This eliminates this mysterious hack as a part of the problem and makes it easier to reason about.

The next part was trickier. Go will not let you pass a pointer to data that contains another Go pointer into C. This might mean some more work later on, but my proof of concept code didn't have anything like this. The closest thing was a struct of some floats, but no pointers there. After some experimentation and finding posts about similarish problems with this rule, I learned that some of the internal data structures Go uses include pointers that set off this panic. The case I found a post on was with slices, but what I found was the problem was using empty interfaces.

All arguments passed into the Go-land version of objc_msgSend have to be empty interface because they could be anything. We do a type switch that asserts them into a particular type and set up a proper unsafe pointer for them. However, with structs there is a problem: we can't type assert to every possible struct type. We use reflection to even determine it's a struct in the first place, but even with reflection we can't get a version of the value that isn't an empty interface.

Here's why this is a problem. Empty interfaces seem like raw pointers, but they internally maintain their underlying type. An interface value is a pointer to two pointers, one to the value and one to type data. This sets off the panic.

Eventually some experimentation led to other problems that led to posts that ultimately solved the problem. This piece of code unpacks an empty interface value into a struct that represents the internal data structure (which could change in the future! But probably won't) and grabs the pointer to the value.

func rawPtr(v interface{}) unsafe.Pointer {
    type h struct {
        t unsafe.Pointer
        p unsafe.Pointer
    }
    return (*h)(unsafe.Pointer(&v)).p
}
Enter fullscreen mode Exit fullscreen mode

It's obvious in hindsight. Once I had this, I was able to get the proof of concept code to run. It didn't entirely work, a window was not displayed, however there were no explicit errors and an NSApplication was started as I could see it in the task switcher and it had an empty OS X menu. That's progress!

Top comments (0)