JNI_OnLoad reports its own failures through std.log:
android_dispatch.zig:144-159 warns when RegisterNatives refuses a method
or when com/craft/runtime/CraftNative is not on the class path, and still
returns a valid JNI version so the app keeps running.
Those messages go to stderr. Android discards stderr. There is no
std_options, no custom logFn and no __android_log_write binding anywhere
in packages/zig/src/android*.zig or jni.zig — the only custom logFn in
the tree is src/minimal.zig:59-61, which is the macOS executable.
The result is that three quite different states are indistinguishable from outside:
libcraft.sowas never shipped (which is always, today — #200)- it loaded but
RegisterNativesrefused a method - it loaded and bound correctly but the action declined
All three end with the Kotlin shim answering, and nothing anywhere says which happened.
iOS does not have this problem: ios_dispatch.zig:572 logs
craft-bridge dispatch t=mobile a=<action> i=<id> and it reaches the device
console, which is exactly what the runtime leg of the mobile E2E suite counts
to prove the Zig archive was in the loop (scripts/mobile-e2e/protocol.ts,
zigDispatchCount). The Android leg has no equivalent assertion available.
A logFn that forwards to __android_log_write with a fixed tag would make
logcat the Android counterpart, and would let the E2E harness assert which
side served a call there too.