| name | native-addon-loading |
| description | Diagnose and fix native (C/C++) Node.js addon loading in this repo's embedded nodejs-mobile runtime — the rolled-up backend's `__loadAddon` helper, Android `jniLibs/lib<name>__<version>.so` (bare-name dlopen from the APK mmap), and iOS `<name>__<version>.xcframework` (Embed & Sign into the .app). Use when a native module fails to load at runtime, when you see `dlopen`, `UnsatisfiedLinkError`, `cannot locate symbol`, `Library not loaded`, `image not found`, `code signature`, a `p_align`/16 KB alignment APK reject, or a prebuild-download/404 from `scripts/build-backend.ts`; or when touching `sodium-native`, `better-sqlite3`, `crc-native`, `quickbit-native`, `rabin-native`, `simdle-native`, `fs-native-extensions`, the rollup addon-loader plugin, `jniLibs`, or `ios/Frameworks`. |
Native addon loading (embedded nodejs-mobile)
The embedded Node backend can't load native addons the normal way: the .node
files never ship, and the addon resolvers (bindings, node-gyp-build,
require.addon) can't reach where the binaries actually live. The build
rewrites every loader call at bundle time and ships the binaries through each
platform's standard packaging. When something fails to load, the cause is almost
always in that pipeline — not the addon's own code.
The seven native modules
Source of truth: scripts/lib/native-modules.ts.
| Module | N-API? |
|---|
better-sqlite3 | no (node-bindings, lazy on first new Database()) |
crc-native, fs-native-extensions, quickbit-native, rabin-native, simdle-native, sodium-native | yes |
scripts/build-backend.ts enumerates every concrete (name, version) pair from
the backend's resolved node_modules (top-level and nested — a dep graph can
carry two versions of the same addon) and emits one artifact per pair per
platform. Versions come from the backend lockfile, never a hand-maintained list.
How loading actually works
backend/rollup-plugins/rollup-plugin-addon-loader.js replaces three patterns
with __loadAddon(name, version):
require('bindings')('foo.node') → __loadAddon('<pkg>', '<ver>')
require('node-gyp-build')(__dirname) → __loadAddon('<pkg>', '<ver>')
require.addon('.', __filename) → __loadAddon('<pkg>', '<ver>')
name/version come from the package.json that owns the rewritten file, so
multi-version graphs get the right binary per importer. The key is
<name>__<version> (double underscore — @ is unvalidated in framework
dir-names / Mach-O install names).
The __loadAddon helper is injected as each output bundle's banner:
- Android (
androidAddonLoaderBanner): process.dlopen(mod, 'lib' + key + '.so')
— a bare filename, no path. Bionic resolves it against the APK's
lib/<abi>/ mmap region. This only works because AndroidManifest.xml has
android:extractNativeLibs="false" and android/build.gradle keeps libs
uncompressed/aligned (packagingOptions.jniLibs.useLegacyPackaging = false).
A full-path dlopen would fail — nothing is unpacked to disk.
- iOS (
iosAddonLoaderBanner): process.dlopen(mod, NATIVE_LIB_DIR + '/' + key + '.framework/' + key).
NATIVE_LIB_DIR is set by Swift (AppLifecycleDelegate.nodeService's
nodeEntryPoint) to <App>.app/Frameworks before NodeMobileStartNode.
Xcode's Embed & Sign phase populates that dir from the vendored xcframeworks.
Where the artifacts live
| Platform | Artifact (generated by scripts/build-backend.ts) |
|---|
| Android | android/src/main/jniLibs/<abi>/lib<name>__<version>.so per ABI (armeabi-v7a, arm64-v8a, x86_64) |
| iOS | ios/Frameworks/<name>__<version>.xcframework (device ios-arm64 + fat sim slice), declared via s.vendored_frameworks glob in ios/ComapeoCore.podspec |
Prebuilt binaries are downloaded by build-backend.ts from
digidem/<name>-nodejs-mobile GitHub Releases. The xcframework wrap needs macOS
Xcode tooling (xcodebuild, lipo, install_name_tool) and is skipped on Linux
(Android CI).
Diagnostic order — check the pipeline, not the addon
- Did the build run?
npm install alone does NOT build the backend. Run
npm run backend:build (or npm run setup). Then confirm the artifact exists:
- Android:
ls android/src/main/jniLibs/arm64-v8a/ | grep <name>
- iOS:
ls ios/Frameworks/ | grep <name>
A missing lib<name>__<version>.so / <name>__<version>.xcframework is the
most common cause.
- Did the rewrite land in the bundle? The rewrite is version-specific, so a
bundle/binary version skew breaks loading silently:
grep -o "__loadAddon('[^']*', '[^']*')" android/src/main/assets/nodejs-project/index.mjs
Every (name, version) printed must have a matching artifact on disk. If the
bundle asks for a version you didn't emit, rebuild the backend so both sides
agree.
- Read the runtime error (see per-platform sections). The Node side logs to
the addon failing — the dlopen message names the file it couldn't load.
Android symptoms
iOS symptoms
dlopen(...): Library not loaded / image not found → the xcframework wasn't
embedded. Confirm ios/Frameworks/<name>__<version>.xcframework exists, the
podspec glob covers it, and Xcode's Embed & Sign phase ran (<App>.app/Frameworks/
should contain <name>__<version>.framework/). Verify rpath/embed with
otool -L on the app binary.
code signature invalid on a locally-built/ad-hoc artifact: signatures are
build/device-specific. The BrowserStack device archive is an unsigned
arm64 device binary — it can't run on a simulator at all, regardless of
signing, so don't diagnose it as an addon problem. For a local iOS run build
with expo run:ios (signs with your Apple dev team, so the embedded backend's
keychain access works).
NATIVE_LIB_DIR unset → Swift didn't export it before NodeMobileStartNode;
the JS helper then dlopens against undefined/....
Prebuild fetch failures (build-backend.ts)
- A 404 / "no prebuild" download usually means a version skew (lockfile vs
package.json) requesting a release tag that isn't published on
digidem/<name>-nodejs-mobile. Check npm ls <name> --prefix backend against
the release tags; if you bumped an addon, the prebuild must be published first.
False leads (don't chase these)
- "The
.so/addon is broken, rebuild the C code." Almost never. The binaries
are prebuilt and known-good; the failure is a missing artifact, a version skew
between bundle and binary, an ABI mismatch, or an embed/signing gap.
UnsatisfiedLinkError in :ComapeoCore = addon problem. No — that's the
process-isolation / React-Native-init guard (ComapeoProcessGuard.kt), a
different subsystem.