Skip to content

Native Mac APIs for Go. Soon to be renamed DarwinKit!

License

Notifications You must be signed in to change notification settings

buffuwei/macdriver

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MacDriver Logo

Native Apple APIs for Golang!

GoDoc Go Report Card @progriumHQ on Twitter Project Forum Sponsor Project

Aug 15, 2023: MacDriver is becoming DarwinKit, which increases API coverage by an order of magnitude and is an overall upgrade in quality and scope. It has been rewritten, reorganized, and definitely has breaking API changes. The legacy branch and previous releases are still available for existing code to work against. We're working towards a 0.5.0-preview release followed by a 0.5.0 release finalizing the new API and rename to DarwinKit.


DarwinKit lets you work with supported Apple frameworks and build native applications using Go. It makes developing simple applications simple. With XCode and Go 1.18+ installed, you can write this program in a main.go file:

package main

import (
	"runtime"

	"github.com/progrium/macdriver/macos"
	"github.com/progrium/macdriver/macos/appkit"
	"github.com/progrium/macdriver/macos/foundation"
	"github.com/progrium/macdriver/macos/webkit"
)

func init() {
	runtime.LockOSThread()
}

func main() {
	macos.Launch(func(app appkit.Application, delegate *appkit.ApplicationDelegate) {
		app.SetActivationPolicy(appkit.ApplicationActivationPolicyRegular)
		app.ActivateIgnoringOtherApps(true)

		url := foundation.URL_URLWithString("http://progrium.com")
		req := foundation.URLRequest_InitWithURL(url)
		frame := foundation.Rect{Size: foundation.Size{1440, 900}}

		config := webkit.NewWebViewConfiguration()
		wv := webkit.WebView_InitWithFrameConfiguration(frame, config)
		wv.LoadRequest(req)

		w := appkit.Window_InitWithContentRectStyleMaskBackingDefer(frame,
			appkit.ClosableWindowMask|appkit.TitledWindowMask,
			appkit.BackingStoreBuffered, false)
		w.SetContentView(wv)
		w.MakeKeyAndOrderFront(w)
		w.Center()

		delegate.SetApplicationShouldTerminateAfterLastWindowClosed(func(appkit.Application) bool {
			return true
		})
	})
}

Then in this directory run:

go mod init helloworld
go get github.com/progrium/macdriver@latest
go run main.go

You just made a macOS program without using XCode or Objective-C. Run go build to get an executable.

Although currently outside the scope of this project, if you wanted you could put this executable into an Application bundle. You could even add entitlements, then sign and notarize this bundle or executable to let others run it. It could theoretically even be put on the App Store. It could theoretically be put on an iOS, tvOS, or watchOS device, though you would have to use different APIs.

Caveats

  • You still need to know or learn how Apple frameworks work, so you'll have to use Apple documentation and understand how to translate Objective-C example code to the equivalent Go with DarwinKit.
  • Your programs link against the actual Apple frameworks, so XCode needs to be installed for the framework headers and any program built will use cgo.
  • Exceptions in frameworks will segfault, giving you both an Objective-C stacktrace and a Go panic stacktrace. You will be debugging a hybrid Go and Objective-C program.
  • Goroutines that interact with GUI objects need to dispatch operations on the main thread otherwise it will segfault.
  • You will be using two memory management systems. Framework objects are managed by the Objective-C memory manager and you will use Retain, Release, or Autorelease on them on top of considering Go memory management.
  • This is all tenable for simple programs, but these are the reasons we don't recommend large/complex programs using DarwinKit.

Examples

macos/_examples/largetype - A Contacts/Quicksilver-style Large Type utility in under 80 lines: largetype screenshot

macos/_examples/pomodoro - A menu bar pomodoro timer in under 80 lines: pomodoro gif

More examples

How it works

After acquiring NeXT Computer in the 90s, Apple used NeXTSTEP as the basis of their software stack, which was written in Objective-C. Unlike most systems languages with object orientation, especially of the C lineage, Objective-C implements OOP as a runtime library. The weird OOP specific syntax is effectively rewritten into C calls to libobjc, which is a normal C library implementing the Objective-C runtime. This runtime could be used to bring OOP to any language that can make calls to C code. It also lets you interact with objects and classes registered by other libraries, such as the Apple frameworks.

At the heart of DarwinKit is a package wrapping the Objective-C runtime using cgo and libffi. This is actually all you need to interact with Objective-C objects and classes, it'll just look like this:

app := objc.CallMethod[objc.Object](objc.GetClass("NSApplication"), objc.Sel("sharedApplication"))
objc.CallMethod[objc.Void](app, objc.Sel("run"))

So we wrap these calls in a Go API that lets us write code like this:

app := appkit.NSApplication_SharedApplication()
app.Run()

These bindings are great, but we need to define them for every API we want to use. Presently, Apple has around 200 frameworks of nearly 5000 classes with 77k combined methods and properties. Not to mention all the constants, functions, structs, unions, and enums we need to work with those objects.

So DarwinKit generates its bindings. This is the hard part. Making sure the generation pipeline accurately produces usable bindings for all possible symbols is quite an arduous, iterative, manual process. Then since we're moving symbols that lived in a single namespace into Go packages, we have to manually decouple dependencies between them enough to avoid circular imports. If you want to help add frameworks, this whole process is documented here.

Objects in Objective-C are passed around as typed pointer values. When we receive an object from a method call in Go, the objc package receives it as a pointer, which it first puts into an unsafe.Pointer. The bindings for a class define a struct type that embeds an objc.Object struct, which contains a single field to hold the unsafe.Pointer. So unless working with a primitive type, you're working with an unsafe.Pointer wrapped in an objc.Object wrapped in a struct type that has the methods for the class of the object of the pointer.

If you have questions, feel free to ask in the discussion forums.

Thanks

This project was inspired by and originally based on packages written by Mikkel Krautz. The latest version is based on packages written by Dong Liu.

License

MIT

About

Native Mac APIs for Go. Soon to be renamed DarwinKit!

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Go 90.1%
  • Objective-C 9.3%
  • Shell 0.2%
  • HTML 0.2%
  • Makefile 0.1%
  • GLSL 0.1%