Skip to content

Port Mob.VendorUsb Android C bridges to Zig (rebase mob#5 + mob_new#2) #6

@GenericJam

Description

@GenericJam

What

Bring @HeroesLament's Mob.VendorUsb PRs forward to current master by porting the Android JNI bridges from C to Zig.

  • Runtime PR: mob#5 — Elixir API + iOS stubs + Android NIFs (these need porting)
  • Templates PR: mob_new#2 — Kotlin/JNI codegen (mostly clean — see below)

Why this exists

Both PRs were opened against master at b4471290… on 2026-05-09. Shortly after, commit b091e67 (Phase 6b iter 3d (mob side): finale — delete mob_nif.c, all-Zig NIF surface) deleted android/jni/mob_nif.c entirely and migrated the Android NIF surface to Zig. The PR's 241 lines of C in mob_nif.c therefore cannot be applied as written — that file no longer exists.

The Elixir/iOS/Erlang sides of the PR are unaffected by the migration and only have textual conflicts.

Scope of the port

1. Android C → Zig (the actual port)

The seven NIFs added in android/jni/mob_nif.c need to be added to android/jni/mob_nif.zig instead. They are:

vendor_usb_list_devices/1
vendor_usb_request_permission/1
vendor_usb_open/1
vendor_usb_bulk_write/3
vendor_usb_start_reading/2
vendor_usb_stop_reading/1
vendor_usb_close/1

Each NIF dispatches to a @JvmStatic method on MobBridge (Kotlin side, no port needed — that's in mob_new#2). The seven mob_send_vendor_usb_* delivery functions and the jmethodID cache slots also need their Zig equivalents.

2. The patterns to follow

android/jni/mob_nif.zig already has 30+ NIFs in this shape. Pick any one (e.g. webview_load_url, camera_start) and mirror it:

pub fn vendor_usb_open(
    env: ?*erts.ErlNifEnv,
    _argc: c_int,
    argv: [*c]const erts.ERL_NIF_TERM,
) callconv(.c) erts.ERL_NIF_TERM {
    _ = _argc;
    // 1. Extract args from argv via erts.enif_get_*
    // 2. Acquire JNIEnv via mob_zig.jni.AttachCurrentThread (see existing helpers)
    // 3. Call the MobBridge.vendor_usb_open method via cached jmethodID
    // 4. Return enif_make_atom(env, \"ok\") or an error term
}

Useful neighbors:

  • mob_zig.zig — JNI bindings, libc, jmethodID cache (Cache struct + getOrInit)
  • mob_erts.zig — NIF helpers (makeAtom, makeBinary, getBinary, sendToPid, etc.)
  • mob_beam.zigmob_send_* helpers (the C-side mob_send_vendor_usb_* callbacks the Kotlin reader thread invokes need new Zig siblings here, or wire directly into existing dispatch)

The PR's C code uses jmethodID caching at android/jni/mob_beam.h — that file is still C and may need to be touched, OR the cache slots can move into mob_zig.zig's Cache struct. Pick whichever matches the existing pattern most cleanly.

3. The driver_tab registration

Each NIF needs to land in the static driver table. The Zig NIFs in mob_nif.zig are registered via the generated driver_tab_android.zig (see MobDev.StaticNifs in mob_dev — the registration happens via mob.exs :static_nifs). For built-in mob NIFs (not user project NIFs), there's a direct registration in mob_nif.zig's NIF table. Follow the pattern for the 30+ existing NIFs.

4. Clean-merge parts of mob#5 (no porting needed)

These files have only textual conflicts and can be brought over after the Zig port lands:

File What it adds Conflict shape
lib/mob/vendor_usb.ex Whole new module, 333 lines None (new file)
test/mob/vendor_usb_test.exs 10 tests None (new file)
lib/mob/screen.ex Adds one handle_info({:peripheral, :vendor_usb, ...}) clause that routes through Mob.VendorUsb.normalize_message/1 Trivial; new clause slots into the message routing pipeline before {:mob, :back}
src/mob_nif.erl Adds 7 entries to -export([...]) and -nifs([...]) Trivial textual — current master's lists differ by surrounding NIFs but the additions don't collide
ios/mob_nif.m Appends 60 lines of unsupported-stubs at end of file Trivial — likely appends after current end-of-file content
android/jni/mob_beam.h 13 lines of extern decls Needs review against current mob_beam.h — may be obsolete if delivery moved to Zig

5. mob_new#2 (templates) — mostly clean

The Kotlin code in MobBridge.kt.eex and the manifest changes are unaffected by the Zig migration (that was a runtime-side change; templates still emit beam_jni.c.eex for downstream projects' JNI bridges). Expect textual conflicts only.

The beam_jni.c.eex change (+58 lines of native trampolines) lands as-is because downstream-project glue is still C in the generator.

Done definition

  • mob#5 rebased onto current master; 7 NIFs implemented in android/jni/mob_nif.zig
  • mix mob.deploy --native --device <android-arm64> succeeds for a project that uses Mob.VendorUsb
  • The Mob.VendorUsb lifecycle (list_devices → request_permission → open → start_reading + bulk_write → stop_reading → close) works end-to-end on a real vendor-class device. The original contributor's test rig is AtomVM ESP32 + Taixin TX-AH HaLow modem over a OnePlus CPH2451; any vendor-class USB device with bulk IN/OUT endpoints should exercise the same code path.
  • mob_new#2 rebased onto current master and merges cleanly
  • lib/mob/screen.ex :peripheral routing exercised by a screen that handles a {:peripheral, :vendor_usb, :data, session, binary} event

Hardware caveat

The original test rig (HaLow modem + ESP32) is non-trivial to reproduce. If the agent doing this port doesn't have a vendor-class USB device on hand, stop after the rebased PR builds + tests + deploys without crashing, and ping @HeroesLament to verify the end-to-end lifecycle on their device. Their PR description is explicit about the bug classes they hit (nil → JSON "nil" string, getInt JSONException → BEAM crash); those are not catchable without the real hardware in the loop.

References

  • Migration commit: `b091e67` ("Phase 6b iter 3d (mob side): finale — delete mob_nif.c, all-Zig NIF surface")
  • Zig NIF entry point pattern: `android/jni/mob_nif.zig` lines 56, 71, 90, 306, 400, 425, 482, 492, 503, 520 (each is one NIF)
  • JNI / libc / Android FFI: `android/jni/mob_zig.zig`
  • Static NIF table generation: `MobDev.StaticNifs` in `mob_dev/lib/mob_dev/static_nifs.ex`

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions