Adds the constructor-invocation arm of the foreign-class DSL:
`SurfaceView.new(ctx)` (where `SurfaceView` is a `#foreign #jni_class`
with `static new :: (ctx: *Context) -> *Self;`) lowers to
`FindClass(env, "android/view/SurfaceView") + GetMethodID(env, cls,
"<init>", "(args)V") + NewObject(env, cls, mid, args...)`. Returns
the fresh jobject.
- inst.zig: `JniMsgSend.is_constructor` flag + `parent_class_path`
re-purposed to carry the class being constructed (alongside its
existing nonvirtual-super-class use). Mutually exclusive with
`is_static` / `is_nonvirtual`.
- lower.zig: `lowerCall.field_access` arm now recognises
`Alias.method(args)` where `Alias` resolves in `foreign_class_map`
and the matching member is `static`. `new` routes to a new
`lowerForeignStaticCall` that derives a `(args)V` JNI descriptor
and emits a `JniMsgSend` with `is_constructor=true`. Non-`new`
static calls report a clear "use #jni_static_call" diagnostic
until that sugar lands.
- emit_llvm.zig: new `NewObject` vtable slot (28) + `emitJniConstructor`
helper expanding the FindClass+GetMethodID+NewObject chain. The
jni_msg_send arm short-circuits to it when `is_constructor` is set.
Smoke `ffi-jni-main-03-ctor.sx` exercises both this slice and the
previous super-dispatch slice in a single `onCreate` body: calls
`super.onCreate(b)` then constructs a `SurfaceView` with the Activity
as Context. IR shows the expected six-stage chain (FindClass+GetMethodID+
CallNonvirtual + FindClass+GetMethodID+NewObject); APK builds clean.
Naming caveat: the Java type `android.content.Context` clashes with
sx stdlib's `Context :: struct {...}` (heap-context). The smoke aliases
it `JContext` — future work could add a path-prefix or `as` rename
form on `#jni_class` to avoid the manual rename.
133 host / 6 cross / zig build test all green.
Inside a `#jni_main` (or any sx-defined `#jni_class`) bodied method,
`super.method(args)` lowers to JNI's nonvirtual dispatch against the
parent class resolved via `#extends` (default `android.app.Activity`).
- lower.zig: tracks `current_foreign_class` + `current_foreign_method`
around each `synthesizeJniMainStub` body; pushes the JNIEnv* arg
onto the lexical `#jni_env` stack so omitted-env JNI calls inside
the body see env without a wrapper. New `lowerSuperCall` handles
the `super.method(args)` receiver pattern: derives parent path,
reuses the enclosing method's signature when names match (the
common `super.<override>(args)` case), or looks up the method on
the parent class declared as `#foreign #jni_class`.
- inst.zig: `JniMsgSend` gains `is_nonvirtual: bool` and
`parent_class_path: ?[]const u8` — the dispatch tag + super class
foreign path. Mutually exclusive with `is_static`.
- emit_llvm.zig: new `CallNonvirtual<T>Method` vtable slots + a
fourth dispatch arm. Resolves the parent jclass via
`FindClass(env, parent_path)` (per-call; caching is follow-up),
then `GetMethodID(env, parent_cls, name, sig)`, then
`CallNonvirtual<T>Method(env, obj, parent_cls, mid, args...)`.
Disassembly on the smoke confirms the chain:
`ldr [env+0x30]` (FindClass) → `ldr [env+0x108]` (GetMethodID) →
`ldr [env+0x2d8]` (CallNonvirtualVoidMethod) with `(env, self,
parent_cls, mid, bundle)`.
132 host / 5 cross / zig build test all green. The slice unblocks
Activity lifecycle overrides (onCreate, onResume, onPause) calling
their required `super.<method>(args)` without raw `#jni_call`
boilerplate.
Compilation.lowering_jni_main_decls is populated by lowerToIR (iterating
foreign_class_map for is_main && !is_foreign && runtime==jni_class,
deduped by foreign_path); each entry carries the pre-rendered Java source
from jni_java_emit.emitJavaSource.
createApk extended: when the emission list is non-empty, write each
.java under <stage>/java/<pkg>/<Class>.java, javac --release 11 to
<stage>/classes/, d8 --release --lib <android_jar> --output <stage>
to produce <stage>/classes.dex, then zip the .dex into the unaligned
APK at root level. javac discovery: $JAVA_HOME/bin/javac first, then
`which javac`.
Manifest still hardcodes android.app.NativeActivity (slice 3 wires the
user's class name + android:hasCode="true"), so the bundled .dex is
present but unreferenced at runtime. End-to-end verified via dexdump on
the smoke example's APK — Lco/swipelab/sxjnimain/SxApp; extending
NativeActivity shows up in classes.dex. Non-#jni_main APK builds
(99-android-egl-clear.sx) produce the same shape as before.
Cross-compile tuple added for examples/ffi-jni-main-01-emit.sx
(compile-only — APK exercise is manual).
Adds `ios-sim|examples/ffi-jni-call-02-void.sx` to the cross-compile
tuple list. The `inline if OS == .android { #jni_call(...) }` arm in
that example must strip its body before sema/lower runs on iOS,
otherwise emit_llvm would attempt to load libjvm vtable slots that
don't exist in the iOS SDK and the link step would fail.
This is the JNI mirror of step 1.14, which did the same for
`#objc_call` against Android. Phase 1C is functionally complete:
- Parser accepts all three FFI intrinsics (1.1–1.2)
- `#objc_call` full return-type matrix + selector interning (1.3–1.10)
- `#objc_call` enclosing-construct coverage (1.11–1.13)
- `#objc_call` cross-Android gate (1.14)
- `#jni_call(void)` codegen with vtable indirection (1.15)
- `#jni_call` literal-keyed slot interning (1.16–1.17)
- `#jni_call` return-type matrix s32/s64/f64/bool/*void (1.18–1.22)
- `#jni_static_call` lowering (1.23)
- `#jni_call` cross-iOS gate (1.24, this commit)
3/3 cross-compile tuples pass; 118/119 host tests pass (one
unrelated regression in working tree). Next: Phase 1D for
`library/vendors/sx_android_jni/sx_android_jni.c` — migrate the C
JNI helpers to sx via `#jni_call`. Requires on-device chess
verification per the FFI plan.
Adds `examples/ffi-jni-call-02-void.sx` exercising `#jni_call(void)
(env, target, "name", "sig")` inside an `inline if OS == .android`
arm, plus a new tuple in `tests/cross_compile.sh`. Host run_examples
passes (the inline-if strips the JNI body, leaving "skipped"); the
Android cross-compile FAILs because `lowerFfiIntrinsicCall` still
emits the placeholder diagnostic for any `fic.kind != .objc_call`.
Per the FFI cadence rule this is a test-add (xfail); the next
commit makes the Android cross-compile green by adding the
`.jni_msg_send` opcode and its emit_llvm expansion.
109/109 host tests pass; tests/cross_compile.sh's first real tuple
(`android | examples/ffi-objc-call-10-os-gate.sx`) compiles
through `sx build --target android` without finding any
`@objc_msgSend` / `@sel_registerName` symbols in the output —
the `inline if OS == .ios { #objc_call(...) }` arm is stripped
at sx compile time before emit_llvm runs, so the Android
toolchain (Bionic + libGLESv3 / NDK linker) doesn't see the
Obj-C runtime references that would otherwise be undefined.
Host (macOS): the example prints "host stripped both" — the iOS
arm is stripped (we're not iOS) AND the Android arm is stripped
(we're not Android), confirming `inline if OS == { case }`
symmetric strip-and-render works around `#objc_call` sites.
The example carries a 3-line `android_main` trampoline so the
NDK linker's `-u ANativeActivity_onCreate` / entry-point
discovery is satisfied — pattern shared with chess + the other
android examples.
First step of the FFI ceremony reduction plan (current/PLAN-FFI.md).
Iterates a (target, example) tuple list, runs `sx build --target <t>
<example>`, asserts exit 0 + output file produced. Cross-compile
correctness only — these examples can't run on the host.
Initial tuple list is empty, so the script exits 0 on a clean tree
and contributors without the iOS SDK / Android NDK aren't blocked.
`toolchain_available` short-circuits with a SKIP line when the
requested toolchain isn't installed. Phase 1/2/3 cross-only examples
populate TUPLES as they land.