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#

fn cls(name)

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#

fn drain(p)

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#

fn from_bits(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#

fn get_f64(b, off)

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#

fn 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#

fn invoke(receiver, selname, argbufs)

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#

fn invoke_ret(receiver, selname, argbufs, retsize)

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#

fn nsstring(s)

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#

fn 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#

fn put_f64(b, off, v)

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#

fn read_rect(b)

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#

fn rect(x, y, w, h)

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#

fn sel(name)

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#

fn send(receiver, selname, args...)

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#

fn to_bits(v)

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#

fn to_bits_ratio(num, den)

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#

fn utf8(ns)

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