DEV Community

samuel onireti
samuel onireti

Posted on

Cracking Open the Magic of React Native + C++ for JSI Bindings

Let me start by saying this: I've been knee-deep in React Native development for what feels like forever. From building simple apps to wrestling with performance bottlenecks, I've seen it all. But nothing quite lit a fire under me like discovering the JavaScript Interface (JSI) and how it pairs with C++ to create lightning-fast bindings. It's like peeling back the curtain on a magic show – once you understand the tricks, you can't unsee the elegance. In this article, I'll share my personal dive into this world, walking you through the why, the how, and the "aha" moments that made it click for me. If you're like me, always chasing that next level of app performance, buckle up.

My First Encounter with JSI: Why Bother?

Picture this: It's late 2023, and I'm optimizing a React Native app that's choking on heavy computations. The old bridge architecture – you know, the one that serializes everything between JavaScript and native code – was killing me with latency. Enter JSI, React Native's shiny new-(ish) way to let JavaScript talk directly to the native runtime. No more JSON parsing, no more async headaches; just pure, synchronous bliss.

JSI is essentially an API that exposes the JavaScript engine (like Hermes or V8) to native code. And when you throw C++ into the mix, you get Turbo Native Modules – cross-platform beasts that run circles around the old Objective-C or Java modules. Why C++? Well, in my experience, it's all about speed and portability. C++ handles low-level operations like a champ, and with the New Architecture in React Native (post-0.68), it lets you write once and deploy on both iOS and Android. I remember thinking, "This could change everything for compute-intensive tasks like image processing or cryptography."

But fair warning: It's not all rainbows. JSI demands you handle memory management yourself – no garbage collection safety net. I learned that the hard way when a dangling pointer crashed my app mid-demo. Still, the payoff? Apps that feel native because they are more native.

Setting Up the Stage: Getting Your Environment Ready

Before we crack open the code, let's talk setup. I started with a fresh React Native project (version 0.76 or later for the best New Architecture support). Enable the New Architecture by setting newArchEnabled=true in your gradle.properties for Android and adding the flag to your iOS Podfile.

You'll need Xcode for iOS and Android Studio for the other side. Oh, and CMake for building C++ – don't skip that. I used react-native-builder-bob to scaffold my module, which saved me hours of boilerplate. Command: npx create-react-native-library@latest my-jsi-module. Choose Turbo Module and C++ options.

Once set up, the real fun begins: defining your JS specs.

Defining the Interface: JS Specs and Codegen

In my first attempt, I created a simple module to reverse a string – nothing fancy, but it taught me the ropes. Start by making a specs folder in your app root. Inside, add

``NativeMyModule.ts`:`

``typescript
import {TurboModule, TurboModuleRegistry} from 'react-native';

export interface Spec extends TurboModule {
  readonly reverseString: (input: string) => string;
}

export default TurboModuleRegistry.getEnforcing<Spec>('NativeMyModule');
``
Enter fullscreen mode Exit fullscreen mode

This is your contract between JS and native. Next, tweak package.json for Codegen:

``json
"codegenConfig": {
  "name": "AppSpecs",
  "type": "modules",
  "jsSrcsDir": "specs",
  "android": {
    "javaPackageName": "com.myapp.specs"
  }
}
``
Enter fullscreen mode Exit fullscreen mode

Run yarn react-native codegen (or whatever your script is), and boom – it generates the bindings. I love how Codegen automates the glue code; it felt like cheating after manual bridging.

The C++ Heart: Implementing the Native Logic

Now, the exciting part – writing C++. Create a shared folder for cross-platform code. In NativeMyModule.h:

``cpp
#pragma once
#include <AppSpecsJSI.h>
#include <memory>
#include <string>

namespace facebook::react {
class NativeMyModule : public NativeMyModuleCxxSpec<NativeMyModule> {
public:
  NativeMyModule(std::shared_ptr<CallInvoker> jsInvoker);
  std::string reverseString(jsi::Runtime& rt, std::string input);
};
} // namespace facebook::react
Enter fullscreen mode Exit fullscreen mode

And in NativeMyModule.cpp:

#include "NativeMyModule.h"

namespace facebook::react {
NativeMyModule::NativeMyModule(std::shared_ptr<CallInvoker> jsInvoker)
    : NativeMyModuleCxxSpec(std::move(jsInvoker)) {}

std::string NativeMyModule::reverseString(jsi::Runtime& rt, std::string input) {
  return std::string(input.rbegin(), input.rend());
}
} // namespace facebook::react
``
Enter fullscreen mode Exit fullscreen mode

See that jsi::Runtime& rt? That's your gateway to the JS world. In my projects, I've used it for everything from string manipulations to exposing complex objects. Pro tip from my mishaps: Always use std::move for ownership transfer to avoid premature deallocation.

Platform-Specific Registration: iOS and Android

iOS Side
For iOS, I focused on bridging the C++ to Objective-C++. In your module's .mm file, install the JSI bindings:

``objective-c
#import <React/RCTBridgeModule.h>
#import "bindings.h"  // Your C++ header

@interface MyModule : NSObject <RCTBridgeModule>
@end

@implementation MyModule

RCT_EXPORT_MODULE(MyModule);

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) {
  RCTCxxBridge *cxxBridge = (RCTCxxBridge *)self.bridge;
  jsi::Runtime *runtime = (jsi::Runtime *)cxxBridge.runtime;
  if (runtime) {
    mymodule::install(*runtime);
    return @YES;
  }
  return @NO;
}

@end
``
Enter fullscreen mode Exit fullscreen mode

In bindings.cpp, define the install function to register a host function:

``cpp
void mymodule::install(jsi::Runtime &rt) {
  auto reverseFunc = jsi::Function::createFromHostFunction(
    rt,
    jsi::PropNameID::forAscii(rt, "reverseString"),
    1,  // Number of args
    [](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) -> jsi::Value {
      std::string input = args[0].asString(rt).utf8(rt);
      std::string reversed(input.rbegin(), input.rend());
      return jsi::String::createFromUtf8(rt, reversed);
    }
  );
  rt.global().setProperty(rt, "reverseString", std::move(reverseFunc));
}
``
Enter fullscreen mode Exit fullscreen mode

I had to debug this for hours because I forgot to check if the runtime was available – always do that on device!

Update your Podfile with the module path and run pod install with RCT_NEW_ARCH_ENABLED=1.

Android Side
Android uses CMake. In jni/CMakeLists.txt:

cmake_minimum_required(VERSION 3.13)
project(mymodule)
include(${REACT_ANDROID_DIR}/cmake-utils/ReactNative-application.cmake)
target_sources(${CMAKE_PROJECT_NAME} PRIVATE ../../shared/NativeMyModule.cpp)
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ../../shared)
Enter fullscreen mode Exit fullscreen mode

Add to build.gradle:

externalNativeBuild {
  cmake {
    path "src/main/jni/CMakeLists.txt"
  }
}
Enter fullscreen mode Exit fullscreen mode

Download and modify OnLoad.cpp to register your module. It's similar to iOS but with JNI flair.

The Gotchas: Memory, Types, and Debugging

Diving into C++ with JSI isn't without pitfalls. From the cheatsheet I leaned on heavily, memory management is key – pointers can bite if you're not careful. For instance, use & for references and * for dereferencing, but watch scopes.

Types are stricter: No loose JS objects; cast everything. I once spent a day fixing a crash from mismatched jsi::Value types. Lambdas are your friends for host functions, but remember to specify -> void if returning nothing.

Debugging? Use jsi::Runtime logs and Chrome DevTools for JS side. On native, LLDB for iOS and Android Studio's debugger.

Advanced Magic: Host Objects and Beyond

Once basics clicked, I leveled up to host objects – C++ classes exposed as JS objects. Inherit from jsi::HostObject and override get/set. I've used this for persistent storage modules, where C++ handles file I/O seamlessly.

Integrating libraries like libpng for image metadata? Game-changer. My app's performance jumped 3x on heavy tasks.

The Spell Is Cast:

Cracking open React Native + C++ for JSI bindings felt like unlocking a secret level in a game I thought I knew. It's personal for me because it turned frustrating lags into smooth experiences. If you're building high-perf apps, give it a shot, start small, like my string reverser, and scale up. The magic is in the direct connection, and once you taste it, there's no going back.

Top comments (0)