rnue
Bridging JS & C++

Building a TurboModule

Using TurboModules to connect JavaScript to C++

In this guide we will dive into the process of building a TurboModule in more detail.

Create a TypeScript spec

Start by creating a new package

This package will contain our spec, and host the generated bridge code.

Write the spec

The spec defines the methods, events and types for the module. The spec is just a TypeScript file that you can write and save anywhere in your native package.

Here is an example of a spec for a native module called ExampleNative. This module will help us access readings from a hardware sensor that is integrated into our Unreal C++ project:

NativeExampleNative.ts
import type { CodegenTypes, TurboModule } from "react-native";
import { TurboModuleRegistry } from "react-native";

export type SensorMode = "slow" | "fast";

export interface SensorReading {
  sensorId: string;
  value: number;
}

export interface Spec extends TurboModule {
  readonly onReading: CodegenTypes.EventEmitter<SensorReading>;

  setEnabled(enabled: boolean): void;
  getLatestValue(): number;
  loadReadings(): Promise<ReadonlyArray<SensorReading>>;
}

export default TurboModuleRegistry.get<Spec>("ExampleNative");

You can refer to the Codegen Typings section for a map of the types that can be used in the spec.

Provide codegen configuration in package.json

This will be used by @rnue/codegen to help it find the spec and know where to output generated boilerplate bridge code.

package.json
{
  "name": "example-native",
  "version": "0.0.1",
  "private": true,
  "main": "./src/index.ts",
  "scripts": {
    "codegen": "rnue-codegen codegen",
    "lint": "eslint .",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "@rnue/codegen": "0.0.3",
    "typescript": "^6.0.3"
  },
  "peerDependencies": {
    "react": "^19.2.3",
    "react-native": "^0.86.0"
  },
  "peerDependenciesMeta": {
    "react": {
      "optional": true
    },
    "react-native": {
      "optional": true
    }
  },
  "codegenConfig": {
    "name": "ExampleNative",
    "type": "modules",
    "jsSrcsDir": "specs",
    "includesGeneratedCode": true,
    "outputDir": {
      "unreal": "unreal/"
    }
  }
}

Verify

At the end of the process you should have a package with roughly the following structure:

NativeExampleNative.ts
index.ts
package.json
tsconfig.json

Generate the boilerplate

Run the following command:

npm run codegen

The result will be an unreal/ folder with the following files:

Do not modify ExampleNativeSpec.h, ExampleNativeAdvanced.h, ExampleNativeBinding.cpp, or ExampleNativeJSI.h. Codegen replaces these files on each run.

ExampleNative.uplugin
ExampleNative.Build.cs
ExampleNative.cpp
ExampleNativeJSI.h
ExampleNativeService.h
ExampleNativeService.cpp
ExampleNativeBinding.cpp
ExampleNative.h
ExampleNativeSpec.h
ExampleNativeAdvanced.h

Go ahead and explore the generated code. Look at ExampleNativeService.cpp. The file contains fail-fast implementations that obey these rules:

  • Void and synchronous methods throw FReactModuleError.
  • Typed Promise methods reject their Promise.
  • Raw-JSI Promise methods throw synchronously.

This step is also automatic and done by running @rnue/codegen.

Implement the native service

Your code will live in ExampleNativeService.cpp. The service extends and implements the generated spec bridge:

ExampleNativeService.cpp
class FExampleNativeService final : public ExampleNative::Generated::IService
{
public:
    void SetEnabled(bool bEnabled) override;
    double GetLatestValue() override;
    void LoadReadings(ReactNativeUnreal::TReactPromise<TArray<ExampleNative::Generated::FSensorReading>> Promise) override;
};

Configure the native module

Create a file named react-native.config.js to properly configure the native module and make it discoverable by @rnue/cli:

react-native.config.js
"use strict";

module.exports = {
  dependency: {
    platforms: {
      unreal: {
        pluginPath: "./unreal/ExampleNative",
        buildModules: ["ExampleNative"],
        turboModules: ["ExampleNative"],
        requiresPackages: {},
      },
    },
  },
};

Use the native service

To use your newly created native module from JavaScript, you have to export both the spec types and the spec default export.

index.ts
import ExampleNative from "../specs/NativeExampleNative";
export type { Spec } from "../specs/NativeExampleNative";

export { ExampleNative };
export default ExampleNative;

Note that ExampleNative will be null if the TurboModule is not properly initialized in Unreal Engine.

You can now import it elsewhere and call the native module:

app.ts
import ExampleNative from "example-native";

if (ExampleNative === null) {
  throw new Error("ExampleNative is not available.");
}

ExampleNative.setEnabled(true);
const latestValue = ExampleNative.getLatestValue();
const readings = await ExampleNative.loadReadings();

On this page