Table of Contents

    Book an Appointment

    INTRODUCTION

    While working on a major architectural upgrade for a high-traffic FinTech mobile application, we set out to transition the platform to Expo SDK 54 and React Native 0.81.5. The goal was to fully leverage the React Native New Architecture and the latest Hermes engine optimizations to handle complex, real-time financial charting.

    Everything functioned seamlessly in our local development environments. However, when we pushed the update to our CI/CD pipeline using EAS Build, the iOS builds abruptly halted. We were greeted with a wall of obscure C++ linker errors explicitly complaining about missing symbols for the arm64 architecture.

    When you are building applications that handle secure financial transactions, unpredictable deployment failures are unacceptable. This issue surfaced a deep architectural friction point between prebuilt React Native frameworks and locally compiled native modules. We realized that resolving this required diving into the C++ ABI (Application Binary Interface) layer of the JavaScript Interface (JSI). This challenge inspired this article so other engineering teams can avoid similar deployment blockers when modernizing their mobile stacks.

    PROBLEM CONTEXT

    Modern React Native relies heavily on JSI to enable synchronous communication between JavaScript and native C++ code. This is particularly relevant when using advanced libraries like react-native-reanimated or Expo’s native modules, which bypass the old asynchronous bridge for performance.

    In our FinTech application, the architecture relies on Expo Application Services (EAS) to orchestrate cloud builds. To speed up CI/CD execution, EAS frequently utilizes prebuilt binaries for core React Native dependencies (like React-Core-prebuilt). The application integrates heavily with native modules that must be compiled on the fly during the build process.

    The issue appears at the boundary where the locally compiled C++ code (from Expo modules and Reanimated) attempts to link against the precompiled C++ code inside the React Native and Hermes frameworks. If the C++ ABI versions do not perfectly align, the iOS linker cannot resolve the symbols, leading to a hard build failure.

    WHAT WENT WRONG

    The EAS build logs revealed a fatal linking error during the final stages of the iOS compilation phase. The symptoms were isolated to arm64 architecture targets:

    Undefined symbols for architecture arm64:
      "vtable for hermes::vm::NopCrashManager", referenced from:
          hermes::vm::RuntimeConfig::RuntimeConfig() in libExpoModulesCore.a[7](EXJavaScriptRuntime.o)
      "typeinfo for facebook::jsi::HostObject", referenced from:
          typeinfo for expo::ExpoModulesHostObject in libExpoModulesCore.a[26]
          typeinfo for facebook::react::TurboModule in libRNReanimated.a[49]
      "typeinfo for facebook::jsi::NativeState", referenced from:
          typeinfo for expo::EventEmitter::NativeState in libExpoModulesCore.a[3]
    ld: symbol(s) not found for architecture arm64
    clang: error: linker command failed with exit code 1

    Analyzing the logs, we noticed that libExpoModulesCore.a and libRNReanimated.a were failing to locate basic virtual tables (vtable) and type information (typeinfo) for core Hermes and JSI classes. We also observed references containing specific ABI tags like [abi:ne180100] in the raw symbol outputs.

    This confirmed an architectural oversight in the dependency chain: the locally compiled static libraries were being built with headers expecting one specific C++ standard and ABI structure, while the prebuilt React Native frameworks pulled in by EAS exported a different ABI version. The missing vtable usually indicates that a virtual member function lacks a definition within the expected ABI namespace.

    HOW WE APPROACHED THE SOLUTION

    Our initial diagnostic steps focused on isolating the variables in the EAS environment:

    • Toggling the New Architecture: We explicitly enabled and disabled newArchEnabled. The error persisted, indicating the issue was tied to the underlying JSI/Hermes engine integration, not just the TurboModules architecture.
    • Switching Build Environments: We migrated the EAS build image between different macOS and Xcode versions (from xcode-16.2 to newer environments). The linker failure remained constant, ruling out an Xcode-specific bug.
    • Forcing Source Compilation: We set buildReactNativeFromSource: true within the expo-build-properties plugin to force React Native to compile from source rather than using prebuilts. Surprisingly, this still failed with the same linker error, suggesting the injected compilation flags were universally misaligned.
    • Isolating Dependencies: We temporarily uninstalled react-native-reanimated and cleared the ios/ directory to force a pristine expo prebuild. The error shifted entirely to libExpoModulesCore.a, proving the issue was systemic to how C++ headers were being resolved across all native modules.

    We deduced that the C++ language standard applied during the local compilation phase of the CocoaPods was diverging from the standard used to build the Hermes engine. React Native 0.81.5 introduces stricter C++ requirements (often migrating toward C++20). If the local Pods default to C++17 or lack the correct compiler flags, the resulting ABI tags will mismatch, breaking the linker.

    When organizations hire software developer teams to handle complex migrations, they expect this level of root-cause analysis rather than blindly applying temporary patches.

    FINAL IMPLEMENTATION

    To resolve the ABI mismatch without permanently ejecting from the managed Expo workflow, we needed to enforce strict C++ standard alignment across every native module compiled during the Pod installation phase.

    We achieved this by injecting a robust post_install hook into the iOS Podfile. This script iterates through every target and forcefully overrides the C++ language standard to match the one expected by the React Native 0.81.5 prebuilts.

    Here is the sanitized implementation deployed to our configuration:

    post_install do |installer|
      react_native_post_install(installer)
      installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
          # Force alignment of C++ language standard for ABI compatibility
          config.build_settings['CLANG_CXX_LANGUAGE_STANDARD'] = 'c++20'
          config.build_settings['CLANG_CXX_LIBRARY'] = 'libc++'
          
          # Ensure existing C++ flags are preserved while adding Folly/JSI specific overrides
          existing_flags = config.build_settings['OTHER_CPLUSPLUSFLAGS'] || ['$(inherited)']
          if existing_flags.is_a?(String)
            existing_flags = existing_flags.split(' ')
          end
          
          unless existing_flags.include?('-DFOLLY_CFG_NO_COROUTINES=1')
            existing_flags << '-DFOLLY_CFG_NO_COROUTINES=1'
          end
          
          config.build_settings['OTHER_CPLUSPLUSFLAGS'] = existing_flags.join(' ')
        end
      end
    end
    

    Validation Steps:

    • We triggered a fresh expo prebuild --clean locally to generate the updated Podfile.
    • We cleared the EAS build cache to ensure no tainted intermediate object files (.o) were reused.
    • We executed the cloud build. The C++ compilers across all native modules correctly aligned their ABI tags with the Hermes headers, and the arm64 linker completed successfully.

    By programmatically aligning the C++ standards via the CocoaPods lifecycle, we maintained the convenience of EAS prebuilds while satisfying the stringent linking requirements of modern React Native architectures.

    LESSONS FOR ENGINEERING TEAMS

    This scenario underscores several critical insights for teams managing large-scale mobile platforms:

    • ABI Boundaries are Unforgiving: When dealing with JSI and Hermes, treating JavaScript as merely a scripting layer is a mistake. Your mobile application is a C++ application, and C++ ABI rules apply strictly.
    • Understand Your CI/CD Caching: EAS and other cloud build providers cache precompiled artifacts aggressively. When debugging linker errors, always clear the remote cache to validate your configuration changes.
    • Prebuilts Require Flag Parity: If you consume prebuilt binary frameworks, your local compilation flags (like CLANG_CXX_LANGUAGE_STANDARD) must be in perfect parity with the flags used to generate those frameworks.
    • Automate Dependency Fixes: Avoid manual patching of Xcode projects. Use post_install hooks or Expo Config Plugins to programmatically apply build settings, ensuring reproducibility across environments.
    • Version Alignment is Critical: Moving to cutting-edge versions like Expo SDK 54 requires ensuring that third-party modules (like Reanimated) have explicitly published support for the new JSI headers.
    • Strategic Talent Allocation: Transitioning to the New Architecture is not a trivial task. When you hire react native developers for enterprise modernization, ensure they have proven experience traversing the bridge between JavaScript and native C++ build systems.
    • Continuous Monitoring: Linker errors are often the first symptom of deeper architectural incompatibilities. Set up your pipelines to fail fast and retain build artifacts for rapid debugging.

    WRAP UP

    Upgrading to Expo SDK 54 and React Native 0.81.5 brings incredible performance benefits, but it also introduces strict C++ ABI alignment requirements. By understanding how JSI interfaces with the Hermes engine at the compiler level, we successfully unblocked our CI/CD pipeline and delivered a highly performant, resilient FinTech application. If you need engineering maturity to navigate complex architectural upgrades, contact us to explore how our dedicated teams can drive your next deployment.

    Social Hashtags

    #ReactNative #ExpoSDK54 #Expo #HermesEngine #JSI #MobileDevelopment #iOSDevelopment #EASBuild #ReactNativeDev #FinTechDevelopment #TurboModules #NewArchitecture #SoftwareEngineering #DevOps #CPlusPlus

     

    Frequently Asked Questions