MOB-98: fix Android JNI owner mismatch for tier-2 native components - #35
Merged
Conversation
MobBridge.kt.eex declared nativeDeliverComponentEvent as a bare external fun on MobNativeViewRegistry, but the generated JNI export in beam_jni.c.eex is Java_<pkg>_MobBridge_nativeDeliverComponentEvent. JNI resolves a native method by its declaring class, so a real event from a Compose native component threw UnsatisfiedLinkError. Moved the declaration onto MobBridge as @JvmStatic external fun (matching every other nativeDeliver* callback in the file) and call it through the qualified MobBridge.nativeDeliverComponentEvent(...) from the registry. Also adds MobNew.Templates.Lint.native_funs_owned_by_mob_bridge/1 — the existing external_fun_jni_consistency/2 only checks that Kotlin and C agree on the function NAME, not which class actually owns the Kotlin declaration, so it stayed green through this exact bug. The new check verifies every native fun in MobBridge.kt is actually declared inside object MobBridge. Device-verified on an Android emulator: a real tier-2 native component (Compose Button) fires a tagged event, the app stays alive, and the event reaches handle_event/3.
From code review on PR #35: - native_funs_owned_by_mob_bridge/1 checked pure byte-position containment within object MobBridge's span — an external fun nested inside another class DECLARED inside object MobBridge (e.g. `object MobBridge { class Helper { external fun bar() } }`) sat inside the span and passed, even though JNI would need MobBridge$Helper as the declaring class, not MobBridge. Added brace_depth_between/3: a match only counts as a direct member if net brace depth from the span's open brace to the match is 0. - Wired the check into check_kotlin/1's aggregate — it was only ever exercised by a dedicated test, not the standard lint pass every other Kotlin-relevant check runs as part of. Safe to add: it already no-ops (returns []) for content with no "object MobBridge" block, so running it against MainActivity.kt or any other .kt content can't produce a false positive. - Documented the check in AGENTS.md's "things that bite" list, per repo convention (same commit as the change, not a follow-up).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
MobBridge.kt.eexdeclarednativeDeliverComponentEventas a bareexternal funonMobNativeViewRegistry, but the generated JNI export inbeam_jni.c.eexisJava_<pkg>_MobBridge_nativeDeliverComponentEvent. JNI resolves a native method by its declaring class, so a real event from a Compose native component threwUnsatisfiedLinkError.MobBridgeas@JvmStatic external fun(matching every othernativeDeliver*callback in the file) and call it through the qualifiedMobBridge.nativeDeliverComponentEvent(...)from the registry.MobNew.Templates.Lint.native_funs_owned_by_mob_bridge/1— the existingexternal_fun_jni_consistency/2only checks that Kotlin and C agree on the function NAME, not which class actually owns the Kotlin declaration, so it stayed green through this exact bug. The new check verifies every native fun inMobBridge.ktis actually declared insideobject MobBridge.Test plan
mix test(withMOB_DIR/MOB_DEV_DIRpointed at the companion mob/mob_dev branches) — 328 passed, 7 new tests (5 inlint_test.exs, 2 inproject_generator_test.exspinning a freshly generated project contains the fix)mix test --only lint(ktlint on generated output) — cleanmix format/mix credo --strict— cleanhandle_event/3Linear: MOB-98 (companion PRs: mob fix/mob-98-native-component-events, mob_dev fix/mob-98-native-component-events)
🤖 Generated with Claude Code