Symptom
A project with a Rustler-backed NIF that's enabled for Android (e.g. `mob.exs` `:static_nifs` entry with `archs: [:all]` or `[:android_arm64, :android_arm32]`) deploys cleanly but the BEAM dies on app launch with:
```
thread '' panicked at rustler-0.37.4/src/sys/nif_filler.rs:27:70:
called `Result::unwrap()` on an `Err` value:
DlSym { source: "undefined symbol: enif_priv_data" }
```
Reproduced today against `nif_combo` on the Android emulator (`emulator-5554`, sdk_gphone64_arm64) under OTP 29.0 (`@otp_hash 550d7b78`).
Root cause
`rustler-0.37.4/src/sys/nif_filler.rs` populates its NIF dispatch table by calling:
```rust
let lib = unsafe { Library::open(None::<&OsStr>, RTLD_NOW | RTLD_GLOBAL) };
// then for each enif_* symbol:
self.lib.get::(name.as_bytes()).unwrap();
```
`Library::open(None, ...)` opens the main executable's symbol scope (effectively `dlopen(NULL, ...)`). On iOS/macOS that scope sees everything in the binary, including the BEAM's `enif_` functions. On Android, the app library (`libnif_combo.so` here) is loaded by the Java runtime with `RTLD_LOCAL`, so its statically-linked-in `enif_` symbols are NOT in the global scope. The first `get("enif_priv_data")` returns `DlSym` error, `.unwrap()` panics, the BEAM dies.
This is the same shape of issue we hit and worked around for the BEAM's own crypto NIF dispatch (load the static `crypto_nif_init` via `erts_static_nif_tab` instead of dlopen `crypto.so`). Rustler's filler doesn't know about that machinery and falls into the same RTLD_LOCAL hole.
Confirmed scope
- C NIF (handwritten, no rustler init filler) ✓ works on Android
- Pythonx ✓ works on Android (no rustler)
- Zigler-backed NIF ✓ works on Android (after the Bionic-stdlib fix in zigler#zigler-android-armv7)
- Rustler-backed NIF ✗ panics at init
iOS device + iOS sim are unaffected (Rustler runs cleanly there).
Fix candidates (ordered by intrusiveness)
-
Rustler upstream patch — extend `nif_filler.rs` with an Android branch that resolves symbols via a different mechanism (e.g. an init-time function pointer table passed in by the host instead of dlsym).
-
Pre-load via RTLD_GLOBAL from JNI — before `mob_start_beam()`, have the JNI-side code re-`dlopen` its own .so with `RTLD_GLOBAL` so its symbols become visible to subsequent `dlopen(NULL)` calls. Cheap; doesn't require touching rustler.
-
Linker flags on libnif_combo.so — pass `-Wl,--export-dynamic` (or equivalent) at the link step so `enif_*` symbols land in the dynamic symbol table even though the shared library is loaded RTLD_LOCAL. Need to verify whether dlopen(NULL) on Android sees the dynsym table of the calling library; if yes, this is a one-line zig build change.
-
Provide a custom init helper — expose a `mob_rustler_init(table_ptr)` from `libbeam.a` that hands rustler the dispatch table directly, then patch rustler to call that helper on Android.
Option 2 is probably the smallest viable patch. Option 3 is the cleanest if it works.
Reference
- The other agent's `mob_dev-android-armv7` merge note claimed "Rustler and rustler-related dlsym fallback already arch-agnostic" but that turns out to refer to mob_dev's wiring (per-ABI cross-compile, target atom split), not the rustler runtime symbol resolution. Verifying today against nif_combo with all three Android targets enabled, the Rustler crash is reproducible.
- Companion issue #6 is unrelated — that's the C → Zig port for HeroesLament's vendor_usb PR.
Workaround for now
Set `archs: [:ios]` on Rust NIF entries in `mob.exs` until this is fixed. C, Zig, and Python NIFs all work on Android.
Symptom
A project with a Rustler-backed NIF that's enabled for Android (e.g. `mob.exs` `:static_nifs` entry with `archs: [:all]` or `[:android_arm64, :android_arm32]`) deploys cleanly but the BEAM dies on app launch with:
```
thread '' panicked at rustler-0.37.4/src/sys/nif_filler.rs:27:70:
called `Result::unwrap()` on an `Err` value:
DlSym { source: "undefined symbol: enif_priv_data" }
```
Reproduced today against `nif_combo` on the Android emulator (`emulator-5554`, sdk_gphone64_arm64) under OTP 29.0 (`@otp_hash 550d7b78`).
Root cause
`rustler-0.37.4/src/sys/nif_filler.rs` populates its NIF dispatch table by calling:
```rust
let lib = unsafe { Library::open(None::<&OsStr>, RTLD_NOW | RTLD_GLOBAL) };
// then for each enif_* symbol:
self.lib.get::(name.as_bytes()).unwrap();
```
`Library::open(None, ...)` opens the main executable's symbol scope (effectively `dlopen(NULL, ...)`). On iOS/macOS that scope sees everything in the binary, including the BEAM's `enif_` functions. On Android, the app library (`libnif_combo.so` here) is loaded by the Java runtime with `RTLD_LOCAL`, so its statically-linked-in `enif_` symbols are NOT in the global scope. The first `get("enif_priv_data")` returns `DlSym` error, `.unwrap()` panics, the BEAM dies.
This is the same shape of issue we hit and worked around for the BEAM's own crypto NIF dispatch (load the static `crypto_nif_init` via `erts_static_nif_tab` instead of dlopen `crypto.so`). Rustler's filler doesn't know about that machinery and falls into the same RTLD_LOCAL hole.
Confirmed scope
iOS device + iOS sim are unaffected (Rustler runs cleanly there).
Fix candidates (ordered by intrusiveness)
Rustler upstream patch — extend `nif_filler.rs` with an Android branch that resolves symbols via a different mechanism (e.g. an init-time function pointer table passed in by the host instead of dlsym).
Pre-load via RTLD_GLOBAL from JNI — before `mob_start_beam()`, have the JNI-side code re-`dlopen` its own .so with `RTLD_GLOBAL` so its symbols become visible to subsequent `dlopen(NULL)` calls. Cheap; doesn't require touching rustler.
Linker flags on libnif_combo.so — pass `-Wl,--export-dynamic` (or equivalent) at the link step so `enif_*` symbols land in the dynamic symbol table even though the shared library is loaded RTLD_LOCAL. Need to verify whether dlopen(NULL) on Android sees the dynsym table of the calling library; if yes, this is a one-line zig build change.
Provide a custom init helper — expose a `mob_rustler_init(table_ptr)` from `libbeam.a` that hands rustler the dispatch table directly, then patch rustler to call that helper on Android.
Option 2 is probably the smallest viable patch. Option 3 is the cleanest if it works.
Reference
Workaround for now
Set `archs: [:ios]` on Rust NIF entries in `mob.exs` until this is fixed. C, Zig, and Python NIFs all work on Android.