ffi 3.1: Cls.static_method(args) lowers to objc_msg_send on the class object

Implementation half of the Phase 3.1 cadence step.
`lowerForeignStaticCall` for `#objc_class` / `#objc_protocol` runtimes
no longer bails; it routes through a new `lowerObjcStaticCall` helper
that loads the class object from a module-scoped cached slot (populated
once per module via `objc_getClass`) and dispatches `objc_msg_send`
with the same selector-mangling as Phase 3.0's instance dispatch.

Three pieces:

1. `Module.objc_class_cache` — parallel to `objc_selector_cache`,
   insertion-ordered list of (class_name, slot_GlobalId) so the
   constructor that calls `objc_getClass` per slot at module load
   is deterministic. `lookupObjcClass` / `appendObjcClass` accessors.
2. `internObjcClassObject` in lower.zig — get-or-create a
   `OBJC_CLASSLIST_REFERENCES_<Cls>` global pointer; matches clang's
   naming convention. `lowerObjcStaticCall` reuses
   `deriveObjcSelector` from 3.0 for the selector, loads the class
   slot, and emits `objc_msg_send(class_obj, sel, args)`.
3. `emitObjcClassInit` in emit_llvm.zig — companion to
   `emitObjcSelectorInit`. Walks `objc_class_cache`, synthesizes a
   constructor `__sx_objc_class_init` that calls `objc_getClass(name)`
   per slot, registers in `@llvm.global_ctors` for AOT (extending the
   existing array if the selector init already created it), and
   injects a direct call into main's prelude after any prior init
   calls so the ORC JIT path runs it too.

Surface form is `.` (`NSObject.class()`) matching JNI's `Alias.new(...)`
convention rather than the plan's notional `::` — avoids extending the
parser for a new postfix operator with no other use case.

Test `examples/ffi-objc-dsl-05-static.sx` exercises NSObject's
`+class` and `+description` class methods via the new syntax, asserts
both return non-null. NSObject is always available at module-load,
unlike runtime-created test classes that wouldn't exist yet when
the class-init constructor runs.

164/164 tests; chess builds + runs clean on all three platforms.
This commit is contained in:
agra
2026-05-25 16:23:24 +03:00
parent b07ee53a39
commit 8406cc1fed
7 changed files with 281 additions and 38 deletions

View File

@@ -4,44 +4,36 @@
// selector is derived by the same default mangling as Phase 3.0
// (`stringWithUTF8String_(s)` → "stringWithUTF8String:").
//
// Mirrors the JNI surface (`Alias.new(...)` etc.); the lowering
// disambiguates static vs instance by looking at `method.is_static` on
// the foreign-class member.
// Mirrors JNI's static-dispatch surface (`Alias.new(...)` etc.); the
// lowering disambiguates static vs instance by looking at
// `method.is_static` on the foreign-class member.
//
// Pre-3.1: `lowerForeignStaticCall` only handles JNI runtime + the
// `new` constructor; any other static call bails. Snapshot pins the
// bail diagnostic.
// Uses NSObject because the cached class slot is populated by a
// constructor at module-load — runtime-created test classes wouldn't
// exist yet when `objc_getClass` runs. NSObject is always available
// on macOS via libobjc.
#import "modules/std.sx";
#import "modules/compiler.sx";
#import "modules/std/objc.sx";
SxProbeStatic :: #foreign #objc_class("SxProbeStatic") {
static answer :: () -> s32;
static add :: (a: s32, b: s32) -> s32;
}
answer_imp :: (self: *void, _cmd: *void) -> s32 callconv(.c) {
42;
}
add_imp :: (self: *void, _cmd: *void, a: s32, b: s32) -> s32 callconv(.c) {
a + b;
NSObject :: #foreign #objc_class("NSObject") {
// `+(Class)class` — niladic, name verbatim, selector = "class".
// Returns the class object itself.
static class :: () -> *void;
// `+(NSString *)description` on the class returns a description
// string. Niladic, selector = "description".
static description :: () -> *void;
}
main :: () -> s32 {
inline if OS == .macos {
ns_object := objc_getClass("NSObject".ptr);
cls := objc_allocateClassPair(ns_object, "SxProbeStatic".ptr, 0);
// class_addMethod on the metaclass — that's where class methods live.
metacls := object_getClass(xx cls);
class_addMethod(metacls, sel_registerName("answer".ptr), xx answer_imp, "i@:".ptr);
class_addMethod(metacls, sel_registerName("add:b:".ptr), xx add_imp, "i@:ii".ptr);
objc_registerClassPair(cls);
n := SxProbeStatic.answer();
print("answer() = {}\n", n);
s := SxProbeStatic.add(7, 35);
print("add(7, 35) = {}\n", s);
c := NSObject.class();
if c != null {
print("class non-null\n");
}
d := NSObject.description();
if d != null {
print("description non-null\n");
}
}
inline if OS != .macos {
print("skipped (not macos)\n");