ReviewOS

also looking at this

craft-native/craft

test(android): the ratchet, and the two seams JNI leaves unchecked

#142
Merged chris wants to merge test/android-conformance-ratchet into main
2 files +360 -0
9 days ago

CraftBridge.kt exposes 103 @JavascriptInterface methods and Zig serves one.

iOS reached this file's job with 105 unmigrated and is at 13. The ratchet exists now rather than later for the reason the iOS one earned: the second migration is where a name quietly stops matching, and by then nothing remembers what the first one agreed to.

Android has two contracts where iOS has one

The call arrives through JNI, so there are two strings that must agree, not one.

1. The action — @JavascriptInterface fun getDeviceInfo() against the A block of whichever module serves it. Same shape as the iOS scan, anchored on the annotation rather than on fun: CraftBridge.kt is full of private helpers (sendEvent, permission plumbing) that aren't page-callable, which is the same reason the iOS scan is bounded to the dispatcher rather than the whole file.

2. The binding — the name in android_dispatch.natives against the external fun that CraftNative.kt declares.

That second one has no compiler behind it at all. RegisterNatives matches by string at load time: the Zig compiler sees a string literal, the Kotlin compiler sees an external fun it assumes someone will bind, and the two agree right up until one is renamed.

Both directions are checked, and they fail differently:

what happens
Zig registers a name the class lacksRegisterNatives refuses the whole batch at load — every action drops to the shim
Kotlin declares a name nobody bindsLoads fine; throws UnsatisfiedLinkError the first time a page reaches the feature

The second is quieter and worse: it survives the app starting.

A third string, also uncompiled

FindClass takes a binary name with slashes (com/craft/runtime/CraftNative); CraftNative.kt declares a package with dots. Two files, two languages, and a prebuilt library that can't find its holder binds nothing at all. The package is derived from the Kotlin and compared against holder_class — and asserted to stay untemplated, since a substitution marker there would make the shipped library app-specific, which is the entire reason the holder is a separate class from CraftBridge.

Verified

mutationresult
Zig action typo (getDeviceinfo)ZigServesAnActionTheSpecDoesNotHave
registered native renamedRegisteredNativeNotDeclared
holder_class disagrees with the Kotlin packageHolderClassDoesNotMatchPackage
ratchet lowered 102 → 101MigrationWentBackwards — the count really is 102
@JavascriptInterface scan brokenthe non-vacuity floor

The reverse binding direction is covered too (DeclaredNativeNotRegistered), though it can't be mutation-tested from the Zig side alone without editing the Kotlin template.

zig build test:android → 80/80, 11/11 steps.

Next

Tier 0 — clipboardRead/clipboardWrite, log, getMemoryUsage, vibrate, setBadge, openURL. No permissions, no UI, no callbacks; each is a Build static or a single instance call, and the ratchet now makes each one lower a number.

2 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.