What happened?
react-native-nitro-image@0.15.1 crashes at app startup on Android when the app is launched on the React Native debugOptimized build variant (debug app code linked against the release React Native prefab AAR — RN 0.84's "fast dev" variant).
libNitroImage.so fails to dlopen because the vtable for facebook::react::DebugStringConvertible is not exported from the release-flavor libreactnative.so. NitroImage's generated Fabric props class (HybridNitroImageViewProps) inherits from react::ViewProps → react::Props → react::DebugStringConvertible, so the vtable for that base class must be available at link time AND at runtime.
NitroImageOnLoad E Failed to load NitroImage C++ library!
java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol
"_ZTVN8facebook5react22DebugStringConvertibleE"
referenced by ".../lib/arm64/libNitroImage.so"...
at com.margelo.nitro.image.NitroImageOnLoad$Companion.initializeNative(NitroImageOnLoad.kt:25)
at com.margelo.nitro.image.NitroImagePackage.<clinit>(NitroImagePackage.java:44)
at com.facebook.react.PackageList.getPackages(PackageList.java:56)
...
Diagnosis
Comparing the two flavors of libreactnative.so shipped by RN 0.84.1 (via nm -D):
| Symbol |
Debug prefab (used at link time by NitroImage) |
Release prefab (used at runtime in *Optimized) |
_ZTIN8facebook5react22DebugStringConvertibleE (typeinfo) |
V (weak) |
V (weak) |
_ZTSN8facebook5react22DebugStringConvertibleE (typeinfo name) |
V (weak) |
V (weak) |
_ZTVN8facebook5react22DebugStringConvertibleE (vtable) |
D (data, present) |
stripped |
NitroImage builds against the debug prefab (under node_modules/react-native-nitro-image/android/build/intermediates/cmake/debug/obj/arm64-v8a/libreactnative.so, vtable present), so linking succeeds. The APK however bundles the release prefab (vtable removed by --gc-sections / hidden visibility), so dlopen fails at startup.
react-native-nitro-modules alone does NOT trigger this — the issue only appears once react-native-nitro-image is added, because only NitroImage's nitrogen-generated Fabric code subclasses RN-internal classes that need the DebugStringConvertible parent vtable. Plain HybridObject doesn't.
Versions
react-native@0.84.1
react-native-nitro-modules@0.35.9
react-native-nitro-image@0.15.1
react-native-vision-camera@5.0.11 (transitive; pulls in nitro-image)
- New Architecture: enabled (default in RN 0.84)
- AGP 8.12.0, NDK from RN's
ndkVersion
- Reproduces on the standard
debugOptimized variant generated by the RN Gradle plugin
Workarounds
- Run the plain
debug variant (not debugOptimized) — the debug prefab still exports the vtable. Slower dev startup but works. Confirmed.
- Release builds are unaffected (everything is the release prefab consistently).
- Forcing
*Optimized variants to fall back to the debug RN prefab via matchingFallbacks also unblocks, but it defeats the purpose of debugOptimized.
Possible upstream fixes
- Make NitroImage's nitrogen-generated Fabric subclasses self-sufficient w.r.t. the
DebugStringConvertible vtable (e.g. provide an out-of-line virtual destructor / explicitly override the virtual chain so the parent vtable is no longer required at dlopen).
- Or guard the
HybridNitroImageViewComponent/HybridNitroImageViewProps generation behind a flag, so consumers that don't actually use <NitroImage /> (e.g. only use Photo.toImage() / non-component APIs from react-native-vision-camera) can opt out.
- Or document the
*Optimized incompatibility explicitly.
Happy to test patches.
What happened?
react-native-nitro-image@0.15.1crashes at app startup on Android when the app is launched on the React NativedebugOptimizedbuild variant (debug app code linked against the release React Native prefab AAR — RN 0.84's "fast dev" variant).libNitroImage.sofails to dlopen because the vtable forfacebook::react::DebugStringConvertibleis not exported from the release-flavorlibreactnative.so. NitroImage's generated Fabric props class (HybridNitroImageViewProps) inherits fromreact::ViewProps→react::Props→react::DebugStringConvertible, so the vtable for that base class must be available at link time AND at runtime.Diagnosis
Comparing the two flavors of
libreactnative.soshipped by RN 0.84.1 (vianm -D):*Optimized)_ZTIN8facebook5react22DebugStringConvertibleE(typeinfo)V(weak)V(weak)_ZTSN8facebook5react22DebugStringConvertibleE(typeinfo name)V(weak)V(weak)_ZTVN8facebook5react22DebugStringConvertibleE(vtable)D(data, present)NitroImage builds against the debug prefab (under
node_modules/react-native-nitro-image/android/build/intermediates/cmake/debug/obj/arm64-v8a/libreactnative.so, vtable present), so linking succeeds. The APK however bundles the release prefab (vtable removed by--gc-sections/ hidden visibility), so dlopen fails at startup.react-native-nitro-modulesalone does NOT trigger this — the issue only appears oncereact-native-nitro-imageis added, because only NitroImage's nitrogen-generated Fabric code subclasses RN-internal classes that need theDebugStringConvertibleparent vtable. PlainHybridObjectdoesn't.Versions
react-native@0.84.1react-native-nitro-modules@0.35.9react-native-nitro-image@0.15.1react-native-vision-camera@5.0.11(transitive; pulls in nitro-image)ndkVersiondebugOptimizedvariant generated by the RN Gradle pluginWorkarounds
debugvariant (notdebugOptimized) — the debug prefab still exports the vtable. Slower dev startup but works. Confirmed.*Optimizedvariants to fall back to the debug RN prefab viamatchingFallbacksalso unblocks, but it defeats the purpose ofdebugOptimized.Possible upstream fixes
DebugStringConvertiblevtable (e.g. provide an out-of-line virtual destructor / explicitly override the virtual chain so the parent vtable is no longer required at dlopen).HybridNitroImageViewComponent/HybridNitroImageViewPropsgeneration behind a flag, so consumers that don't actually use<NitroImage />(e.g. only usePhoto.toImage()/ non-component APIs fromreact-native-vision-camera) can opt out.*Optimizedincompatibility explicitly.Happy to test patches.