Closes #230.
What was missing
android_conformance_test.zig compared the two sides of the JNI binding by name only, in both directions. The descriptor — the (Landroid/app/Activity;Z)Z half — had nothing checking it.
That is the quietest of the three failures. RegisterNatives refuses the whole table when one entry's descriptor does not match the class, so the library still loads, no native binds at all, and every action falls back to the Kotlin shim. The shim E2E leg passes, because the shim is what it tests. Only the runtime leg notices, as a missing registered N natives line, after a full emulator run.
#215 changed one descriptor, nativeDispatchDeepLink to (Ljava/lang/String;Z)Z, and could only pin that one string in a Zig test. Whether it matched the Kotlin was still CI's job.
What it checks now
Each external fun in CraftNative.kt is turned into the descriptor JNI expects for it, and compared with the signature the table registers under that name.
Types resolve in two steps:
- Kotlin builtins from a small map:
Boolean→Z,Int→I,Long→J,Float→F,Double→D,ByteArray→[B,String→Ljava/lang/String;, and a missing return type →V. - Everything else from the holder's own
importlines.Activityfindsimport android.app.Activityand becomesLandroid/app/Activity;. So the five classes the file passes today —Activity,WebView,IntentSender,SharedPreferences,SQLiteDatabase— need no table, and neither will the next one.
A type that resolves as neither fails the test by name, rather than being skipped:
CraftNative.kt uses the type 'NotAThing', which this test cannot turn into a
JNI descriptor: it is neither a Kotlin builtin nor a class the file imports.
Add it to builtinDescriptor, or import it in CraftNative.kt.String? and String are one descriptor: nullability is Kotlin's, not the JVM's. The parser reads across newlines, so the declarations that wrap — nativeLocationResult and its seven parameters — are covered like the rest.
Mutations run against it
| mutation | result |
|---|---|
nativeDispatchDeepLink registered as (Ljava/lang/String;)Z, its shape before #215 | DescriptorDoesNotMatchKotlin |
Kotlin changes nativeStartListening's available: Boolean to Int | DescriptorDoesNotMatchKotlin |
| Kotlin adds a parameter of an imported type the table does not carry | DescriptorDoesNotMatchKotlin |
| Kotlin adds a parameter of a type that is neither builtin nor imported | UnknownKotlinType, with the message above |
Each prints both descriptors, so the failure says which side to change.
The test also asserts both scans found at least 100 entries, so a file that stops matching cannot leave it comparing empty maps — the same non-vacuity guard the two name tests carry.
Verification
zig build test and zig build test:android, clean before and after each mutation.