BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News Using Go in Native macOS Apps with MacDriver

Using Go in Native macOS Apps with MacDriver

This item in japanese

MacDriver is a recent project aiming to enable interoperability between Go and Objective-C. This will make it possible to write macOS, and potentially iOS apps using Go, as well as reuse Go code in macOS apps written in Objective-C or Swift.

The foundation of MacDriver is the objc package, which is a Go wrapper around the Objective-C runtime. It enables accessing any class or convenience methods available in the runtime, which includes the possibility of creating or extending classes at runtime, just as you could do in Objective-C proper. The following is an example of how you can define and instantiate an AppDelegate class as required for any macOS and iOS app:

cls := objc.NewClass("AppDelegate", "NSObject")
cls.AddMethod("applicationDidFinishLaunching:", func(app objc.Object) {
	fmt.Println("Launched!")
})
objc.RegisterClass(cls)

delegate := objc.Get("AppDelegate").Alloc().Init()
app := objc.Get("NSApplication").Get("sharedApplication")
app.Set("delegate:", delegate)
app.Send("run")

In addition to the objc package, MacDriver includes bindings for several frameworks belonging to the macOS SDK, with the aim of making it possible to use their APIs as if they were native Go libraries. Those include the core package, corresponding to the Foundation framework; the cocoa package, corresponding to AppKit; and webkit, corresponding to the WebKit framework. At the time being, only a limited number of classes are supported for each framework but new classes and packages will be added as needed, says Jeff Lindsay. Bindings are written by hand, though, which may slow the process down.

The last component of MacDriver is a bridging system enabling calling Go code from native macOS applications. MacDriver takes the approach to run Go code in a separate process and provides higher-level abstractions to declaratively describe and modify structs that can be copied over.

InfoQ has taken the chance to speak with MacDriver creator Jeff Lindsay.

InfoQ: What motivated you to create MacDriver?

Jeff Lindsay: I've been trying to get Go bindings to the Objective-C runtime for years now, specifically to work with Apple APIs and frameworks. Not just Cocoa for native GUI programming, but any Apple framework. I've seen a few one-off bindings for specific APIs like for creating menu bar extras or creating basic windows, but eventually they just weren't sufficient. They would often try to be cross-platform, which is nice, but then not expose certain platform specific APIs, or worse they seem to assume they won't be used together. A lot of GUI frameworks, Cocoa included, want to own the main thread and each library would sort of assume exclusive ownership of it.

The great thing about Objective-C is that it has a dynamic runtime in the form of a C library. Making general bindings for that would open up access to all APIs, including low level graphics APIs, and their Neural Engine API that before the M1 Macs was only available on iOS. Since all of Apple's platforms are built on the Objective-C runtime, it also opens up the possibility to write Go code that uses frameworks on iOS, tvOS, etc.

In theory, it being a C library would make this project straightforward. Go already has cgo for relatively easy integration and linking against C libraries. The problem was that it didn't support calling variadic C functions, and the most used function in the Objective-C runtime was the function to make method calls, which is variadic. I finally found an abandoned project that figured out a way around this, but it had broken in several ways as the Go compiler matured and got stricter. I finally sat down and figured out how to get it working again, but then left it for about a year before I really needed to use it.

My primary use case wasn't to build full fledged native apps. It was actually to have an Electron-style webview window where I could build most of an app's interface, but then also have native menus, systray icons, system notifications, etc. The kinds of cross-platform features Electron exposes. Then to have access (or potential access) to all other native APIs made this approach feel it had the most bang for buck. I wouldn't actually call it a GUI framework, but it provides a great base layer to build one on. Similarly, although MacDriver is very specific to Apple, I'm working on exposing a subset of APIs as declarative resources that could have Linux and Windows counterparts for a cross-platform Electron-esque library. Those would be separate projects.

InfoQ: How mature is MacDriver to use it for production apps?

Lindsay: At this moment I wouldn't call it production-ready. It is usable, but the difficult part of these kinds of systems is maintaining harmony between two different memory management systems sharing data. It's mostly there, but myself and others are finalizing APIs for working with NSAutoreleasePool and documenting memory safety pitfalls before I even cut the first 0.1 release.

There are actually three layers to MacDriver. First, the meat of MacDriver; you have the Objective-C runtime bindings, which let you call anything in Objective-C dynamically. This gives it a sort of RPC-style API, which is powerful but not very Go feeling or type-safe. The second, optional layer consists of Go packages that provide types and methods that wrap specific Apple framework APIs. I originally intended to generate these, but based on my experience it's always better to start by hand to find good API conventions and solve specific problems (methods that return structs, for example, are treated specially) before getting into code generation. Apple publishes schema data for their APIs in BridgeSupport files intended for this sort of thing, but over the years they seem to have not made it a priority to keep them up-to-date. So for now, me and others are just adding these by hand as needed. Luckily this is a near 1:1 mapping of APIs, so there's nearly zero "design" work. You can just read Apple docs and add wrappers for new methods and types.

The third layer is experimental. That's where I'm playing with a totally different API focusing on declaratively describing common cross-platform resources like windows, menus, notifications, etc. This would be the basis for a common API across potential Windows and Linux equivalent projects.

So there's a lot of power here, but it's very early and welcome to pretty easy and significant contributions.

InfoQ: Do you have any plans to extend supported frameworks and target iOS app development?

Lindsay: I would love to, but since this is just one building block of what I'm ultimately up to, I think that's up to the community and new contributors for now.

InfoQ: What’s on MacDriver roadmap to reach 1.0?

Lindsay: That's a good question! I actually don't have a great answer other than stability, docs, process, more wrappers, and clearer overall scope. I started a discussion on GitHub to brainstorm ideas of what people might want (and are willing to help with) for a 1.0 release. Unfortunately I'm no Objective-C expert (though thoroughly obsessed with its history), so I'm pretty dependent on people with more experience on some of the more nuanced areas. If you're an Objective-C developer also interested in Go, I'd love your help!

MacDriver can be forked on GitHub.

Rate this Article

Adoption
Style

BT