Skip to content

MOB-98: fix Android JNI owner mismatch for tier-2 native components - #35

Merged
GenericJam merged 2 commits into
masterfrom
fix/mob-98-native-component-events
Aug 26, 2026
Merged

MOB-98: fix Android JNI owner mismatch for tier-2 native components#35
GenericJam merged 2 commits into
masterfrom
fix/mob-98-native-component-events

Conversation

@GenericJam

Copy link
Copy Markdown
Owner

Summary

  • 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.
  • Added 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.

Test plan

  • mix test (with MOB_DIR/MOB_DEV_DIR pointed at the companion mob/mob_dev branches) — 328 passed, 7 new tests (5 in lint_test.exs, 2 in project_generator_test.exs pinning a freshly generated project contains the fix)
  • mix test --only lint (ktlint on generated output) — clean
  • mix format / mix credo --strict — clean
  • Device-verified on an Android emulator: a real tier-2 native component (Compose Button) fires a tagged event, the app stays alive, the event reaches handle_event/3

Linear: MOB-98 (companion PRs: mob fix/mob-98-native-component-events, mob_dev fix/mob-98-native-component-events)

🤖 Generated with Claude Code

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).
@GenericJam
GenericJam merged commit a98e8f3 into master Aug 26, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant