Files
sx/library/modules/std/objc.sx
agra a1736f3213 ffi M1.2 A.5: synthesized +alloc IMP + ensureCRuntimeDecl helper
For every sx-defined #objc_class, emit a C-callconv +alloc IMP
that the Obj-C runtime calls when '[Cls alloc]' fires (from sx
code, UIKit instantiation, Info.plist principal class, etc.):

  +alloc IMP (cls: Class, _cmd: SEL) -> id
      instance = class_createInstance(cls, 0)
      state    = malloc(STATE_SIZE)
      memset(state, 0, STATE_SIZE)
      object_setIvar(instance, load(@__<Cls>_state_ivar), state)
      return instance

STATE_SIZE = max(typeSizeBytes(state struct), 1) — always at
least one byte so the ivar is never null after +alloc returns.

The IMP is registered on the METACLASS (class methods live there
— every Class object's isa points to the metaclass) in emit_llvm's
class-pair init constructor:

  metaclass = object_getClass(cls)
  sel_alloc = sel_registerName("alloc")
  class_addMethod(metaclass, sel_alloc, alloc_imp, "@@:")

That override wins over NSObject's default +alloc; runtime
instantiations get the __sx_state ivar bound automatically.

Per-instance allocator binding (the plan's full design — store
the Allocator value in the state struct so -dealloc frees through
the same one) is deferred. libc malloc/free is fine for v1; we'll
upgrade once Month 4's autoreleasepool + ARC ops shake out.

REFACTOR: collapsed five duplicate 'get<Name>Fid' helpers and
their cache fields (object_getIvar, object_setIvar,
class_createInstance, malloc, memset) into a single
'ensureCRuntimeDecl(name, params, ret) -> FuncId'. The helper
checks for an existing decl by name first (avoids the
'class_createInstance.1' duplicate-symbol crash when stdlib's
'#foreign' decl is already in the module). One helper instead
of one-per-function = ~150 lines deleted.

object_getIvar / object_setIvar added to stdlib std/objc.sx
so user code can use them too (146 exercises object_getIvar
to verify __sx_state was bound to a non-null state pointer
after +alloc).

146-objc-class-alloc-roundtrip.sx end-to-end against macOS:
'[SxFoo alloc]' returns non-null AND object_getIvar(instance,
__sx_state) returns the state ptr. Real Obj-C runtime, no
mocks.

175 example tests pass (+1). zig build test green.
2026-05-25 23:17:30 +03:00

96 lines
4.9 KiB
Plaintext

// Obj-C runtime FFI primitives.
//
// objc_msgSend has the standard ARM64 calling convention (no varargs path).
// Each call site must invoke through a function pointer of the *exact*
// argument and return shape. The idiom:
//
// msg_fn : (recv: *void, sel: *void, arg: [*]u8) -> *void = xx objc_msgSend;
// result := msg_fn(receiver, selector, c_string);
// ─── Obj-C primitive type aliases ───────────────────────────────────────
// Named stand-ins for the three opaque Obj-C runtime types. They all
// resolve to `*void` at the LLVM layer (no runtime cost) but improve
// readability in foreign-class declarations and call sites.
//
// id — any Obj-C instance pointer
// Class — a class object pointer
// SEL — a registered selector
//
// `Class(T)` parameterization (phantom-typed, with `#extends`-aware
// covariance) is a follow-up — needs compiler-level type-check support.
// For now, `Class` alone is the only form; assignments are not checked
// against the referent's class hierarchy.
id :: *void;
Class :: *void;
SEL :: *void;
// Apple's `BOOL` is a signed char (NOT sx's built-in `bool`, which is
// LLVM `i1`). Obj-C method signatures that take or return `BOOL` cross
// the FFI boundary as `s8`.
BOOL :: s8;
// On macOS libobjc is auto-loaded by libSystem; on iOS it isn't, so we
// link it explicitly. Foundation registers NSString etc. with the runtime,
// also auto-loaded on macOS and required as an explicit framework on iOS.
objc :: #library "objc";
#framework "Foundation";
objc_getClass :: (name: [*]u8) -> *void #foreign objc;
objc_lookUpClass :: (name: [*]u8) -> *void #foreign objc;
sel_registerName :: (name: [*]u8) -> *void #foreign objc;
class_createInstance :: (cls: *void, extra: usize) -> *void #foreign objc;
object_getClass :: (obj: *void) -> *void #foreign objc;
object_getIvar :: (obj: *void, ivar: *void) -> *void #foreign objc;
object_setIvar :: (obj: *void, ivar: *void, val: *void) #foreign objc;
// Declared with the simplest non-variadic shape. Cast per call site.
objc_msgSend :: (recv: *void, sel: *void) -> *void #foreign objc;
// ─── Dynamic class registration ─────────────────────────────────────────
// Define a new Obj-C class at runtime: allocate the pair, attach methods +
// protocols, then finalize with `objc_registerClassPair`. The class is then
// usable via `class_createInstance` and Obj-C dispatch.
//
// IMPs (method implementations) are function pointers with the implicit
// Obj-C method shape: `(self: *void, _cmd: *void, ...args) -> ret` with
// `callconv(.c)` so they land args in the standard C registers.
//
// Method type encoding strings follow Apple's runtime DSL:
// v = void c = char/BOOL i = int l = long f = float d = double
// @ = id (object) : = SEL # = Class
// Return type comes first, then receiver (`@`), then `_cmd` (`:`), then args.
// Examples:
// "v@:" -> void method(id, SEL)
// "c@:" -> BOOL method(id, SEL)
// "@@:@" -> id method(id, SEL, id)
// "B@:@@" -> BOOL method(id, SEL, id, id)
objc_allocateClassPair :: (super: *void, name: [*]u8, extra: usize) -> *void #foreign objc;
class_addMethod :: (cls: *void, sel: *void, imp: *void, types: [*]u8) -> bool #foreign objc;
class_addProtocol :: (cls: *void, proto: *void) -> bool #foreign objc;
objc_getProtocol :: (name: [*]u8) -> *void #foreign objc;
objc_registerClassPair :: (cls: *void) -> void #foreign objc;
// Foundation C-API helpers (Foundation is already linked above via #framework).
// NSLog takes an NSString format; the variadic tail is not exposed here.
NSLog :: (fmt: *void) #foreign;
// ─── Convenience helpers ────────────────────────────────────────────────
// These hide the typed-fn-pointer cast for the most common shapes. They
// re-register selectors per call — if you're in a tight loop, cache the SEL.
// Wrap a C string in an autoreleased NSString.
ns_string :: (s: [*]u8) -> *void {
cls := objc_getClass("NSString".ptr);
sel := sel_registerName("stringWithUTF8String:".ptr);
fn_ptr : (*void, *void, [*]u8) -> *void callconv(.c) = xx objc_msgSend;
return fn_ptr(cls, sel, s);
}
// View an NSString's bytes as a C string. The returned pointer's lifetime is
// tied to the NSString; don't free it.
c_string :: (ns: *void) -> [*]u8 {
sel := sel_registerName("UTF8String".ptr);
fn_ptr : (*void, *void) -> [*]u8 callconv(.c) = xx objc_msgSend;
return fn_ptr(ns, sel);
}