ffi 2D: migrate android.sx safe-insets to declarative #jni_class blocks

The four foreign-class declarations move into a new sub-module
`library/modules/platform/android_jni.sx`, imported under a named
namespace from `android.sx`:

    Jni :: #import "modules/platform/android_jni.sx";

This keeps the bare class names (`Activity`, `Window`, `View`,
`WindowInsets`) out of the top level — consumers that flat-import
`modules/platform/android.sx` no longer see `View` collide with
`modules/ui/view.sx`'s protocol of the same name (chess hit this
on the first build attempt).

Compiler-side change: `scanDecls`/`lowerDecls` now also iterate any
`namespace_decl` they encounter and register the contained
`foreign_class_decl`s under their qualified name (`Jni.Activity`).
The recursive scan continues to register the bare names too, so
cross-class refs inside method signatures (e.g. `getWindow ::
(self: *Self) -> *Window`) still resolve through the bare key.
Receiver types like `*Jni.Activity` now route through
`getStructTypeName` → "Jni.Activity" → `foreign_class_map` lookup.

`sx_query_safe_insets_jni`'s param signature changes from
`activity: *Activity` to `activity: *Jni.Activity`; the caller in
`AndroidPlatform.safe_insets` casts via `xx`.

Verified on-device — chess APK built with the new sx, installed via
`adb install -r`, launched on the Pixel. Screencap shows the board
rendering with correct status-bar clearance (time + battery icons
visible above the board, board sized below them) — safe insets are
being queried via the new declarative dispatch and produce the same
values as the pre-migration hand-rolled #jni_call chain.

129/129 examples + cross_compile 3/3 + on-device chess all green.
This commit is contained in:
agra
2026-05-20 12:19:15 +03:00
parent c9db2a8dc0
commit 60f3ffed46
3 changed files with 63 additions and 27 deletions

View File

@@ -135,35 +135,19 @@ sx_android_activity_clazz :: (activity: *void) -> *void {
}
// Declarative JNI class bindings for the safe-insets dispatch chain
// (Phase 2D migration). Each `#jni_class` declares the sx-side alias,
// the JNI class path, and the methods we use — `inst.method(args)`
// inside a `#jni_env(env) { ... }` scope lowers to the same JNI vtable
// indirection as the hand-rolled `#jni_call` form, with the descriptor
// auto-derived from the sx signature (see [src/ir/jni_descriptor.zig]).
WindowInsets :: #jni_class("android/view/WindowInsets") {
getSystemWindowInsetTop :: (self: *Self) -> s32;
getSystemWindowInsetLeft :: (self: *Self) -> s32;
getSystemWindowInsetBottom :: (self: *Self) -> s32;
getSystemWindowInsetRight :: (self: *Self) -> s32;
}
View :: #jni_class("android/view/View") {
getRootWindowInsets :: (self: *Self) -> *WindowInsets;
}
Window :: #jni_class("android/view/Window") {
getDecorView :: (self: *Self) -> *View;
}
Activity :: #jni_class("android/app/Activity") {
getWindow :: (self: *Self) -> *Window;
}
// live in a named sub-module so they don't collide with consumer-side
// types (e.g. `modules/ui/view.sx`'s `View` protocol). The compiler
// registers the foreign-class decls inside under both qualified
// (`Jni.Activity`) and bare (`Activity`) names — receiver types use
// the qualified form, cross-class refs in method signatures use the
// bare form.
Jni :: #import "modules/platform/android_jni.sx";
// sx-side reimplementation of the JNI dispatch chain. Caller provides
// an already-attached `JNIEnv*` and the activity's `clazz` jobject
// (cast to `*Activity` so the method-call DSL can find it in the
// foreign-class registry). Outputs physical-pixel insets.
sx_query_safe_insets_jni :: (env: *void, activity: *Activity, top: *s32, left: *s32, bottom: *s32, right: *s32) -> void {
// (cast to `*Jni.Activity` so the method-call DSL can route it through
// the foreign-class registry). Outputs physical-pixel insets.
sx_query_safe_insets_jni :: (env: *void, activity: *Jni.Activity, top: *s32, left: *s32, bottom: *s32, right: *s32) -> void {
inline if OS != .android { return; }
top.* = 0; left.* = 0; bottom.* = 0; right.* = 0;
@@ -421,7 +405,7 @@ impl Platform for AndroidPlatform {
attached : bool = false;
env := sx_android_get_env(g_android_activity, @attached);
if env != null {
clazz : *Activity = xx sx_android_activity_clazz(g_android_activity);
clazz : *Jni.Activity = xx sx_android_activity_clazz(g_android_activity);
sx_query_safe_insets_jni(env, clazz, @t, @l, @b, @r);
if attached { sx_android_detach_env(g_android_activity); }
}

View File

@@ -0,0 +1,31 @@
// Declarative JNI class bindings used by the Android platform module's
// safe-insets dispatch chain. Imported under a named namespace from
// `modules/platform/android.sx` so the bare class names (`Activity`,
// `Window`, `View`, `WindowInsets`) don't pollute the top-level
// namespace when consumers flat-import the platform module — `View`
// in particular collides with `modules/ui/view.sx`'s protocol.
//
// Inside the platform module these are referenced as `Jni.Activity`,
// `Jni.Window`, etc. The compiler registers the decls both qualified
// and bare in `foreign_class_map`, so cross-class refs in method
// signatures (`getWindow :: (self: *Self) -> *Window`) still resolve
// against the bare name within the namespace.
WindowInsets :: #jni_class("android/view/WindowInsets") {
getSystemWindowInsetTop :: (self: *Self) -> s32;
getSystemWindowInsetLeft :: (self: *Self) -> s32;
getSystemWindowInsetBottom :: (self: *Self) -> s32;
getSystemWindowInsetRight :: (self: *Self) -> s32;
}
View :: #jni_class("android/view/View") {
getRootWindowInsets :: (self: *Self) -> *WindowInsets;
}
Window :: #jni_class("android/view/Window") {
getDecorView :: (self: *Self) -> *View;
}
Activity :: #jni_class("android/app/Activity") {
getWindow :: (self: *Self) -> *Window;
}

View File

@@ -367,6 +367,7 @@ pub const Lowering = struct {
self.registerForeignClassDecl(&decl.data.foreign_class_decl);
},
.namespace_decl => |ns| {
self.registerNamespacedForeignClasses(ns);
if (self.main_file != null) {
self.lowerDecls(ns.decls);
}
@@ -507,6 +508,7 @@ pub const Lowering = struct {
self.registerForeignClassDecl(&decl.data.foreign_class_decl);
},
.namespace_decl => |ns| {
self.registerNamespacedForeignClasses(ns);
if (self.main_file != null) {
self.scanDecls(ns.decls);
}
@@ -8121,6 +8123,25 @@ pub const Lowering = struct {
self.foreign_class_map.put(fcd.name, fcd) catch {};
}
/// When a namespaced import (`Ns :: #import "..."`) contains foreign-class
/// declarations, ALSO register them under their qualified name `Ns.Class`
/// so receiver types like `*Ns.Class` can find the fcd. The recursive
/// scan/lower already handles bare-name registration; this only adds the
/// qualified-name entry, so cross-class refs in method signatures
/// (`*View` → bare lookup) still work.
fn registerNamespacedForeignClasses(self: *Lowering, ns: ast.NamespaceDecl) void {
for (ns.decls) |inner| {
if (inner.data == .foreign_class_decl) {
const fcd = &inner.data.foreign_class_decl;
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ns.name, fcd.name }) catch fcd.name;
self.foreign_class_map.put(qualified, fcd) catch {};
} else if (inner.data == .namespace_decl) {
// Nested namespaces — qualify with both prefixes.
self.registerNamespacedForeignClasses(inner.data.namespace_decl);
}
}
}
/// Register an impl block: register its methods as TypeName.method in fn_ast_map.
fn registerImplBlock(self: *Lowering, ib: *const ast.ImplBlock, is_imported: bool, decl: *const Node) void {
// Parameterised-protocol impl (e.g. `impl Into(Block) for Closure() -> void`):