diff --git a/examples/ffi-jni-main-01-emit.sx b/examples/ffi-jni-main-01-emit.sx index 6ddb979..2eef07f 100644 --- a/examples/ffi-jni-main-01-emit.sx +++ b/examples/ffi-jni-main-01-emit.sx @@ -12,27 +12,20 @@ // --apk /tmp/sxjnimain.apk --bundle-id co.swipelab.sxjnimain \ // -o /tmp/libsxjnimain.so examples/ffi-jni-main-01-emit.sx // unzip -l /tmp/sxjnimain.apk | grep classes.dex -// -// Cross-compile test (compile-only): see tests/cross_compile.sh's -// `android | examples/ffi-jni-main-01-emit.sx` tuple. APK creation -// itself isn't exercised by cross_compile.sh — only that the example -// lowers and links cleanly with `#jni_main` in scope. #import "modules/std.sx"; #import "modules/compiler.sx"; +// `*Bundle` resolves through the class registry to `android.os.Bundle` +// in the emitted Java — needed for `onCreate`'s @Override to match +// NativeActivity's superclass signature. +Bundle :: #foreign #jni_class("android/os/Bundle") { } + // `#jni_main` flags this as the launchable Android Activity class. The -// empty body intentionally has zero methods — slice 2 just verifies the -// .java/.dex pipeline; `onCreate` overriding lands once slice 4 wires -// `RegisterNatives` so the `sx_` symbols actually resolve. -SxApp :: #jni_main #jni_class("co/swipelab/sxjnimain/SxApp") { } +// `onCreate` body is empty for now — slice 4 wires `RegisterNatives` +// so the `sx_onCreate` native delegate actually binds to a sx-side fn. +SxApp :: #jni_main #jni_class("co/swipelab/sxjnimain/SxApp") { + onCreate :: (self: *Self, b: *Bundle) { } +} main :: () -> s32 { 0; } - -// Android NDK entry symbol — kept as a 3-line trampoline so this example -// passes `--target android` builds via `tests/cross_compile.sh`. -android_main :: (app: *void) { - inline if OS == .android { - main(); - } -} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 3970469..2fd2ac8 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -219,34 +219,45 @@ pub const Lowering = struct { self.checkRequiredEntryPoints(); } - /// On Android, the OS loader calls `android_main(app: *void)` — there's - /// no `main()` invocation from the system. If the user hasn't defined - /// `android_main`, native_app_glue can't find it at runtime and the - /// activity dies with an unhelpful "library doesn't export - /// `android_main`" error after the .so is loaded. Catch this at - /// compile time with a clear hint pointing at the platform module - /// that provides the helper. + /// On Android, the OS loads the .so via one of two entry paths: + /// + /// 1. **`#jni_main` Activity** (preferred) — Java-side Activity class + /// declared as `Foo :: #jni_main #jni_class("...") { onCreate :: ... }`. + /// The Java class drives lifecycle; sx provides native delegates. + /// 2. **`android_main` trampoline** (legacy NativeActivity path) — user + /// defines `android_main(app: *void)`, native_app_glue's + /// `ANativeActivity_onCreate` jumps into it on a worker thread. + /// + /// Either satisfies the check. When both are missing, the .so won't + /// have an entry point Android can reach, so emit a diagnostic that + /// shows both options. fn checkRequiredEntryPoints(self: *Lowering) void { const tc = self.target_config orelse return; if (!tc.isAndroid()) return; + var it = self.foreign_class_map.iterator(); + while (it.next()) |entry| { + const fcd = entry.value_ptr.*; + if (fcd.is_main and !fcd.is_foreign and fcd.runtime == .jni_class) return; + } + const wanted = self.module.types.internString("android_main"); - var has_defn = false; for (self.module.functions.items) |func| { if (func.name != wanted) continue; if (func.is_extern) continue; if (func.blocks.items.len == 0) continue; - has_defn = true; - break; + return; } - if (has_defn) return; + if (self.diagnostics) |diags| { diags.addFmt(.err, null, - "target is Android but no `android_main` function defined. " ++ - "The OS calls `android_main(app: *void)` as the entry point — " ++ - "add it to your main.sx (it can be a 3-line trampoline that " ++ - "calls `sx_android_bootstrap(app)` then `main()` — see " ++ - "`examples/99-android-egl-clear.sx`).", .{}); + "target is Android but no entry point declared. " ++ + "Either declare a `#jni_main` Activity class:\n\n" ++ + " MyApp :: #jni_main #jni_class(\"co/example/MyApp\") {{\n" ++ + " onCreate :: (self: *Self, b: *Bundle) {{ /* ... */ }};\n" ++ + " }}\n\n" ++ + "or define an `android_main` trampoline that bootstraps the " ++ + "legacy NativeActivity path (see `examples/99-android-egl-clear.sx`).", .{}); } }