objc

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2026 License: BSD-3-Clause Imports: 4 Imported by: 0

README

go-macos/objc

CI Go Reference Go 1.26.4

A pure-Go (CGO_ENABLED=0) bridge to the macOS Objective-C runtime, reached entirely through ebitengine/puregodlopen + 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)
Runtime classes RegisterClass, MethodDef
Strings NSString, GoString
Objects / dictionaries Stringify, DictToMap, MapToDict
Frameworks Load + Foundation / AppKit / WebKit / CoreFoundation / Security / LibSystem
Scopes AutoreleasePool
Run loops App, RunApp, Run(ctx, class, setup) + Runner
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
        }},
    })

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.

Documentation

Overview

Package objc is a pure-Go (CGO_ENABLED=0) bridge to the macOS Objective-C runtime. It reaches the OS entirely through github.com/ebitengine/purego — dlopen + objc_msgSend — so it links with no cgo and no shelling out to osascript.

It exists to end the copy-paste of the same Objective-C plumbing across the fleet's CGO=0 macOS code (go-macos/notify, go-widgets/tray, the go-news-reader and go-reddit readers, …): every one of them independently re-implemented selector/class lookup, NSString<->Go bridging, framework dlopen, autorelease-pool handling and a run loop. This package is the single shared home for those primitives.

Surface

  • Sel / GetClass / ClassID: cached selector and class lookup.
  • [ID.Send] and the generic Send: typed objc_msgSend helpers (the generic form carries float64/CGFloat and struct returns through the correct calling convention).
  • RegisterClass: define an Objective-C class at runtime from Go method closures.
  • NSString / GoString: NSString<->Go string conversion. GoString copies via -getCString:maxLength:encoding: (never a raw -UTF8String pointer deref, which trips go vet's unsafeptr check).
  • Stringify / DictToMap / MapToDict: NSObject/NSDictionary helpers.
  • Load plus the framework-path constants (Foundation, AppKit, WebKit, CoreFoundation, LibSystem): idempotent dlopen.
  • AutoreleasePool: run a closure inside an NSAutoreleasePool scope.
  • App / RunApp: the shared NSApplication and its [NSApp run] loop.
  • Run: a Foundation run-loop runner pinned to a locked OS thread, with a task queue serviced on that thread — for observer-driven Foundation work (e.g. NSDistributedNotificationCenter).

Portability

Every exported symbol is defined on all platforms so consumers cross-compile. On non-darwin GOOS the runtime entry points report ErrUnsupported (or return a zero ID); the OS-independent logic — the selector/class caches, name validation and the string-marshalling core — stays fully functional and testable there.

Index

Constants

View Source
const (
	Foundation     = "/System/Library/Frameworks/Foundation.framework/Foundation"
	AppKit         = "/System/Library/Frameworks/AppKit.framework/AppKit"
	WebKit         = "/System/Library/Frameworks/WebKit.framework/WebKit"
	CoreFoundation = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"
	Security       = "/System/Library/Frameworks/Security.framework/Security"
	LibSystem      = "/usr/lib/libSystem.B.dylib"
)

Standard framework and dylib paths for Load.

Variables

View Source
var (
	// ErrUnsupported is returned by the runtime entry points on non-darwin
	// platforms (the whole runtime is macOS-only).
	ErrUnsupported = errors.New("objc: unsupported on this platform (darwin only)")
	// ErrEmptyName is returned when a selector or class name is empty.
	ErrEmptyName = errors.New("objc: empty name")
	// ErrNameHasNUL is returned when a selector or class name contains a NUL
	// byte (the C runtime terminates names at the first NUL, so an embedded one
	// would silently truncate).
	ErrNameHasNUL = errors.New("objc: name contains NUL byte")
	// ErrDlopen wraps a framework dlopen failure from [Load].
	ErrDlopen = errors.New("objc: dlopen failed")
)

Errors reported by the package. They are stable and may be tested with errors.Is.

Functions

func AutoreleasePool

func AutoreleasePool(fn func())

AutoreleasePool runs fn inside a fresh NSAutoreleasePool, draining the pool when fn returns (even if it panics). Use it around a burst of autoreleased allocations on a thread that has no ambient pool.

func DictToMap

func DictToMap(dict ID) map[string]string

DictToMap flattens an NSDictionary to map[string]string, rendering both keys and values with Stringify. The nil dictionary yields an empty map.

func GoString

func GoString(id ID) string

GoString copies an NSString's UTF-8 bytes into a Go-owned buffer and returns them. It returns "" for the nil object or an empty string. It fills a Go slice via -getCString:maxLength:encoding: rather than dereferencing the ObjC-owned -UTF8String pointer, so it stays free of any uintptr->Pointer arithmetic (the buffer handed to ObjC is a live Go pointer the collector tracks).

func Load

func Load(paths ...string) error

Load dlopens each of paths (idempotently — dlopen refcounts, and a repeat open of an already-resident framework is cheap). It returns the first failure wrapped in ErrDlopen. Use the framework-path constants, e.g. Load(Foundation, AppKit).

func Run

func Run(ctx context.Context, runnerClass Class, setup func(r *Runner)) error

Run drives a Foundation run loop on the calling goroutine — which it pins to its OS thread — until ctx is cancelled, then returns ctx.Err(). It creates an instance of runnerClass (register it with RegisterClass), calls setup with the Runner once the instance exists (attach your observers there, using r.Object() and r.Submit), and services work queued through r.Submit on this thread. It is the mechanism behind observer-driven Foundation delivery such as NSDistributedNotificationCenter, which requires a running CFRunLoop.

runnerClass must implement a no-op keepAlive: method (a repeating timer targets it to keep the loop from busy-spinning before observers attach). Call Run on a dedicated goroutine; it blocks for the lifetime of ctx.

func RunApp

func RunApp(policy int)

RunApp sets the shared application's activation policy and enters the AppKit run loop ([NSApp run]), which blocks until the application terminates. It must be called on the process main OS thread. policy is an NSApplicationActivationPolicy value (0 = Regular, 1 = Accessory/menu-bar).

func Send

func Send[T any](id ID, sel SEL, args ...any) T

Send sends selector sel to id with args and returns the result typed as T. Use it when the message returns a scalar or a by-value struct (e.g. a CGFloat via Send[float64] or an NSRect via a matching struct), so purego marshals the return through the correct calling convention. For an object (or ignored) result, use the inherited ID.Send, which returns an ID.

func Stringify

func Stringify(v ID) string

Stringify renders any object as a Go string: NSStrings directly, anything else through -description (so a non-string value degrades to its textual form rather than crashing the getCString path). The nil object yields "".

func ValidateName

func ValidateName(name string) error

ValidateName rejects names the C runtime cannot carry: empty (ErrEmptyName) or containing a NUL byte (ErrNameHasNUL, since objc_registerName / objc_getClass terminate at the first NUL and would silently truncate). It is a convenience for consumers that build selector or class names from untrusted input before passing them to Sel, GetClass or RegisterClass.

Types

type Class

type Class = objc.Class

Class is an Objective-C class object.

func GetClass

func GetClass(name string) Class

GetClass returns the class named name, caching the result. An unknown class (or the zero name) yields the zero class.

func RegisterClass

func RegisterClass(name string, super Class, methods []MethodDef) (Class, error)

RegisterClass defines a new Objective-C class named name with superclass super and the given instance methods, and returns it. Each method's Fn is a Go closure whose first two parameters are the receiver ID and the invoked SEL; RegisterClass wraps it into an IMP. It is the runtime-class mechanism behind delegates, timer targets and menu-action handlers. (Instance variables and protocols are intentionally omitted; the fleet's classes need neither.)

type ID

type ID = objc.ID

ID is an Objective-C object pointer (`id`); the zero value is nil.

func App

func App() ID

App returns the shared NSApplication instance (+[NSApplication sharedApplication]), creating it on first call.

func ClassID

func ClassID(name string) ID

ClassID returns the class named name as an ID, so class ("factory") messages (alloc, sharedApplication, …) can be sent to it directly.

func MapToDict

func MapToDict(m map[string]string) ID

MapToDict builds an autoreleased NSMutableDictionary of NSString->NSString from m.

func NSString

func NSString(s string) ID

NSString builds an autoreleased NSString from a Go string.

type IMP

type IMP = objc.IMP

IMP is an Objective-C method implementation pointer.

type MethodDef

type MethodDef = objc.MethodDef

MethodDef binds a selector to a Go closure for RegisterClass. Its Fn's first two parameters must be (ID, SEL); RegisterClass wraps it into an IMP.

type Runner

type Runner struct {
	// contains filtered or unexported fields
}

Runner is the live handle to a Run loop. Its methods are safe to call from any goroutine; each marshals its work onto the run-loop thread.

func (*Runner) Object

func (r *Runner) Object() ID

Object returns the runner's Objective-C instance (an instance of the class passed to Run). Observer callbacks and timers should target it. Valid only while Run is active.

func (*Runner) Submit

func (r *Runner) Submit(fn func())

Submit runs fn on the run-loop thread and blocks until it completes, so all Objective-C work stays on the one thread that owns the loop.

type SEL

type SEL = objc.SEL

SEL is an Objective-C selector.

func RegisterName

func RegisterName(name string) SEL

RegisterName is an alias for Sel. It carries purego's spelling so a call site migrated from github.com/ebitengine/purego/objc compiles unchanged while gaining the selector cache.

func Sel

func Sel(name string) SEL

Sel returns the selector named name, caching the result so repeated lookups are a map hit. An empty name yields the zero selector.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL