Skip to content
go-macosPublic

About

Pure-Go CGO=0 macOS Objective-C runtime bridge (purego): selector/class lookup, NSString interop, framework dlopen, autorelease pool, run loops. No cgo, no osascript.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

31 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-macos/objc

CI Go Reference Go 1.27.1 License: BSD-3-Clause

A pure-Go (CGO_ENABLED=0) bridge to the macOS Objective-C runtime, reached entirely through ebitengine/purego — dlopen + objc_msgSend, no cgo, no shelling out to osascript.

It is the single shared home for the Objective-C plumbing that the fleet's CGO=0 macOS code kept re-implementing: selector/class lookup, NSString↔Go bridging, framework dlopen, autorelease-pool handling and run loops.

Surface

Area API
Selector / class lookup (cached) Sel, RegisterName, GetClass, ClassID
Messaging ID.Send, generic Send[T] (scalars, CGFloat, structs)
Hot-path messaging Msg0, Msg1, Msg2, Msg3 (integer and pointer arguments, integer result)
Runtime classes RegisterClass, RegisterClassWithProtocols, GetProtocol, MethodDef
Strings NSString, GoString
Objects / dictionaries Stringify, DictToMap, MapToDict
Geometry NSPoint, NSSize, NSRect
Frameworks Load, Open + Foundation / AppKit / WebKit / CoreFoundation / Security / LibSystem
Scopes AutoreleasePool
Application App, RunApp, SetApplicationIconImage
Leaving a loop RunAppLoop(policy, stop), StopApp, WakeMainRunLoop
Run loops Run(ctx, class, setup) + Runner, PumpRunLoop
Main-thread hop DispatchMain (libdispatch dispatch_async onto the main queue)
Blocks Block, NewBlock, Block.Release, Block.Copy
Validation ValidateName
import "github.com/go-macos/objc"

objc.Load(objc.Foundation, objc.AppKit)

s := objc.GoString(objc.NSString("héllo"))                 // "héllo"
n := objc.Send[float64](objc.NSString("3.5"), objc.Sel("doubleValue")) // 3.5

cls, _ := objc.RegisterClass("MyTarget", objc.GetClass("NSObject"),
    []objc.MethodDef{
        {Cmd: objc.Sel("fire:"), Fn: func(self objc.ID, _ objc.SEL, sender objc.ID) {
            // handle sender
        }},
    })

ID.Send takes its arguments as any and calls objc_msgSend through reflection: 5 allocations with no argument, 7 with one. For a message sent every frame, Msg0 to Msg3 go straight to purego.SyscallN and cost 1. They take integer or pointer arguments only (a pointer written as uintptr(unsafe.Pointer(p)) in the call) and return an integer; anything with a float or a struct still goes through Send. AutoreleasePool uses them.

n := objc.Msg0(s, objc.Sel("length"))
objc.Msg3(enc, objc.Sel("setBuffer:offset:atIndex:"), buf, 0, 2)

Asynchronous Cocoa APIs take a completion handler block. The Go function's first parameter is the block itself; the rest are the block's arguments:

block := objc.NewBlock(func(b objc.Block, content objc.ID, err objc.ID) {
    defer b.Release() // the block is retained until released
    if err != 0 {
        log.Println(objc.Stringify(err))
        return
    }
    use(content)
})
objc.ClassID("SCShareableContent").
    Send(objc.Sel("getShareableContentWithCompletionHandler:"), block)

Threads

AppKit's rules about threads are not advice, and getting them wrong here does not produce an error — it produces a trap inside AppKit with nothing of your program on the stack. Two of them are worth knowing before you write anything against this package.

App()'s first call binds the application to the thread that makes it. +[NSApplication sharedApplication] creates the shared application and ties it to whoever asks first. Ask from a goroutine that is not on the process main OS thread and everything appears to work — the application is returned, messages to it are answered — until the main thread tries to take an event, which dies with SIGTRAP inside -nextEventMatchingMask:. Only the first call is bound by this: once the application exists, messaging it from another goroutine is fine. RunApp and RunAppLoop must be on that thread anyway, so a program that starts with either is already right.

runtime.LockOSThread() pins the current thread, not the main one. It is the obvious thing to reach for and it does not get you to the main thread; a goroutine other than the one running main() is on that thread only by luck. Measured in this package's own tests: a locked test goroutine lands there on most runs and not all of them, which is worse than never — it makes a fixture that cannot work pass most of the time, and it cost one CI red that took a day to read. WakeMainRunLoop stops the main thread's loop while PumpRunLoop runs the current thread's, so if you are not on that thread the two are different loops.

If you need the main thread from somewhere else, the honest ways are DispatchMain (which needs the main thread to be running a loop, so it is for a program, not a test) or, in a test, a TestMain that keeps the main goroutine locked and serves a channel — see mainthread_darwin_test.go, which also asserts that the channel really reaches that thread rather than assuming it.

Design

  • On darwin the runtime types (ID, SEL, Class, IMP, MethodDef) are aliases of the corresponding purego/objc types, so method closures satisfy purego's strict NewIMP (id, SEL) receiver check and existing purego-based call sites can switch their import to this package and keep compiling.
  • On non-darwin the same symbols exist as uintptr-width types and stubs that report [ErrUnsupported], so consumers cross-compile. The OS-independent core (selector/class caches, name validation, the GoString marshalling logic and Load) stays fully functional and is covered 100% on every lane through injection seams.
  • The darwin C/objc calls sit behind fake-injection seams, so their error branches are reachable in tests; the real objc_msgSend / NSString / RegisterClass / run-loop paths are verified on-device in CI.

License

BSD-3-Clause © the go-macos/objc authors.

About

Pure-Go CGO=0 macOS Objective-C runtime bridge (purego): selector/class lookup, NSString interop, framework dlopen, autorelease pool, run loops. No cgo, no osascript.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages