module objc
module lib/objc.mu
import "objc"
objc provides access to the Objective-C runtime on macOS: classes, selectors, message sends, and — through NSInvocation — methods whose signatures the integer-only FFI cannot express directly.
The FFI passes and returns 64-bit integer-class values only. That reaches most of Objective-C: object pointers, selectors, integers and C strings all travel as plain words. What it cannot reach are floating-point and structure arguments — on arm64 an NSRect is a homogeneous floating-point aggregate handed over in vector registers the dispatcher never loads. NSInvocation is the escape hatch: its API is entirely pointer-shaped (setArgument:atIndex: and getReturnValue: copy raw bytes to and from caller-owned memory), and it performs the register marshaling itself, inside AppKit. invoke() and invoke_ret() wrap that dance; to_bits()/from_bits() convert between µ numbers and the IEEE-754 doubles those byte buffers hold, in pure integer arithmetic — µ needs no float type to speak CGFloat.
Object pointers are plain µ integers here (including negative ones: modern runtimes hand out tagged pointers with the top bit set), so they can be written byte-wise into invocation argument buffers.
Imports
Functions
cls#
cls returns the class object for a class name (cached), 0 when the class does not exist, or an error when the runtime is unavailable.
Source lib/objc.mu:72
fn cls(name) {
ok := init()
if is_error(ok) {
return ok
}
got := _state["classes"][name]
if got != nil {
return got
}
c := dlcall(_state["getcls"], {"ret": "i64", "args": ["cstr"]}, name)
if is_error(c) {
return c
}
_state["classes"][name] = c
return c
}drain#
drain drains an autorelease pool created by pool(). The pool itself is consumed: create a fresh one afterwards.
Source lib/objc.mu:331
fn drain(p) {
return send(p, "drain")
}from_bits#
from_bits converts IEEE-754 double bits to a µ integer, rounding halves up. Screen geometry is what these doubles carry, so the nearest pixel is the honest answer; denormals collapse to 0, infinities and NaN answer an error.
Source lib/objc.mu:466
fn from_bits(bits) {
if bits == 0 {
return 0
}
neg := bits >> 63 & 1
exp := bits >> 52 & 2047
mant := bits & (1 << 52 - 1)
if exp == 2047 {
return error("objc.from_bits: infinity or NaN")
}
if exp == 0 {
return 0
}
m := mant + 1 << 52
e := exp - 1075
v := 0
if e >= 0 {
if e > 10 {
return error("objc.from_bits: value too large")
}
v = m << e
} else {
k := 0 - e
if k >= 63 {
return 0
}
half := 1 << (k - 1)
v = (m + half) >> k
}
if neg == 1 {
v = 0 - v
}
return v
}get_f64#
get_f64 reads the little-endian double at byte offset off as a µ number.
Source lib/objc.mu:532
fn get_f64(b, off) {
bits := 0
i := 7
while i >= 0 {
bits = bits << 8 | b[off + i]
i = i - 1
}
return from_bits(bits)
}init#
init loads the Objective-C runtime, AppKit and Foundation. Idempotent; every entry point calls it, so programs only call it to fail early. Returns true, or an error on platforms without an Objective-C runtime.
Source lib/objc.mu:29
fn init() {
if _state["ready"] {
return true
}
key := platform()
if !strings.starts_with(key, "darwin") {
return error("objc.init: no Objective-C runtime on " + key)
}
objc := dlopen("/usr/lib/libobjc.A.dylib")
if is_error(objc) {
return objc
}
// Loading the frameworks registers their classes with the runtime; the
// handles themselves are never used again.
appkit := dlopen("/System/Library/Frameworks/AppKit.framework/AppKit")
if is_error(appkit) {
return appkit
}
libc := dlopen("/usr/lib/libSystem.B.dylib")
if is_error(libc) {
return libc
}
_state["getcls"] = dlsym(objc, "objc_getClass")
_state["selreg"] = dlsym(objc, "sel_registerName")
_state["msgsend"] = dlsym(objc, "objc_msgSend")
_state["strlen"] = dlsym(libc, "strlen")
_state["memcpy"] = dlsym(libc, "memcpy")
_state["ready"] = true
return true
}invoke#
invoke calls a method through NSInvocation: each element of argbufs is a buffer holding the raw bytes of one argument (starting at index 2 — self and _cmd are set from receiver and selname). Returns the invocation after firing it, so a caller can pull the return value out, or an error.
Source lib/objc.mu:186
fn invoke(receiver, selname, argbufs) {
s := sel(selname)
if is_error(s) {
return s
}
msig := send(receiver, "methodSignatureForSelector:", s)
if is_error(msig) {
return msig
}
if msig == 0 {
return error("objc.invoke: no method " + selname)
}
nsinv := cls("NSInvocation")
if is_error(nsinv) {
return nsinv
}
inv := send(nsinv, "invocationWithMethodSignature:", msig)
if is_error(inv) {
return inv
}
send(inv, "setSelector:", s)
send(inv, "setTarget:", receiver)
i := 0
while i < len(argbufs) {
r := send(inv, "setArgument:atIndex:", argbufs[i], i + 2)
if is_error(r) {
return r
}
i = i + 1
}
r := send(inv, "invoke")
if is_error(r) {
return r
}
return inv
}invoke_ret#
invoke_ret calls a method through NSInvocation and copies its return value into a fresh buffer of retsize bytes (an NSRect is 32, a CGFloat or object is 8). Returns the buffer, or an error.
Source lib/objc.mu:242
fn invoke_ret(receiver, selname, argbufs, retsize) {
inv := invoke(receiver, selname, argbufs)
if is_error(inv) {
return inv
}
out := buf(retsize)
r := send(inv, "getReturnValue:", out)
if is_error(r) {
return r
}
return out
}nsstring#
nsstring returns an autoreleased NSString for a µ string.
Source lib/objc.mu:261
fn nsstring(s) {
c := cls("NSString")
if is_error(c) {
return c
}
return send(c, "stringWithUTF8String:", s)
}pool#
pool creates an NSAutoreleasePool. Cocoa parks autoreleased objects in the innermost pool; without one they leak. Drain and recreate per event-loop turn.
Source lib/objc.mu:312
fn pool() {
c := cls("NSAutoreleasePool")
if is_error(c) {
return c
}
p := send(c, "alloc")
if is_error(p) {
return p
}
return send(p, "init")
}put_f64#
put_f64 writes v (a µ integer) as a little-endian double at byte offset off.
Source lib/objc.mu:513
fn put_f64(b, off, v) {
bits := to_bits(v)
if is_error(bits) {
return bits
}
i := 0
while i < 8 {
b[off + i] = bits >> (i * 8) & 255
i = i + 1
}
return nil
}read_rect#
read_rect unpacks an NSRect buffer into a {x, y, w, h} map, rounding each coordinate to the nearest integer.
Source lib/objc.mu:559
fn read_rect(b) {
return {"x": get_f64(b, 0), "y": get_f64(b, 8), "w": get_f64(b, 16), "h": get_f64(b, 24)}
}rect#
rect packs {x, y, w, h} as an NSRect — 32 bytes of doubles — ready to hand to invoke() or any by-reference rect parameter.
Source lib/objc.mu:547
fn rect(x, y, w, h) {
b := buf(32)
put_f64(b, 0, x)
put_f64(b, 8, y)
put_f64(b, 16, w)
put_f64(b, 24, h)
return b
}sel#
sel returns the selector for a method name (cached), or an error when the runtime is unavailable.
Source lib/objc.mu:98
fn sel(name) {
ok := init()
if is_error(ok) {
return ok
}
got := _state["sels"][name]
if got != nil {
return got
}
s := dlcall(_state["selreg"], {"ret": "i64", "args": ["cstr"]}, name)
if is_error(s) {
return s
}
_state["sels"][name] = s
return s
}send#
send sends a message: objc_msgSend(receiver, selector, args...). Arguments map by µ type — integers pass through, nil and false are 0, true is 1, strings pass as C strings, buffers pass their byte address. The raw 64-bit result returns as a µ integer; interpret it per the method's contract. Only integer-class signatures belong here — anything with floats or structs goes through invoke()/invoke_ret().
Source lib/objc.mu:128
fn send(receiver, selname, args...) {
s := sel(selname)
if is_error(s) {
return s
}
types := ["i64", "i64"]
vals := [receiver, s]
i := 0
while i < len(args) {
a := args[i]
t := type(a)
if t == "STRING" {
types = append(types, "cstr")
vals = append(vals, a)
} else {
if t == "BUFFER" {
types = append(types, "ptr")
vals = append(vals, a)
} else {
if t == "INTEGER" {
types = append(types, "i64")
vals = append(vals, a)
} else {
if t == "BOOLEAN" {
types = append(types, "i64")
if a {
vals = append(vals, 1)
} else {
vals = append(vals, 0)
}
} else {
if t == "NULL" {
types = append(types, "i64")
vals = append(vals, 0)
} else {
return error("objc.send expects integer/string/buffer argument, got " + t)
}
}
}
}
}
i = i + 1
}
return dlcall(_state["msgsend"], {"ret": "i64", "args": types}, vals)
}to_bits#
to_bits returns the IEEE-754 double bit pattern of a µ integer. Every value up to 2^53 is exact; beyond that the low bits round the way doubles round.
Source lib/objc.mu:338
fn to_bits(v) {
if type(v) != "INTEGER" {
return error("objc.to_bits expects integer, got " + type(v))
}
if v == 0 {
return 0
}
neg := false
if v < 0 {
neg = true
v = 0 - v
}
k := 0
m := v
while m > 1 {
m = m >> 1
k = k + 1
}
mant := 0
if k <= 52 {
mant = v << (52 - k) - 1 << 52
} else {
mant = v >> (k - 52) - 1 << 52
}
bits := (1023 + k) << 52 | mant
if neg {
bits = bits | 1 << 63
}
return bits
}to_bits_ratio#
to_bits_ratio returns the IEEE-754 double bit pattern of num/den, correctly rounded (ties away from zero) — how a fractional CGFloat or NSTimeInterval is spelled without µ growing a float type: 3.5 seconds is to_bits_ratio(3500, 1000). Both arguments must be integers, |num| below 2^52 and den positive below 2^52, which every screen coordinate and every sane time interval is.
Source lib/objc.mu:386
fn to_bits_ratio(num, den) {
if type(num) != "INTEGER" || type(den) != "INTEGER" {
return error("objc.to_bits_ratio expects integers, got " + type(num) + "/" + type(den))
}
if den <= 0 {
return error("objc.to_bits_ratio expects positive denominator")
}
if num == 0 {
return 0
}
limit := 1 << 52
if num >= limit || 0 - num >= limit || den >= limit {
return error("objc.to_bits_ratio: operands too large")
}
neg := false
if num < 0 {
neg = true
num = 0 - num
}
// Normalise num/den into [1, 2), tracking the binary exponent, then long-
// divide 53 mantissa bits out one at a time.
e := 0
while num < den {
num = num * 2
e = e - 1
}
while num >= den * 2 {
den = den * 2
e = e + 1
}
frac := num - den
mant := 0
i := 0
while i < 52 {
frac = frac * 2
mant = mant * 2
if frac >= den {
mant = mant + 1
frac = frac - den
}
i = i + 1
}
frac = frac * 2
if frac >= den {
mant = mant + 1
if mant == 1 << 52 {
mant = 0
e = e + 1
}
}
bits := (1023 + e) << 52 | mant
if neg {
bits = bits | 1 << 63
}
return bits
}utf8#
utf8 returns the µ string contents of an NSString (empty for 0/nil).
Source lib/objc.mu:273
fn utf8(ns) {
if ns == 0 {
return ""
}
p := send(ns, "UTF8String")
if is_error(p) {
return p
}
if p == 0 {
return ""
}
n := dlcall(_state["strlen"], {"ret": "i64", "args": ["i64"]}, p)
if is_error(n) {
return n
}
if n == 0 {
return ""
}
b := buf(n + 1)
r := dlcall(_state["memcpy"], {"ret": "i64", "args": ["ptr", "i64", "i64"]}, b, p, n)
if is_error(r) {
return r
}
return str(b)
}Internal helpers
Underscore-prefixed names are implementation detail. They are listed so the module's source reads without surprises, not as API — they may change at any time.
| _state | — |