android: Platform-owned entry bridge + .android OS enum variant

User writes BOTH `main` and a 3-line `android_main(app)` trampoline.
The library provides `sx_android_bootstrap(app)` (stashes the NDK app
pointer into a platform-owned global) and `AndroidPlatform` impl of
the Platform protocol. The library NEVER references `main` — the OS-
shape entry symbol lives in user code where the other entry symbols
already live. iOS / SDL3 keep their existing shape; only Android adds
the trampoline.

Cross-cutting bits this commit ships:

  library/modules/compiler.sx
    Add `android` variant to `OperatingSystem`.

  src/ir/lower.zig
    - injectComptimeConstants: map TargetConfig.isAndroid() → .android.
    - New Pass 4 `checkRequiredEntryPoints`: emit a clean diagnostic
      when `--target android` is requested but `android_main` isn't
      defined, instead of letting the user crash on a dlopen-time
      missing-symbol error.

  library/modules/platform/android.sx
    AndroidPlatform impl of the Platform protocol — EGL bringup on
    `APP_CMD_INIT_WINDOW`, ALooper(0) polling, dispatches the user's
    frame closure each ~16 ms tick. `sx_android_bootstrap(app)` is the
    only function exposed for the entry trampoline.

  examples/99-android-egl-clear.sx
    Rewritten to use the new pattern: minimum `main` + `android_main`
    pair, AndroidPlatform-driven render loop. Doubles as the usage
    reference users hand off to the compiler diagnostic.

Verified on Pixel 7 Pro: purple clear-color frame, periodic
`rendered 60 frames` logcat lines. iOS-sim chess + 86/86 regression
tests pass.
This commit is contained in:
agra
2026-05-19 00:23:33 +03:00
parent efb087559d
commit 561ad03a7c
4 changed files with 426 additions and 165 deletions

View File

@@ -207,6 +207,39 @@ pub const Lowering = struct {
self.lowerMainAndComptime(decls);
// Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered
self.lowerDeferredTypeFns();
// Pass 4: target-specific entry-point sanity checks
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.
fn checkRequiredEntryPoints(self: *Lowering) void {
const tc = self.target_config orelse return;
if (!tc.isAndroid()) 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;
}
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`).", .{});
}
}
/// Inject compile-time constants from target_config into comptime_constants.
@@ -223,6 +256,8 @@ pub const Lowering = struct {
self.findVariantIndex(os_info.@"enum".variants, "wasm")
else if (tc.isWindows())
self.findVariantIndex(os_info.@"enum".variants, "windows")
else if (tc.isAndroid())
self.findVariantIndex(os_info.@"enum".variants, "android")
else if (tc.isLinux())
self.findVariantIndex(os_info.@"enum".variants, "linux")
else if (tc.isIOS())