ffi 2.16c green: TL fallback via C-helper runtime + always-omit env in #jni_call

`#jni_call` collapses to a single surface — env is *always* implicit:
either picked up from the lexically-enclosing `#jni_env(env) { ... }`
block's Ref (cheap, register-resident, no TL touch) or from the
runtime's thread-local slot via `sx_jni_env_tl_get()` (one fn call
per dispatch). The explicit-env shape is gone — chess and the
existing tests migrate cleanly by wrapping their helper-fn bodies
in `#jni_env(env) { ... }`.

The TL slot lives outside the user's IR module so the LLVM ORC JIT
can load object files cleanly without `orc_rt` for TLS support:

  library/vendors/sx_jni_runtime/sx_jni_env_tl.c:
    static _Thread_local void *sx_jni_env_tl_slot;
    void *sx_jni_env_tl_get(void) { return sx_jni_env_tl_slot; }
    void sx_jni_env_tl_set(void *env) { sx_jni_env_tl_slot = env; }

Linkage:
- sx-the-compiler links the .c file via build.zig so the JIT
  process-symbol generator resolves `sx_jni_env_tl_get`/`_set`.
- AOT targets get the same .c file auto-linked via the lowering
  pass: when lower touches the TL externs, it sets
  `needs_jni_env_tl_runtime`, and `Compilation.lowerToIR` appends a
  synthetic `CImportInfo` to `lowering_extra_c_sources` that
  `collectCImportSources` merges with user-written ones.

Lowering-side changes:
- `getJniEnvTlFids` lazily declares the two externs (parallel
  to `getSelRegisterNameFid`) and flips `needs_jni_env_tl_runtime`.
- `#jni_env(env) { body }` emits save→set→body→restore via three
  `call` ops to the externs; the inner body sees env via the
  lexical-direct stack.
- `lowerJniCall` resolves env from `jni_env_stack` (top) or the TL
  fallback. The explicit-env branch is gone.
- `jni_env_stack_base` tracks per-fn lexical scope so lazy-lowering
  a callee doesn't accidentally see the caller's Ref (Refs are only
  valid inside one fn's instruction stream).

Test migration (mechanical):
- ffi-jni-call-{01..09}: each helper fn wraps `#jni_call(...)`
  bodies in `#jni_env(env) { ... }`. Returning values pass through
  the block as an expression — `#jni_env` now also lowers in
  expression position.

Verified:
- zig build test + tests/run_examples.sh: 130/130 green.
- tests/cross_compile.sh: 3/3 green.
- Chess APK rebuilt + reinstalled on Pixel. Board renders with
  status-bar clearance + info panel intact; no crashes in logcat.
  Safe-insets dispatch through `#jni_env` + lexical-direct now
  fully exercised end-to-end on real hardware.
This commit is contained in:
agra
2026-05-20 13:53:25 +03:00
parent 013cf9f1bb
commit 6a3260ff65
26 changed files with 330 additions and 96 deletions

View File

@@ -10,14 +10,17 @@
main :: () -> s32 {
inline if false {
// Instance method: env, target, name, sig, args...
#jni_call(*void)(null, null, "getWindow", "()Landroid/view/Window;");
env : *void = null;
#jni_env(env) {
// Instance method: target, name, sig, args...
#jni_call(*void)(null, "getWindow", "()Landroid/view/Window;");
// Static method: env, class, name, sig, args...
#jni_static_call(s32)(null, null, "max", "(II)I", 3, 7);
// Static method: class, name, sig, args...
#jni_static_call(s32)(null, "max", "(II)I", 3, 7);
// Returning a Java primitive (jboolean → sx bool).
#jni_call(bool)(null, null, "isShown", "()Z");
// Returning a Java primitive (jboolean → sx bool).
#jni_call(bool)(null, "isShown", "()Z");
}
}
print("parse-only ok\n");
0;

View File

@@ -31,7 +31,9 @@ main :: () -> s32 {
// sx_android_jni.c lands.
env : *void = null;
target : *void = null;
#jni_call(void)(env, target, "noop", "()V");
#jni_env(env) {
#jni_call(void)(target, "noop", "()V");
}
}
inline if OS != .android {
print("skipped (not android)\n");

View File

@@ -17,8 +17,10 @@
g_should_call : bool = false;
unused_jni :: (env: *void, target: *void) {
#jni_call(void)(env, target, "noop", "()V");
#jni_call(void)(env, target, "noop", "()V");
#jni_env(env) {
#jni_call(void)(target, "noop", "()V");
#jni_call(void)(target, "noop", "()V");
}
}
main :: () -> s32 {

View File

@@ -14,7 +14,9 @@
g_should_call : bool = false;
read_int :: (env: *void, target: *void) -> s32 {
#jni_call(s32)(env, target, "getCount", "()I");
#jni_env(env) {
#jni_call(s32)(target, "getCount", "()I");
}
}
main :: () -> s32 {

View File

@@ -10,7 +10,9 @@
g_should_call : bool = false;
read_long :: (env: *void, target: *void) -> s64 {
#jni_call(s64)(env, target, "currentTimeMillis", "()J");
#jni_env(env) {
#jni_call(s64)(target, "currentTimeMillis", "()J");
}
}
main :: () -> s32 {

View File

@@ -7,7 +7,9 @@
g_should_call : bool = false;
read_double :: (env: *void, target: *void) -> f64 {
#jni_call(f64)(env, target, "getValue", "()D");
#jni_env(env) {
#jni_call(f64)(target, "getValue", "()D");
}
}
main :: () -> s32 {

View File

@@ -9,7 +9,9 @@
g_should_call : bool = false;
read_bool :: (env: *void, target: *void) -> bool {
#jni_call(bool)(env, target, "isShown", "()Z");
#jni_env(env) {
#jni_call(bool)(target, "isShown", "()Z");
}
}
main :: () -> s32 {

View File

@@ -14,7 +14,9 @@
g_should_call : bool = false;
get_window :: (env: *void, activity: *void) -> *void {
#jni_call(*void)(env, activity, "getWindow", "()Landroid/view/Window;");
#jni_env(env) {
#jni_call(*void)(activity, "getWindow", "()Landroid/view/Window;");
}
}
main :: () -> s32 {

View File

@@ -10,7 +10,9 @@
g_should_call : bool = false;
call_static_max :: (env: *void, cls: *void) -> s32 {
#jni_static_call(s32)(env, cls, "max", "(II)I", 3, 7);
#jni_env(env) {
#jni_static_call(s32)(cls, "max", "(II)I", 3, 7);
}
}
main :: () -> s32 {