Skip to content

feat(sokol): register AndroidBackendContext with core seam (#310 stage 3) - #312

Merged
apotema merged 2 commits into
mainfrom
feat/310-sokol-android-backend-context
Jun 13, 2026
Merged

feat(sokol): register AndroidBackendContext with core seam (#310 stage 3)#312
apotema merged 2 commits into
mainfrom
feat/310-sokol-android-backend-context

Conversation

@apotema

@apotema apotema commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Stage 3 of labelle-core#310 — the sokol backend registers an AndroidBackendContext with core's seam (Stages 1 core + 2 engine merged), so sokol-Android keeps immersive + gamepad working now that core/engine no longer link sokol's symbols directly.

What

  • backends/sokol/src/android.zig (new adapter) — builds the core.AndroidBackendContext from sokol's own symbols: get_native_activity wraps sapp.androidGetNativeActivity() (@constCasting sokol's ?*const anyopaque to the seam's ?*anyopaque); gamepad_init/gamepad_shutdown are extern "c" decls for the existing JNI glue (android_gamepad_jni.c, already compiled into the input module). pub fn backendContext() returns the vtable.
  • backends/sokol/src/input.zig — re-export pub const android = @import("android.zig") so the generated main reaches it as backend_input.android.
  • backends/sokol/build.zig — Android branch wires labelle-core onto the input module (mutually exclusive with the Linux-desktop core route).
  • src/codegen/lifecycle/callback.zig — the generated sokol sokol_main() now emits, on every sokol-Android build (gamepad needs the context even with immersive off): engine.core.registerAndroidBackend(@import("backend_input").android.backendContext());before the optional engine.android.enableImmersiveMode(). Both run on the UI thread at startup, before core's gamepad source first polls. Desktop/wasm/iOS codegen unchanged (gated on platform == .android).
  • test/ — a sokol_mobile_lifecycle fixture + 3 codegen assertions (registration emitted + precedes immersive; emitted with immersive off; neither on non-Android).

Verification

  • Assembler zig build + zig build test → green (incl. the 3 new assertions).
  • Sokol desktop + wasm (-Dtarget=wasm32-emscripten) → green, unaffected.
  • Adapter Android compile-check (zig build-obj -target aarch64-linux-android against the merged seam core) → clean (the @constCast, externs, and context literal type-check on Android).
  • Grep: generated codegen references engine.core.registerAndroidBackend + backend_input.android; no removed sokol externs in src/.

Honest scope

Full sokol-Android device/APK build not run — it needs the sokol-Android native toolchain (NDK sysroot GLESv3/EGL/android/log wired by the generated app build) and a seam-core install the example pins to (the example currently pins pre-seam local: siblings; only the merged core/engine main carry the seam). Verified via codegen assertions + the Android-target adapter compile-check against the seam core.

Version-compat

Generated sokol-Android apps now call engine.core.registerAndroidBackend(...), so they require labelle-core ≥ the Stage-1 seam + labelle-engine ≥ Stage-2. Pre-seam apps won't compile. (Stage 4 wires the bgfx adapter using the same backend-agnostic JNI glue.)

…ore#310, Stage 3)

Sokol-Android must register an AndroidBackendContext now that core/engine
no longer link sokol's symbols directly. Add backends/sokol/src/android.zig
adapter (callconv(.c) get_native_activity wrapper @constCasting sokol's const
result + extern labelle_android_gamepad_init/_shutdown), surface it as
backend_input.android, wire core onto the sokol input module on Android, and
emit engine.core.registerAndroidBackend(...) at the top of the generated
sokol-Android sokol_main() — before enableImmersiveMode() and before the
gamepad source initializes. Emitted on every sokol-Android build (gamepad
detection needs it even when immersive mode is off); desktop/wasm/ios
codegen unchanged.
@cursor

cursor Bot commented Jun 13, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes Android startup ordering and requires seam-era core/engine; mis-ordered registration would break immersive mode and gamepad detection on sokol-Android, though codegen tests lock the intended order.

Overview
Stage 3 (labelle-core#310) wires the sokol backend into core’s runtime Android JNI seam: core/engine no longer depend on fixed sokol externs, so sokol must register a vtable at startup or Android gamepad + immersive behavior becomes no-ops.

A new backends/sokol/src/android.zig adapter builds AndroidBackendContext from sapp.androidGetNativeActivity() (with a @constCast wrapper) and the existing labelle_android_gamepad_* JNI glue. input.zig re-exports it as backend_input.android only on Android targets; build.zig adds a labelle-core import on the input module for Android (parallel to the Linux desktop core route).

Codegen changes buildImmersiveEntryCode: every sokol-Android build emits engine.core.registerAndroidBackend(@import("backend_input").android.backendContext()); at the top of sokol_main(), before the optional enableImmersiveMode() (registration is no longer tied to immersive being enabled). Non-Android platforms emit nothing.

Tests add a sokol_mobile_lifecycle fixture and three assertions on registration presence, ordering vs immersive, and Android-only gating.

Reviewed by Cursor Bugbot for commit 3ae1ec2. Bugbot is set up for automated code reviews on this repo. Configure here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements Stage 3 of the backend-agnostic JNI seam (labelle-core#310) for the Sokol Android backend, introducing a new android.zig adapter to register the AndroidBackendContext at startup. The build configuration, codegen, and tests are updated to support this integration. The review feedback highlights potential compilation failures on non-Android platforms due to unconditional imports of labelle-core and references to Android-specific Sokol symbols, recommending compile-time gating. Additionally, it suggests removing redundant callconv(.c) specifiers on extern "c" declarations.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread backends/sokol/src/input.zig Outdated
/// (`engine.core.registerAndroidBackend(...)`) so core's gamepad source and the
/// engine's immersive mode can reach the running ANativeActivity / InputManager
/// without core/engine linking any sokol symbol directly. See `android.zig`.
pub const android = @import("android.zig");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

On non-Android platforms (such as macOS or Windows desktop), labelle-core is not added as an import to input_mod in build.zig. Since android.zig unconditionally imports labelle-core, any semantic analysis or reference to android on those platforms will trigger a compilation error. Gating the import of android.zig using agp.is_android prevents this file from being imported on non-Android platforms.

pub const android = if (agp.is_android) @import("android.zig") else struct {};

Comment on lines +73 to +76
fn getNativeActivity() callconv(.c) ?*anyopaque {
const act = sapp.androidGetNativeActivity();
return @constCast(act);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since android.zig is parsed on all platforms, referencing sapp.androidGetNativeActivity() directly can cause compilation failures on non-Android platforms where the symbol is not present in the sokol package. Pruning the function body at compile-time using comptime when is_android is false prevents the compiler from analyzing the missing symbol.

fn getNativeActivity() callconv(.c) ?*anyopaque {\n    if (comptime !is_android) return null;\n    const act = sapp.androidGetNativeActivity();\n    return @constCast(act);\n}

Comment thread backends/sokol/src/android.zig Outdated
Comment on lines +64 to +65
extern "c" fn labelle_android_gamepad_init(activity: ?*anyopaque) callconv(.c) void;
extern "c" fn labelle_android_gamepad_shutdown() callconv(.c) void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In Zig, extern "c" functions automatically use the C calling convention (callconv(.c)). Specifying both is redundant and can be simplified.

extern \"c\" fn labelle_android_gamepad_init(activity: ?*anyopaque) void;\nextern \"c\" fn labelle_android_gamepad_shutdown() void;

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5308d61. Configure here.

Comment thread backends/sokol/src/input.zig Outdated
@apotema

apotema commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

Bot triage — addressed in the latest commit:

  • Unconditional pub const android = @import("android.zig") (Gemini + Cursor, high) — fixed. android.zig imports labelle-core (AndroidBackendContext) and references sokol's androidGetNativeActivity, neither of which is wired into the input module on desktop/wasm. It only compiled because the unreferenced re-export was lazily skipped — fragile. The re-export is now gated to Android (abi == .android or .androideabi); on other targets it's an empty namespace and android.zig is never analyzed. This also resolves the sapp.androidGetNativeActivity non-Android concern (med).
  • Redundant callconv(.c) on extern "c" fn (Gemini, med) — fixed; dropped it from the two gamepad-glue externs (extern "c" already implies the C convention).

Verified: assembler zig build+test, sokol desktop + wasm, and the Android-target adapter compile-check against the merged seam core — all green.

…redundant callconv (review)

Stage-3 bot review:
- Unconditional `pub const android = @import("android.zig")` (Gemini+Cursor, high):
  android.zig imports labelle-core + references sokol's androidGetNativeActivity,
  neither wired into the input module on desktop/wasm. It only passed because the
  unreferenced re-export was lazily skipped — fragile. Gate the re-export so the
  adapter is analyzed only on Android; on other targets it's an empty namespace.
  Resolves the companion sapp.androidGetNativeActivity desktop concern too.
- Redundant callconv(.c) on extern "c" fn (Gemini, medium): extern "c" already
  implies the C convention; dropped it from the two gamepad-glue externs.
@apotema
apotema merged commit 09a997c into main Jun 13, 2026
5 checks passed
@apotema
apotema deleted the feat/310-sokol-android-backend-context branch June 13, 2026 14:28
apotema added a commit that referenced this pull request Jun 13, 2026
Backend work since v0.39.1:
- wgpu: macOS Metal surface + textured sprite rendering (#290, #291)
- bgfx: macOS bring-up to on-device Android (#296 epic, #304/#305/#307/#308/#309)
- #310 AndroidBackendContext adapters: sokol (#312) + bgfx (#313) register the
  core seam; bgfx-Android gamepad via the shared android_gamepad sub-package
- bgfx desktop gamepad: GLFW (#315) + SDL HIDAPI / Switch-pad support (#318)
- cached bgfx CI job (#295)

Android codegen now calls core.registerAndroidBackend → requires
labelle-core >= v1.17.0 + labelle-engine >= v1.50.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant