Skip to content

[Android] Native library symbol resolution incompatibility with third-party SDKs using wrapped symbols #54220

@hyochan

Description

@hyochan

Description

React Native's native library loading mechanism is incompatible with third-party SDKs that use wrapped C++ symbols (with __wrap__ prefix). This blocks integration of Meta Horizon Platform SDK and potentially other SDKs with similar symbol requirements.

The Core Issue

React Native loads libc++_shared.so with standard symbols during static initialization (via ReactNativeJNISoLoader.staticInit()), preventing later-loaded native libraries from finding wrapped symbols they depend on.

Environment

  • React Native: 0.76.9 (Expo SDK 52), 0.79.x (Expo SDK 53), 0.81.x (Expo SDK 54)
  • Platform: Android
  • Device: Meta Quest 3
  • SDK: Meta Horizon Platform SDK (com.meta.horizon.platform.ovr:android-platform-sdk:72)
  • Build System: Gradle 8.x, CMake 3.x
  • NDK: 26.x

Platform Comparison

Platform Status Repository
Pure Android (Kotlin) ✅ Works Example
Flutter ✅ Works Example
React Native ❌ Crashes Tested on Expo SDK 52, 53, 54 (RN 0.76.x, 0.77.x)

Root Cause Analysis

Symbol Mismatch:

  • APK's libc++_shared.so contains standard symbols: _ZTVSt12length_error
  • Third-party SDK expects wrapped symbols: __wrap__ZTVSt12length_error

React Native's Loading Sequence:

  1. Static Initialization - CatalystInstanceImpl.java:54:

    static {
      ReactNativeJNISoLoader.staticInit();  // ← Loads during class initialization
    }
  2. SoLoader Call - ReactNativeJNISoLoader.kt#L19-L27:

    @Synchronized
    fun staticInit() {
      SoLoader.loadLibrary("reactnativejni")  // ← Loads libc++_shared.so with standard symbols
    }
  3. Hard-coded STL Configuration:

Result: One shared libc++_shared.so for all native modules, loaded before third-party code runs, symbol table fixed at load time, no mechanism to provide alternative symbols.

Attempted Solutions (All Failed)

1. Static STL Linking

  • Tried patching React Native to use ANDROID_STL=c++_static
  • Result: APK still contains libc++_shared.so because third-party modules (reanimated, screens, gesture-handler, etc.) are prebuilt AARs with c++_shared

2. pickFirst Packaging

  • Used pickFirst '**/libc++_shared.so'
  • Result: Build succeeds but runtime crash persists (only selects which file to package, doesn't solve symbol mismatch)

3. Build Flavor Separation

  • Separated Play and Horizon builds using source sets
  • Result: Successfully separates code/dependencies at build time, but React Native still loads standard libc++ first at runtime

Questions for React Native Team

  1. Is there a way to support custom symbol resolution for specific native libraries?
  2. Can library loading order be controlled for special cases?
  3. Is this a known limitation that should be documented?
  4. Should SDK vendors be contacted to avoid wrapped symbols for RN compatibility?

Steps to reproduce

1. Create React Native Project

# Using Expo
npx create-expo-app@latest HorizonTest
cd HorizonTest

# Or bare React Native
npx react-native init HorizonTest
cd HorizonTest

2. Add Horizon Platform SDK Dependency

android/build.gradle:

allprojects {
  repositories {
    maven {
      url "https://maven.horizon.meta.com/"
    }
  }
}

android/app/build.gradle:

dependencies {
  implementation 'com.meta.horizon.platform.ovr:android-platform-sdk:72'
  implementation 'com.meta.horizon.billingclient.api:horizon-billing-compatibility:1.1.1'
}

3. Add Oculus App ID to AndroidManifest.xml

<application>
  <meta-data
    android:name="com.oculus.vr.focusaware"
    android:value="true" />
  <meta-data
    android:name="com.oculus.application_id"
    android:value="YOUR_APP_ID" />
</application>

4. Initialize and Connect to HorizonBillingClient

Kotlin/Java code:

import com.meta.horizon.billingclient.api.HorizonBillingClient
import com.meta.horizon.billingclient.api.BillingClientStateListener
import com.meta.horizon.billingclient.api.BillingResult

val billingClient = HorizonBillingClient.newBuilder(context)
    .setListener { billingResult, purchases ->
        // Handle purchases
    }
    .build()

// This call will trigger the crash
billingClient.startConnection(object : BillingClientStateListener {
    override fun onBillingSetupFinished(billingResult: BillingResult) {
        // Won't reach here
    }

    override fun onBillingServiceDisconnected() {
        // Won't reach here
    }
})

5. Build and Run on Meta Quest Device

# Build APK
cd android && ./gradlew assembleDebug

# Install on Quest
adb install app/build/outputs/apk/debug/app-debug.apk

# Run app
adb shell am start -n com.your.package/.MainActivity

6. Observe Crash

App will crash immediately when startConnection() is called.

React Native Version

0.81.4

Affected Platforms

Runtime - Android

Output of npx @react-native-community/cli info

System:
  OS: macOS 15.6.1
  CPU: (10) arm64 Apple M4
  Memory: 167.38 MB / 16.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 20.19.0
    path: /opt/homebrew/bin/node
  Yarn:
    version: 3.6.1
    path: /opt/homebrew/bin/yarn
  npm:
    version: 10.8.2
    path: /opt/homebrew/bin/npm
  Watchman:
    version: 2025.03.10.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.16.2
    path: /opt/homebrew/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 25.0
      - iOS 26.0
      - macOS 26.0
      - tvOS 26.0
      - visionOS 26.0
      - watchOS 26.0
  Android SDK: Not Found
IDEs:
  Android Studio: 2024.2 AI-242.23339.11.2421.12483815
  Xcode:
    version: 26.0.1/17A400
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.12
    path: /usr/bin/javac
  Ruby:
    version: 2.7.2
    path: /Users/crossplatformkorea/.rbenv/shims/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 19.1.0
    wanted: 19.1.0
  react-native:
    installed: 0.81.4
    wanted: 0.81.4
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: Not found
  newArchEnabled: Not found
iOS:
  hermesEnabled: Not found
  newArchEnabled: Not found

Stacktrace or Logs

[useIAP] initConnection failed: [Error: Call to function 'ExpoIap.initConnection' has been rejected.
→ Caused by: java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "__wrap__ZTVSt12length_error" referenced by "/data/app/~~H2QqsKUEITvWPnjTyKH8MA==/dev.hyo.martie-jmdTuEYhK2gA-IVo9mG0Pw==/base.apk!/lib/arm64-v8a/libpsdk_jni.so"...]



FATAL EXCEPTION: main
Process: dev.hyo.martie, PID: 398
java.lang.UnsatisfiedLinkError: dlopen failed: cannot locate symbol "__wrap__ZTVSt12length_error" referenced by "/data/app/~~XXX/dev.hyo.martie-YYY/base.apk!/lib/arm64-v8a/libpsdk_jni.so"...
    at java.lang.Runtime.loadLibrary0(Runtime.java:1071)
    at java.lang.Runtime.loadLibrary0(Runtime.java:1000)
    at java.lang.System.loadLibrary(System.java:1893)
    at com.meta.horizon.platform.PlatformSDK.<clinit>(PlatformSDK.java)
    at com.meta.horizon.billingclient.api.HorizonBillingClient.startConnection(HorizonBillingClient.java)

MANDATORY Reproducer

hyochan/expo-iap#105

Screenshots and Videos

Expo App (Failed 😞)

expo.mp4

Android App (Success ✅)

android.mp4

Flutter App (Success ✅)

flutter.mp4

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions