# Nitro Modules > A framework for building fast, type-safe native modules for React Native with statically compiled JSI bindings. This file contains all documentation content in a single document following the llmstxt.org standard. ## What is Nitro? Nitro is a framework for building powerful and fast native modules for JS. Simply put, a JS object can be implemented in C++, Swift or Kotlin instead of JS by using Nitro. While Nitro's primary environment is React Native, it also works in any other environment that uses JSI. - A [**Nitro Module**](../concepts/nitro-modules) is a library built with Nitro. It contains one or more **Hybrid Objects**. - A [**Hybrid Object**](../concepts/hybrid-objects) is a native object in Nitro, implemented in either C++, Swift or Kotlin. - [**Nitrogen**](../concepts/nitrogen) is an optional code-generator library authors can use to generate native bindings from a TypeScript interface. ```ts title="Math.nitro.ts" interface Math extends HybridObject<{ ios: 'swift' }> { readonly pi: number add(a: number, b: number): number } ``` ```swift title="HybridMath.swift" class HybridMath : HybridMathSpec { var pi: Double { return Double.pi } func add(a: Double, b: Double) -> Double { return a + b } } ``` This Hybrid Object can then be accessed directly from JS: ```ts import { NitroModules } from 'react-native-nitro-modules' const math = NitroModules.createHybridObject('Math') const result = math.add(5, 7) ``` ### Performance Nitro is all about **performance**. [This benchmark](https://github.com/mrousavy/NitroBenchmarks) compares the total execution time when calling a single native method 100.000 times: ExpoModules TurboModules NitroModules 100.000x addNumbers(...) 434.85ms 115.86ms 7.27ms 100.000x addStrings(...) 429.53ms 179.02ms 29.94ms Note: These benchmarks only compare native method throughput in extreme cases, and do not necessarily reflect real world use-cases. In a real-world app, results may vary. See [NitroBenchmarks](https://github.com/mrousavy/NitroBenchmarks) for full context. #### Lightweight layer While Nitro is built on top of JSI, the layer is very lightweight and efficient. Many things like type-checking is compile-time only, and built with C++ templates or `constexpr` which introduces zero runtime overhead. #### Direct Swift <> C++ interop Unlike Turbo- or Expo-Modules, Nitro-Modules does not use Objective-C at all. Nitro is built using the new [Swift <> C++ interop](https://www.swift.org/documentation/cxx-interop/), which is close to zero-overhead. #### Uses `jsi::NativeState` Hybrid Objects in Nitro are built on top of `jsi::NativeState`, which is more efficient than `jsi::HostObject`. Such objects have proper native prototypes, and their native memory size is known, which allows the garbage collector to properly clean up unused objects. ### Type Safety Nitro Modules are **type-safe** and **null-safe**. By using Nitro's code-generator, [nitrogen](../concepts/nitrogen), TypeScript specs are the single source of truth as generated native interfaces have to exactly represent the declared types. If a function declares a `number`, you can only implement it on the native side as a `Double`, otherwise the app will not compile. ```ts title="Math.nitro.ts" interface Math extends HybridObject<{ ios: 'swift' }> { add(a: number, b: number): number } ``` ```swift title="HybridMath.swift" class HybridMath : HybridMathSpec { func add(a: Double, b: Double) -> String { // code-error // Compile-error: Expected Double! ^ return a + b } } ``` #### Null-safety There is no way for a Nitro Module to return a type that is not expected in TypeScript, which also guarantees null-safety. ```ts interface Math extends HybridObject<{ … }> { getValue(): number getValueOrNull(): number | undefined } ``` ### Object-Oriented approach Every Hybrid Object in Nitro is a native object, which can be created, passed around, and destroyed. ```ts interface Image extends HybridObject<{ … }> { readonly width: number readonly height: number saveToFile(path: string): Promise } interface ImageEditor extends HybridObject<{ … }> { loadImage(path: string): Promise crop(image: Image, size: Size): Image } ``` Functions (or "callbacks") are also first-class citizens of Nitro, which means they can safely be kept in memory, called as often as needed, and will automatically be cleaned up when no longer needed. This is somewhat similar to how other frameworks (like Turbo-Modules) implement "events". ### Modern Languages Nitro is a modern framework, built on top of modern languages like Swift and Kotlin. It has first-class support for modern language features. #### Swift Nitro bridges to Swift directly using the new highly efficient [Swift <> C++ interop](https://www.swift.org/documentation/cxx-interop/). * **Protocols**: Every Hybrid Object's generated specification is a [Swift protocol](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols/). * **Properties**: A getter (and setter) property can be implemented using [Swift properties](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties/). * **Async**/**Await**: Asynchronous functions can use Swift's new [async/await syntax](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/) using the `Promise.async` API. * **No Objective-C**: Instead of bridging through Objective-C interfaces, Nitro bridges to Swift directly from C++. #### Kotlin Nitro bridges to Kotlin directly using [fbjni](https://github.com/facebookincubator/fbjni). * **Interfaces**: Every Hybrid Object's generated specification is a [Kotlin interface](https://kotlinlang.org/docs/interfaces.html). * **Properties**: A getter (and setter) property can be implemented using [Kotlin properties](https://kotlinlang.org/docs/properties.html). * **Coroutines**: Asynchronous functions can use Kotlin's [coroutine syntax](https://kotlinlang.org/docs/coroutines-overview.html) using the `Promise.async` API. * **No Java**: Instead of requiring Java classes, Nitro bridges to Kotlin directly. --- ## Nitro Modules A **Nitro Module** is a library built with Nitro. It may contain one or more [**Hybrid Objects**](hybrid-objects). ### Structure A Nitro Module contains the usual react-native library structure, with `ios/` and `android/` folders, a `package.json`, and a `*.podspec` file for iOS. In addition to the base react-native library template, a Nitro Module also contains: - A TypeScript setup - A `nitro.json` configuration file ### Creating a Nitro Module #### 1. Initialize the template To create a new Nitro Module, simply run `nitrogen init `: ```sh npx nitrogen@latest init react-native-math ``` #### 2. Implement your Hybrid Objects Once you set up the library, you can start implementing your Hybrid Objects! **With Nitrogen ✨** With [Nitrogen](nitrogen) you can ✨ automagically ✨ generate all native interfaces from your TypeScript definitions. After implementing the generated specs, register them using the `HybridObjectRegistry`. See [Hybrid Objects (Implementation)](hybrid-objects#implementation) for more information. **Manually** If you don't want to use Nitrogen, simply create your native C++ classes that inherit from `HybridObject`, and register them using the `HybridObjectRegistry`. See [Hybrid Objects (Implementation)](hybrid-objects#implementation) for more information. #### 3. Set up an example app After creating a Nitro Module, it's time to set up an example app to test your library! **Expo** ```sh npx create-expo-app@latest ``` **Bare RN** ```sh npx @react-native-community/cli@latest init NitroMathExample ``` :::tip The Hybrid Objects from your Nitro Module will be registered in the `HybridObjectRegistry`. This registration process needs to be called from somewhere: - In React Native, this happens in the `*Package.kt` file which calls `.initializeNative()`. - If you are not using React Native, you need to manually call `.initializeNative()` in your library's entry point. ::: --- ## Hybrid Objects A **Hybrid Object** is a native object that can be used from JS like any other object. They can have natively implemented methods, as well as properties (get + set). ```ts title="Math.nitro.ts" interface Math extends HybridObject<{ ios: 'swift' }> { readonly pi: number add(a: number, b: number): number } ``` ```swift title="HybridMath.swift" class HybridMath : HybridMathSpec { var pi: Double { return Double.pi } func add(a: Double, b: Double) -> Double { return a + b } } ``` ### Working with Hybrid Objects If a Hybrid Object is _autolinked_ (see ["**Nitrogen**: 5. Register the Hybrid Objects"](nitrogen#5-register-the-hybrid-objects)), it can be created from JS via `createHybridObject(..)`: ```ts const math = NitroModules.createHybridObject("Math") const result = math.add(5, 7) ``` To enable the usage of `new` and `instanceof`, you can use the `getHybridObjectConstructor(..)` helper method: ```ts const HybridMath = getHybridObjectConstructor("Math") const math = new HybridMath() const isMath = math instanceof HybridMath ``` A Hybrid Object can also create other Hybrid Objects: ```ts title="Image.nitro.ts" interface Image extends HybridObject<{ … }> { readonly width: number readonly height: number saveToFile(path: string): Promise } interface ImageFactory extends HybridObject<{ … }> { loadImageFromWeb(path: string): Promise loadImageFromFile(path: string): Image loadImageFromResources(name: string): Image } ``` ### Base Methods Every Hybrid Object has base methods and properties, like `name`, `toString()` and `equals(..)`: ```ts const math = NitroModules.createHybridObject("Math") const anotherMath = math console.log(math.name) // "Math" console.log(math.toString()) // "[HybridObject Math]" console.log(math.equals(anotherMath)) // true ``` #### Overriding base methods In your implementation, you can override `HybridObject`'s base methods like `toString()` or `dispose()`: **Swift** ```swift title="HybridMath.swift" class HybridMath : HybridMathSpec { func toString() -> String { return "{HybridMath}" } } ``` **Kotlin** ```kotlin title="HybridMath.kt" class HybridMath : HybridMathSpec() { override fun toString(): String { return "{HybridMath}" } } ``` **C++** ```cpp title="HybridMath.hpp" class HybridMath: public HybridMathSpec { std::string toString() override { return "{HybridMath}"; } }; ``` #### `dispose()` Additionally, every Hybrid Object has a `dispose()` method. Usually, you should not need to manually dispose Hybrid Objects as the JS garbage collector will delete any unused objects anyways. Also, most Hybrid Objects in Nitro are just statically exported singletons, in which case they should never be deleted throughout the app's lifetime. In some rare, often performance-critical- cases it is beneficial to eagerly destroy any Hybrid Objects, which is why `dispose()` exists. For example, [VisionCamera](https://github.com/margelo/react-native-vision-camera) uses `dispose()` to clean up already processed Frames to make room for new incoming Frames: ```ts const onFrameListener = (frame: Frame) => { doSomeProcessing(frame) frame.dispose() } ``` ### Implementation Hybrid Objects can be implemented in C++, Swift or Kotlin: **With Nitrogen ✨** Nitrogen will ✨ automagically ✨ generate native specifications for each Hybrid Object based on a given TypeScript definition: ```ts title="Math.nitro.ts" interface Math extends HybridObject<{ ios: 'swift', android: 'kotlin' }> { readonly pi: number add(a: number, b: number): number } ``` Running [nitrogen](nitrogen) will generate the native Swift and Kotlin protocol "`HybridMathSpec`", that now just needs to be implemented in a class: **Swift** ```swift title="HybridMath.swift" class HybridMath : HybridMathSpec { var pi: Double { return Double.pi } func add(a: Double, b: Double) throws -> Double { return a + b } } ``` **Kotlin** ```kotlin title="HybridMath.kt" class HybridMath : HybridMathSpec() { override var pi: Double get() = Double.PI override fun add(a: Double, b: Double): Double { return a + b } } ``` For more information, see the [Nitrogen documentation](nitrogen). **Manually** To implement a Hybrid Object without nitrogen, you just need to create a C++ class that inherits from the [`HybridObject`](https://github.com/margelo/nitro/blob/main/packages/react-native-nitro-modules/cpp/core/HybridObject.hpp) base class, and override `loadHybridMethods()`: ```cpp title="HybridMath.hpp" class HybridMath: public HybridObject { public: HybridMath(): HybridObject(NAME) { } public: double add(double a, double b); protected: void loadHybridMethods() override; private: static constexpr auto NAME = "Math"; }; ``` ```cpp title="HybridMath.cpp" double HybridMath::add(double a, double b) { return a + b; } void HybridMath::loadHybridMethods() { // register base methods (toString, ...) HybridObject::loadHybridMethods(); // register custom methods (add) registerHybrids(this, [](Prototype& proto) { proto.registerHybridMethod( "add", &HybridMath::add ); }); } ``` A Hybrid Object should also override `getExternalMemorySize()` to properly reflect native memory size: ```cpp class HybridMath: public HybridObject { public: // ... size_t getExternalMemorySize() override { return sizeOfSomeImageWeAllocated; } } ``` Optionally, you can also override `toString()` and `dispose()` for custom behaviour. ### Inheritance As the name suggests, Hybrid Objects are object-oriented, meaning they have full support for inheritance and abstraction. A Hybrid Object can either inherit from other Hybrid Objects, or satisfy a common interface. **With Nitrogen ✨** ### Inherit from other Hybrid Objects Each Hybrid Object has a proper JavaScript prototype chain, created automatically and lazily. When a Hybrid Object inherits from another Hybrid Object, it extends the prototype chain: ```ts interface Media extends HybridObject<{ … }> { readonly width: number readonly height: number saveToFile(): Promise } type ImageFormat = 'jpg' | 'png' interface Image extends HybridObject<{ … }>, Media { readonly format: ImageFormat } const image1 = NitroModules.createHybridObject('Image') const image2 = NitroModules.createHybridObject('Image') ``` ```mermaid graph TD; HybridObject["HybridObject (4 props)"]-->HybridMediaSpec["Media (3 props)"] HybridMediaSpec-->HybridImageSpec["Image (1 prop)"] HybridImageSpec-->Image1["const image1"] HybridImageSpec-->Image2["const image2"] ``` ### Inherit from a common interface With Nitrogen, you can define a common TypeScript interface that multiple Hybrid Objects inherit from. This non-HybridObject interface (`Media`) will not be a separate type on the native side, but all Hybrid Objects that extend from it will satisfy the TypeScript type: ```ts interface Media { readonly width: number readonly height: number } interface Image extends HybridObject<{ … }>, Media {} interface Video extends HybridObject<{ … }>, Media {} ``` **Manually** ### Inherit from other Hybrid Objects In C++, inheriting from a Hybrid Object is as simple as extending it, and adding the prototype in `loadHybridMethods()`: ```cpp title="HybridMath.hpp" class HybridImage: public HybridMedia { public: // Image specific methods ImageFormat getFormat(); void loadHybridMethods() override { // register base prototype HybridMedia::loadHybridMethods(); // register all methods we add here registerHybrids(this, [](Prototype& prototype) { prototype.registerHybridGetter("format", &HybridImage::getFormat); }); } }; ``` ### Override base methods/properties In a C++ Hybrid Object, you can also override base methods and properties, even with different types: ```cpp title="HybridMath.hpp" class HybridImage: public HybridMedia { public: // Image specific methods std::string getSize(); void loadHybridMethods() override { // register base protoype HybridMedia::loadHybridMethods(); // HybridMedia already has .width, but it's a number. // We override it to be a string in HybridImage. registerHybrids(this, [](Prototype& prototype) { prototype.registerHybridGetter("width", &HybridImage::getSize); }); } } ``` The HybridImage's prototype's prototype (= HybridMedia's prototype) will still contain the original method/property. ### Memory Size (`memorySize`) Since a HybridObject's implementation is in native code, the JavaScript runtime does not know the actual memory size of a Hybrid Object. Nitro allows Hybrid Objects to declare their memory size by overriding the `memorySize`/`getExternalMemorySize()` accessors, which can account for any external heap allocations you perform: ```swift class HybridImage : HybridImageSpec { private var cgImage: CGImage var memorySize: Int { let imageSize = cgImage.width * cgImage.height * cgImage.bytesPerPixel return imageSize } } ``` Any unused `Image` objects can now be deleted sooner by the JS garbage collector, preventing memory pressures or frequent garbage collector calls. :::tip It is safe to return `0` here, but recommended to somewhat closely estimate the actual size of native object if possible. ::: #### Raw JSI methods If for some reason Nitro's typing system is not sufficient in your case, you can also create a raw JSI method using `registerRawHybridMethod(...)` to directly work with the `jsi::Runtime` and `jsi::Value` types: ```cpp title="HybridMath.hpp" class HybridMath: HybridMathSpec { public: jsi::Value sayHello(jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* args, size_t count); void loadHybridMethods() override { // register base protoype HybridMathSpec::loadHybridMethods(); // register all methods we override here registerHybrids(this, [](Prototype& prototype) { prototype.registerRawHybridMethod("sayHello", 0, &HybridMath::sayHello); }); } } ``` --- ## Hybrid Views A **Hybrid View** is just a [**Hybrid Object**](hybrid-objects) that can also be rendered. It has one additional class-member, `view`: ```ts title="Camera.nitro.ts" export interface CameraProps extends HybridViewProps { enableFlash: boolean } export interface CameraMethods extends HybridViewMethods { } // highlight-next-line export type CameraView = HybridView ``` ```swift title="HybridCamera.swift" class HybridCamera : HybridCameraSpec { var enableFlash: Bool = false var view: UIView { get { return CameraPreviewView() } } } ``` ### Rendering Hybrid Views Unlike a **Hybrid Object**, **Hybrid Views** should not be created manually. Instead, you should use the `getHostComponent(...)` function to get a renderable version of your Hybrid View: ```ts export const Camera = getHostComponent( 'Camera', () => CameraViewConfig ) ``` This can then be rendered in React; ```tsx function App() { return } ``` Internally, the `` view will create the `HybridCamera` hybrid object - one hybrid object per view. ### Accessing the underlying Hybrid Object To access the actual underlying object, you can use the `hybridRef`: ```jsx function App() { return ( { console.log(ref.name) // <-- HybridCamera const image = ref.takePhoto() })} /> ) } ``` > Note: If you're wondering about the `callback(...)` syntax, see ["Callbacks have to be wrapped"](../guides/view-components#callbacks-have-to-be-wrapped). ### Full Guides Check out the [View Components](../guides/view-components) section for a full guide on Hybrid Views. --- ## Nitrogen **Nitrogen** is Nitro's code-generator. It parses TypeScript code using an [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree) parser to generate native interfaces from TypeScript definitions. ```ts title="Math.nitro.ts" interface Math extends HybridObject<{ ios: 'swift' }> { readonly pi: number add(a: number, b: number): number } ``` ```swift title="HybridMathSpec.swift (generated)" protocol HybridMathSpec: HybridObject { var pi: Double { get } func add(a: Double, b: Double) -> Double } ``` When `HybridMathSpec` is not implemented properly on the native side (e.g. if `add(..)` is missing, or if a type is incorrect), **the app will not compile**, which ensures full **type-safety** and **null-safety** at compile-time. ### Nitrogen is optional Nitrogen is a fully optional CLI that does some of the work for you. You can also build Nitro Modules and create Hybrid Objects without nitrogen, by just calling the `registerHybrids` method yourself. ### Who uses Nitrogen? Nitrogen should be used by library-authors, and generated specs should be committed to the repository/package. If you build an app that uses libraries built with Nitro, **you do not need to run nitrogen yourself**. ### Configuration Nitrogen should be installed as a dev-dependency in the Nitro Module (library). **npm** ```sh npm i nitrogen --save-dev ``` **yarn** ```sh yarn add nitrogen -D ``` **pnpm** ```sh pnpm add nitrogen -D ``` **bun** ```sh bun i nitrogen -d ``` Each Nitro Module needs to have a `nitro.json` configuration file. Create a `nitro.json` file in the root directory of your Nitro Module (next to `package.json`), and add the following content: ```json title="nitro.json" { "$schema": "https://nitro.margelo.com/nitro.schema.json", "cxxNamespace": ["math"], "ios": { "iosModuleName": "NitroMath" }, "android": { "androidNamespace": ["math"], "androidCxxLibName": "NitroMath" }, "autolinking": {} } ``` Tweak your module name and namespaces as needed. ### Usage Nitrogen parses all TypeScript files that end in `.nitro.ts`. #### 1. Write TypeScript specs For example, let's create `Math.nitro.ts`: ```ts title="Math.nitro.ts" import { type HybridObject } from 'react-native-nitro-modules' interface Math extends HybridObject<{ ios: 'swift', android: 'kotlin' }> { add(a: number, b: number): number } ``` #### 2. Generate native specs Now run nitrogen: **npm** ```sh npx nitrogen ``` **yarn** ```sh yarn nitrogen ``` **pnpm** ```sh pnpm nitrogen ``` **bun** ```sh bun nitrogen ``` This will always generate a shared C++ interface, and then optionall also Swift and Kotlin sub-classes. The specs go into `./nitrogen/generated/`: ``` 🔧 Loading nitro.json config... 🚀 Nitrogen runs at ~/Projects/nitro/example/dummy 🔍 Nitrogen found 1 spec in ~/Projects/nitro/example/dummy ⏳ Parsing Math.nitro.ts... ⚙️ Generating specs for HybridObject "Math"... shared: Generating C++ code... ⛓️ Setting up build configs for autolinking... 🎉 Generated 1/1 HybridObject in 0.6s! 💡 Your code is in ./nitrogen/generated ‼️ Added 8 files - you need to run `pod install`/sync gradle to update files! ``` :::important You should push the files in `./nitrogen/generated/` to git, and make sure those files are part of your npm package. This way your library will always ship a working package as a whole (including generated interfaces), and the user does not need to do anything else than to install your package. ::: #### 3. Add generated sources to your library All the generated sources (`./nitrogen/generated/`) need to be part of your library's code - so we need to add it to the iOS/Android build files. **With the Nitro template** If you created a library using the [Nitro Module template](https://github.com/margelo/nitro/tree/main/packages/template), your library already includes nitrogen's generated sources. **Manually** #### iOS On iOS, you need to call `add_nitrogen_files(...)` from your library's `.podspec`. Put this at the very end of your spec declaration: ```ruby Pod::Spec.new do |s| # ... load 'nitrogen/generated/ios/NitroExample+autolinking.rb' add_nitrogen_files(s) end ``` #### Android On Android, you first need to add the autogenerated Java/Kotlin sources to your `build.gradle`. Put this at the top of your `build.gradle`, right after any other `apply` calls: ```groovy apply from: '../nitrogen/generated/android/NitroExample+autolinking.gradle' ``` Then, add the autogenerated C++ sources to your `CMakeLists.txt`. Put this somewhere **after** `add_library(...)`: ```cmake include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroExample+autolinking.cmake) ``` :::tip Replace `NitroExample` with your Nitro Module's name as defined in your `nitro.json`. ::: #### 4. Implement the Hybrid Objects To implement `Math` now, you just need to implement the spec: **Swift** ```swift title="HybridMath.swift" class HybridMath : HybridMathSpec { func add(a: Double, b: Double) throws -> Double { return a + b } } ``` **Kotlin** ```kotlin title="HybridMath.kt" class HybridMath : HybridMathSpec() { override fun add(a: Double, b: Double): Double { return a + b } } ``` **C++** ```cpp title="HybridMath.hpp" class HybridMath: public HybridMathSpec { public: HybridMath(): HybridObject(TAG) {} public: double add(double a, double b) override; }; ``` ```cpp title="HybridMath.cpp" double HybridMath::add(double a, double b) { return a + b; } ``` #### 5. Register the Hybrid Objects Nitro needs to be able to initialize an instance of your Hybrid Object - so we need to tell it how to do that. In your `nitro.json`, register `HybridMath` in the `"autolinking"` section: **Swift/Kotlin** ```json { ... "autolinking": { "Math": { "ios": { "language": "swift", "implementationClassName": "HybridMath" }, "android": { "language": "kotlin", "implementationClassName": "HybridMath" } } } } ``` **C++** ```json { ... "autolinking": { "Math": { "all": { "language": "c++", "implementationClassName": "HybridMath" } } } } ``` Make sure `HybridMath` is default-constructible and scoped inside the correct namespace/package/file, **then run Nitrogen**. ##### 5.1. Initialize Android (C++) **With the Nitro template** If you created a library using the [Nitro Module template](https://github.com/margelo/nitro/tree/main/packages/template), your library already initializes the C++ autolinking process from your `*Package.kt` and `OnLoad.cpp` files. **Manually** In your JNI OnLoad function (`OnLoad.cpp` or `cpp-adapter.cpp`), initialize your module: ```cpp title="cpp-adapter.cpp" #include #include #include "NitroMathOnLoad.hpp" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { return facebook::jni::initialize(vm, []() { margelo::nitro::math::registerAllNatives(); }); } ``` Then, to actually load and initialize the C++ part of your library (which calls `JNI_OnLoad` from above), call `initializeNative()` from your library's entry point (`*Package.kt`): ```kotlin title="NitroMathPackage.kt" public class NitroMathPackage: BaseReactPackage() { // ... companion object { init { NitroMathOnLoad.initializeNative(); } } } ``` ##### 5.2. (Optional) ProGuard If you are using ProGuard on Android, make sure to add a `@DoNotStrip` annotation above your `HybridMath` class (and constructor) so Nitro can construct it from C++ in release builds. #### 6. Initialize the Hybrid Objects And finally, to initialize `HybridMath` from JS you just need to call `createHybridObject`: ```ts export const MathModule = NitroModules.createHybridObject("Math") const result = MathModule.add(5, 7) ``` --- ## Nitro Typing System ## Nitro's Typing System Nitro uses an extensible typing system to efficiently convert between JS and C++ types - **statically defined** and fully **type-safe** and **null-safe at compile-time**. For example, a JS `number` will always be a `double` on the native side: ```ts title="Math.nitro.ts" interface Math extends HybridObject<{ ios: 'c++' }> { add(a: number, b: number): number } ``` ```cpp title="HybridMath.hpp" class HybridMath : public HybridMathSpec { public: double add(double a, double b) override; } ``` Nitro strictly enforces **type-safety** and **null-safety** - both at compile-time and at runtime. This prevents accidentally passing a wrong type to `add(..)` (for example, a `string`) and performs null-checks to prevent passing and returning `null`/`undefined` values. On the JS side (TypeScript), type- and null-safety is enforced via TypeScript - so use it! ### Nitrogen [Nitrogen](../concepts/nitrogen) ensures that TypeScript definitions are always in sync with native type definitions. You can also use Nitro without nitrogen, in this case TypeScript definitions have to be written manually. ### Supported Types These are all the types Nitro supports out of the box: JS Type C++ Type Swift Type Kotlin Type number double / int / float Double Double boolean bool Bool Boolean string std::string String String Int64 (bigint) int64_t Int64 Long UInt64 (bigint) uint64_t UInt64 ULong T[] std::vector<T> [T] Array<T> / PrimitiveArray T? std::optional<T> T? T? null NullType NullType NullType [A, B, ...] std::tuple<A, B, ...> (A, B) 🟡  (#38) ❌ A | B | ... std::variant<A, B, ...> Variant_A_B_C Variant_A_B_C (T...) => void std::function<void (T...)> @escaping (T...) -> Void (T...) -> Unit (T...) => R std::function<std::shared_ptr<Promise<R>> (T...)> @escaping (T...) -> Promise<R> (T...) -> Promise<R> Sync<(T...) => R> std::function<R (T...)> @escaping (T...) -> R (T...) -> R Record<string, T> std::unordered_map<std::string, T> Dictionary<String, T> Map<String, T> Error std::exception_ptr Error Throwable Promise<T> std::shared_ptr<Promise<T>> Promise<T> Promise<T> AnyMap std::shared_ptr<AnyMap> AnyMap AnyMap ArrayBuffer std::shared_ptr<ArrayBuffer> ArrayBuffer ArrayBuffer Date std::chrono::system_clock::time_point Date java.time.Instant ..any HybridObject std::shared_ptr<HybridObject> HybridObject HybridObject ..any interface struct T struct T data class T ..any enum enum T enum T enum T ..any union enum T enum T enum T --- ## Minimum Requirements Nitro is a Framework built on top of newer APIs like `jsi::NativeState`. To use Nitro, make sure your app meets the minimum requirements: **iOS** - react-native 0.75 or higher - Xcode 16.4 or higher - Swift 5.9 or higher **Android** - react-native 0.75 or higher - `compileSdkVersion` 34 or higher - `ndkVersion` 27 or higher --- ## How to build a Nitro Module A [Nitro Module](../concepts/nitro-modules) is essentially just a react-native library that depends on react-native-nitro-modules and exposes one or more [Hybrid Objects](../concepts/hybrid-objects). It can either just use react-native-nitro-modules directly from C++, or use [Nitrogen](../concepts/nitrogen) to generate bindings from TypeScript to native - in this case you can even use Swift and Kotlin. This is a quick guide to build a Nitro Module from start to finish: ### 1. Create a Nitro Module First, you need to create a [Nitro Module](../concepts/nitro-modules) - either by bootstrapping a template using [nitrogen](../concepts/nitrogen), [react-native-builder-bob](https://github.com/callstack/react-native-builder-bob) or [create-nitro-module](https://github.com/patrickkabwe/create-nitro-module) - or by manually adding Nitro to your existing library/app. **nitrogen** ```sh npx nitrogen@latest init ``` **react-native-builder-bob** ```sh npx create-react-native-library@latest ``` **create-nitro-module** ```sh npx create-nitro-module@latest ``` **Manually** ### 1.1. Install Nitro and Nitrogen In your existing react-native library, install nitro and nitrogen as dev dependencies: ```sh npm install react-native-nitro-modules --save-dev npm install nitrogen --save-dev ``` Also declare `react-native-nitro-modules` as an **optional peer dependency**, so the app using your library is the one that decides which Nitro version gets installed: ```json title="package.json" { "peerDependencies": { "react-native-nitro-modules": "*" }, "peerDependenciesMeta": { "react-native-nitro-modules": { "optional": true } } } ``` :::warning The `optional` flag matters. Without it, package managers try to satisfy the peer range themselves - and since semver ranges never match pre-releases, `"*"` does **not** match a version like `0.37.0-beta.0`. A user testing a Nitro beta would then get a second, stable copy of `react-native-nitro-modules` installed next to yours, and Nitro throws [`Nitro was installed twice`](../guides/troubleshooting#nitro-was-installed-twice) at runtime. Marking the peer optional stops the package manager from installing Nitro on your behalf. ::: Then, you need to decide if you want to use Nitro's C++ library directly, or use [nitrogen](../concepts/nitrogen) to generate specs: **I will use Nitrogen later on** ### 1.2. Create a `nitro.json` file Next, create a `nitro.json` file. See [Configuration (`nitro.json`)](configuration-nitro-json) for a full guide. ### 1.3. Run nitrogen once After creating a `nitro.json` file, run nitrogen once to generate the autolinking setup: ```sh npx nitrogen ``` ### 1.4. Add nitro's generated autolinking files to your project #### iOS In your iOS `.podspec`, you need to load the `+autolinking.rb` file that was generated by nitrogen: ```ruby Pod::Spec.new do |s| // ... s.source_files = [ ... ] // diff-add load 'nitrogen/generated/ios/NitroExample+autolinking.rb' // diff-add add_nitrogen_files(s) ``` #### Android In your Android's `build.gradle`, load the `+autolinking.gradle` file at top-level (after any `apply plugin` calls) to set up the Kotlin files for autolinking: ```groovy // ... apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' // diff-add apply from: '../nitrogen/generated/android/$$androidCxxLibName$$+autolinking.gradle' ``` And also add the `+autolinking.cmake` file to your `CMakeLists.txt` to set up the C++/JNI autolinking: ```cmake add_library($$androidCxxLibName$$ SHARED ... ) // diff-add include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/$$androidCxxLibName$$+autolinking.cmake) ``` And lastly, call the C++/JNI `registerAllNatives()` function inside your library's `JNI_OnLoad(...)` entry point (often in `cpp-adapter.cpp`) within your `facebook::jni::initialize(...)` call: ```cpp #include #include "$$androidCxxLibName$$OnLoad.hpp" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { // diff-add return facebook::jni::initialize(vm, []() { // diff-add margelo::nitro::$$cxxNamespace$$::registerAllNatives(); // diff-add }); } ``` **I will not use Nitrogen** If you don't plan on using Nitrogen at all - and instead write your [Hybrid Objects](../concepts/hybrid-objects) manually using C++, you do not need to set up any autolinking files since you will be responsible for exposing your Hybrid Objects to JS. ### 2. Create Hybrid Object specs To actually use Nitro, you need to create [Hybrid Objects](../concepts/hybrid-objects) - either by using Nitro's code-generator CLI “[Nitrogen](../concepts/nitrogen)”, or by just manually extending the `HybridObject` base class in C++. **With Nitrogen ✨** ### 2.1. Write the HybridObject specs A spec is just a `*.nitro.ts` file that exports an interface, which extends `HybridObject`: **Swift/Kotlin** ```ts title="Math.nitro.ts" import { type HybridObject } from 'react-native-nitro-modules' export interface Math extends HybridObject<{ ios: 'swift', android: 'kotlin' }> { add(a: number, b: number): number } ``` **C++** ```ts title="Math.nitro.ts" import { type HybridObject } from 'react-native-nitro-modules' export interface Math extends HybridObject<{ ios: 'c++', android: 'c++' }> { add(a: number, b: number): number } ``` ### 2.2. Run nitrogen After writing specs, re-generate the generated code by running [nitrogen](../concepts/nitrogen): ```sh npx nitrogen ``` This then will generate a native specs which you can implement - in C++, that'd be `HybridMathSpec.hpp`. ### 2.3. Implement the generated native specs **Swift/Kotlin** Create a new file (e.g. `ios/HybridMath.swift`), and implement the `HybridMathSpec` protocol: ```swift title="HybridMath.swift" import Foundation import NitroModules class HybridMath : HybridMathSpec { func add(a: Double, b: Double) throws -> Double { return a + b } } ``` Create a new file (e.g. `android/.../HybridMath.kt`), and implement the `HybridMathSpec` interface: ```kotlin title="HybridMath.kt" package com.margelo.nitro.math import com.margelo.nitro.core.* class HybridMath : HybridMathSpec() { override fun add(a: Double, b: Double): Double { return a + b } } ``` **C++** Create a new header and implementation file (e.g. `cpp/HybridMath.hpp` and `cpp/HybridMath.cpp`), and implement the virtual `HybridMathSpec` class: ```cpp title="HybridMath.hpp" #include "HybridMathSpec.hpp" namespace margelo::nitro::math { class HybridMath: public HybridMathSpec { public: HybridMath(): HybridObject(TAG) {} double add(double a, double b) override; }; } ``` ```cpp title="HybridMath.cpp" #include "HybridMath.hpp" namespace margelo::nitro::math { double HybridMath::add(double a, double b) { return a + b; } } ``` **Manually** To create new [Hybrid Objects](../concepts/hybrid-objects) manually, you simply create a new C++ class that meets the following requirements: 1. It **public**-inherits from `HybridObject` 2. It calls the `HybridObject` constructor with its name 3. It overrides `loadHybridMethods()` and registers its JS-callable methods & properties ```cpp title="HybridMath.hpp" #pragma once #include namespace margelo::nitro::math { // diff-add // 1. Public-inherit from HybridObject class HybridMath : public HybridObject { public: // diff-add // 2. Call the HybridObject constructor with the name "Math" HybridMath(): HybridObject("Math") { } double add(double a, double b) { return a + b; } // diff-add // 3. Override loadHybridMethods() void loadHybridMethods() override { // register base methods (toString, ...) HybridObject::loadHybridMethods(); // register custom methods (add) registerHybrids(this, [](Prototype& proto) { proto.registerHybridMethod("add", &HybridMath::add); }); } }; } ``` ### 3. (Optional) Register Hybrid Objects Each Hybrid Object you want to be able to construct from JS has to be registered in Nitro's [`HybridObjectRegistry`](https://github.com/margelo/nitro/blob/main/packages/react-native-nitro-modules/cpp/registry/HybridObjectRegistry.hpp). If you don't want to register this Hybrid Object, you can skip this part - you will still be able to create it from another Hybrid Object's function (e.g. using the Factory-pattern). You can either use [Nitrogen](../concepts/nitrogen) to automatically generate bindings for your [Hybrid Object](../concepts/hybrid-objects)'s constructor, or manually register them using the C++ API for `HybridObjectRegistry`: **With Nitrogen ✨** In your [`nitro.json` config](configuration-nitro-json), you can connect the name of the [Hybrid Object](../concepts/hybrid-objects) (`"Math"`) with the name of the native C++/Swift/Kotlin class that you used to implement the spec (`HybridMath`) using the `autolinking` section: **Swift/Kotlin** ```json title="nitro.json" { ... "autolinking": { // diff-add "Math": { // diff-add "ios": { // diff-add "language": "swift", // diff-add "implementationClassName": "HybridMath" // diff-add }, // diff-add "android": { // diff-add "language": "kotlin", // diff-add "implementationClassName": "HybridMath" // diff-add } // diff-add } } } ``` **C++** ```json title="nitro.json" { ... "autolinking": { // diff-add "Math": { // diff-add "all": { // diff-add "language": "c++", // diff-add "implementationClassName": "HybridMath" // diff-add } // diff-add } } } ``` Now, just run [Nitrogen](../concepts/nitrogen) again to generate the native bindings: ```sh npx nitrogen ``` **Manually** To manually register a C++ class inside the `HybridObjectRegistry`, you need to call `HybridObjectRegistry::registerHybridObjectConstructor(...)` at some point before your JS code runs - e.g. at app startup: ```cpp HybridObjectRegistry::registerHybridObjectConstructor( "Math", []() -> std::shared_ptr { return std::make_shared(); } ); ``` ### 4. Use your Hybrid Objects in JS Lastly, you can initialize and use the registered Hybrid Objects from JS. This is what this will ultimately look like: ```ts import { type HybridObject, NitroModules } from 'react-native-nitro-modules' interface Math extends HybridObject<{ … }> { add(a: number, b: number): number } const math = NitroModules.createHybridObject("Math") const result = math.add(5, 7) // --> 12 ``` ### 5. Run it To test the library you just created, you now need to set up an example app for it. **nitrogen** Nitro's template does not include an example app by default, which makes it easier to be used in monorepos. To create an example app yourself - for example with [Expo](https://expo.dev) - run `create-expo-app`: ```sh npx create-expo-app@latest ``` Then, install the library in the example app - e.g. via: ``` cd example npm install ../ ``` **react-native-builder-bob** The [Builder Bob](https://github.com/callstack/react-native-builder-bob) template already includes an example app. Simply run the react-native app inside the `example/` folder. **create-nitro-module** The [create-nitro-module](https://github.com/patrickkabwe/create-nitro-module) template already includes an example app. Simply run the react-native app inside the `example/` folder. --- ## Configuration (nitro.json) ## Configuration (`nitro.json`) [Nitrogen](../concepts/nitrogen) requires a `nitro.json` file to be configured at the root of each [Nitro Module](../concepts/nitro-modules). ```json { "$schema": "https://nitro.margelo.com/nitro.schema.json", "cxxNamespace": ["$$cxxNamespace$$"], "ios": { "iosModuleName": "$$iosModuleName$$" }, "android": { "androidNamespace": ["$$androidNamespace$$"], "androidCxxLibName": "$$androidCxxLibName$$" }, "autolinking": {}, "ignorePaths": ["**/node_modules"], "gitAttributesGeneratedFlag": true } ``` Nitrogen parses this file with Zod, see [`NitroUserConfig.ts`](https://github.com/margelo/nitro/blob/main/packages/nitrogen/src/config/NitroUserConfig.ts) for more information. ### `cxxNamespace` The `cxxNamespace` is the C++ namespace that all C++ specs will be generated in. It is always relative to `margelo::nitro`, and can also have multiple sub-namespaces: ```json { "cxxNamespace": ["math", "extra"] } ``` ```cpp namespace margelo::nitro::math::extra { // ...generated classes } ``` ### `ios` Settings specifically for the iOS platform. #### `iosModuleName` The `iosModuleName` represents the name of the [clang module](https://clang.llvm.org/docs/Modules.html) that will be emitted by the Swift compiler. When this Nitro Module is a CocoaPod, this is the same thing as the `$$iosModuleName$$.podspec`'s name: ```json { "ios": { "iosModuleName": "NitroMath" } } ``` ```ruby title="NitroMath.podspec" Pod::Spec.new do |s| s.name = "NitroMath" # ... ``` ### `android` Settings specifically for the Android platform. #### `androidNamespace` The `androidNamespace` represents the package namespace in which all Java/Kotlin files are generated and written in. Similar to the `cxxNamespace`, this is always relative to `margelo.nitro`, and can also have multiple sub-namespaces. In most cases, you should keep this in sync with the `namespace` specified in your `build.gradle`. ```json { "android": { "androidNamespace": ["math", "extra"] } } ``` ```kotlin package com.margelo.nitro.test // ... ``` #### `androidCxxLibName` The `androidCxxLibName` represents the name of the native C++ library that JNI will load to connect Java/Kotlin to C++. When this Nitro Module is using CMake, this is the same thing as the library defined in `CMakeLists.txt`. Nitro will load this library at runtime using `System.loadLibrary`. ```json { "android": { "androidCxxLibName": "NitroMath" } } ``` ```cmake project(NitroMath) add_library(NitroMath SHARED src/main/cpp/cpp-adapter.cpp ../cpp/HybridMath.cpp ) ``` ### `autolinking` Contains configuration for all [Hybrid Objects](../concepts/hybrid-objects) that should be autolinked by Nitrogen. All Hybrid Objects specified here must follow these requirements: - They must be default-constructible. That means they need a public constructor that takes zero arguments. If you have a Hybrid Object that is not default-constructible (e.g. `Image` needs a `path` or `url` argument), consider creating a factory Hybrid Object that can initialize instances of your Hybrid Object internally. - C++ Hybrid Objects must be declared in a file that has the same name as the Hybrid Object (for `HybridMath`, create `HybridMath.hpp`). - C++ Hybrid Objects must be scoped in the namespace specified in [`cxxNamespace`](#cxxnamespace). - Kotlin Hybrid Objects must be inside the package namespace specified in [`androidNamespace`](#androidnamespace). - Kotlin Hybrid Objects should be annotated with `@DoNotStrip` to prevent them from being compiled out when using ProGuard. Nitrogen will then generate the following code: ```json title="nitro.json" { // ... "autolinking": { "Math": { "all": { "language": "c++", "implementationClassName": "HybridMath" } } } } ``` ```cpp title="NitroMathOnLoad.cpp (autogenerated)" // ... HybridObjectRegistry::registerHybridObjectConstructor( "Math", []() -> std::shared_ptr { return std::make_shared(); } ); ``` Here, the Hybrid Object "`Math`" is autolinked to create an instance of `HybridMath`, a C++ class. Instead of `cpp`, you can also use `swift` or `kotlin`. ### `ignorePaths` Configures the TypeScript parser to ignore specific given paths when looking for `*.nitro.ts` specs. By default, this is empty (`[]`), but it can be set to ignore paths like `["node_modules", "lib"]`. ### `gitAttributesGeneratedFlag` Configures whether all nitro-generated files are marked as [`linguist-generated`](https://docs.github.com/en/repositories/working-with-files/managing-files/customizing-how-changed-files-appear-on-github) for GitHub. This disables diffing for generated content and excludes them from language statistics. This is controlled via `nitrogen/generated/.gitattributes`. --- ## Android `Context` ## Android `Context` Many Android APIs require a [`Context`](https://developer.android.com/reference/android/content/Context) object, which allows access to application-specific resources and classes, as well as calls to hardware APIs. ### Using `Context` in a `HybridObject` Unlike in TurboModules, a [Hybrid Object](../concepts/hybrid-objects) does not receive a [`Context`](https://developer.android.com/reference/android/content/Context) via its constructor, as that would make it non-portable. Instead, Nitro exposes the current `ReactApplicationContext` via the static [`NitroModules.applicationContext`](https://github.com/margelo/nitro/blob/fb1102ca7657665aad3011d7556fcd06f3cc796d/packages/react-native-nitro-modules/android/src/main/java/com/margelo/nitro/NitroModules.kt#L68-L72) getter, which you can access in your [Hybrid Object](../concepts/hybrid-objects) if needed: ```kotlin class HybridClipboard: HybridClipboardSpec() { private val clipboard: ClipboardManager init { // highlight-next-line val context = NitroModules.applicationContext ?: throw Error("No Context available!") clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager } } ``` ### Using `Context` in a `HybridView` Unlike in a [Hybrid Object](../concepts/hybrid-objects), a [Hybrid View](../concepts/hybrid-views) is platform-specific and always needs a [`Context`](https://developer.android.com/reference/android/content/Context) - e.g. to create Android `View`s. Therefore all `HybridView`s receive [`Context`](https://developer.android.com/reference/android/content/Context) as a constructor argument: ```kotlin class HybridCameraView( // highlight-next-line private val context: ThemedReactContext ): HybridCameraViewSpec() { override val view: View = View(context) } ``` --- ## Entry Point Nitro is built on top of JSI - while the primary target is React Native, Nitro even works on any other target that provides JSI. ### React Native's Entry Point In React Native apps, Nitro makes use of the [autolinking functionality](https://github.com/react-native-community/cli/blob/main/docs/autolinking.md) provided by RN CLI. Nitro itself is a react-native library - either a Native Module (old arch) or a Turbo Module (new arch) - with a single native method: `install()`. This native method will be called from JS when importing `react-native-nitro-modules`, so it will always happen before trying to use the `NitroModules` JS object. ### Manually registering Nitro If you are not within a typical React Native environment (e.g. a brownfield app, an out-of-tree platform, or simply a pure JSI environment), you can also use Nitro Modules by just installing Nitro manually. After creating your `jsi::Runtime` and an instance of `Dispatcher`, simply call `margelo::nitro::install()`: ```cpp #include #include #include jsi::Runtime& runtime = ... std::shared_ptr dispatcher = ... margelo::nitro::install(runtime, dispatcher); ``` Your `Dispatcher` implementation must properly implement `runAsync` and `runSync` to schedule calls on a Thread that can safely access the `jsi::Runtime`. :::tip If your Runtime can be accessed from any Thread, you can also skip Thread-hops here and just call the functions directly in your Dispatcher. ::: #### No Dispatcher Nitro can also be installed without a `Dispatcher`: ```cpp #include #include jsi::Runtime& runtime = ... margelo::nitro::install(runtime); ``` In this case, all synchronous methods and properties remain in-tact, but any asynchronous hybrid methods (`Promise`) or callbacks will throw an error that Nitro does not have a `Dispatcher` to get back to the JS Thread. --- ## Errors Every method in a [Hybrid Object](../concepts/hybrid-objects) can throw an error using the language-default error throwing feature: **Swift** ```swift title="HybridMath.swift" class HybridMath : HybridMathSpec { func add(a: Double, b: Double) throws -> Double { if a < 0 || b < 0 { throw RuntimeError.error(withMessage: "Value cannot be negative!") } return a + b } } ``` **Kotlin** ```kotlin title="HybridMath.kt" class HybridMath : HybridMathSpec() { override fun add(a: Double, b: Double): Double { if (a < 0 || b < 0) { throw Error("Value cannot be negative!") } return a + b } } ``` **C++** ```cpp title="HybridMath.hpp" class HybridMath: public HybridMathSpec { double add(double a, double b) override { if (a < 0 || b < 0) { throw std::runtime_error("Value cannot be negative!"); } return a + b; } }; ``` Errors will be propagated upwards to JS and can be caught just like any other kind of error using `try`/`catch`: ```diff // code-error `Math.add(...)`: Value cannot be negative! ``` #### Promise rejections Promises can also be rejected using error throwing syntax on the native side: **Swift** ```swift title="HybridMath.swift" class HybridMath : HybridMathSpec { func add(a: Double, b: Double) throws -> Promise { return Promise.async { if a < 0 || b < 0 { throw RuntimeError.error(withMessage: "Value cannot be negative!") } return a + b } } } ``` **Kotlin** ```kotlin title="HybridMath.kt" class HybridMath : HybridMathSpec() { override fun add(a: Double, b: Double): Promise { return Promise.async { if (a < 0 || b < 0) { throw Error("Value cannot be negative!") } return@async a + b } } } ``` **C++** ```cpp title="HybridMath.hpp" class HybridMath: public HybridMathSpec { std::shared_ptr> add(double a, double b) override { return Promise::async([=]() -> double { if (a < 0 || b < 0) { throw std::runtime_error("Value cannot be negative!"); } return a + b; }); } }; ``` Promise rejections are handled as usual using the `.catch`, or `await`/`catch` syntax in JS: ```ts const math = // ... try { await math.add(-5, -1) } catch (error) { console.log(error) } ``` --- ## Performance Tips While Nitro is already insanely fast, there are some things you can do that affect the performance of your library. Here are some tips to make your library even faster. ### Avoid dynamic types Any dynamic types require runtime type checking, and cannot be optimized as good as statically known types (compile-time). #### Untyped Maps An [untyped map](../types/untyped-maps) (`AnyMap`) is not only untyped, but also in-efficient. If you can, avoid untyped maps: ```ts title="Bad ❌" interface BadDatabase extends HybridObject<{ … }> { getUser(): AnyMap } ``` ```ts title="Good ✅" interface User { name: string age: number } interface GoodDatabase extends HybridObject<{ … }> { getUser(): User } ``` In some cases (e.g. network requests) you do not know the shape of your data, like in a JSON web-request. In this case, it might make sense to use `ArrayBuffer` or `string`, and parse the data on the JS side using `JSON.parse` - benchmark your code (before vs after) to see if this optimization makes sense for you. #### Variants [Variants](../types/variants) (`A | B`) are dynamic types. Each time you pass a variant to native, Nitro has to check its type at runtime - is it `A` or `B`? Those type-checks are very efficient so this is considered a micro-optimization, but if you can, avoid variants like so: ```ts title="Bad ❌" interface BadDatabase extends HybridObject<{ … }> { set(value: number | string): void } ``` ```ts title="Good ✅" interface GoodDatabase extends HybridObject<{ … }> { setNumber(value: number): void setString(value: string): void } ``` ### Avoid unnecessary objects It is a common pattern to wrap everything in an object in JavaScript. In Nitro, every object gets its own struct and has to be allocated. On iOS this performance impact is almost zero, but on Android the struct is a heap-allocation. If you can, avoid unnecessarily wrapping everything in objects, and flatten the types out in the function signature: ```ts title="Bad ❌" interface SetPayload { key: string value: string onCompleted: () => void } interface BadDatabase extends HybridObject<{ … }> { set(payload: SetPayload): void } ``` ```ts title="Good ✅" interface GoodDatabase extends HybridObject<{ … }> { set(key: string, value: string, onCompleted: () => void): void } ``` ### Use Threading/Asynchronous Promises By default, every function in Nitro is fully synchronous. If your function takes long to execute, the JS Thread can not do any other work in the meantime. In such cases, mark your function asynchronous by returning a [`Promise`](../types/promises), which you can then use to run the heavy processing code on a different thread: ```ts title="Bad ❌" interface BadDatabase extends HybridObject<{ … }> { writeLargeData(data: string): void } ``` ```ts title="Good ✅" interface GoodDatabase extends HybridObject<{ … }> { writeLargeData(data: string): Promise } ``` Keep in mind that switching to a different Thread on the native side introduces a small overhead by itself. This only benefits performance if the actual computation inside the function body takes longer than the thread-switch. ### Use `ArrayBuffer` for large data For large data sets, conventional [arrays](../types/arrays) are in-efficient as each value has to be copied individually. In contrast to conventional arrays, [Array Buffers](../types/array-buffers) are zero-copy, meaning native memory can be directly shared to JS without copying the data. For example, to return a large list of numbers we could use [array buffers](../types/array-buffers) (`Float64Array`) instead of [arrays](../types/arrays): ```ts title="Bad ❌" interface BadDatabase extends HybridObject<{ … }> { getAsBlob(): number[] } ``` ```ts title="Good ✅" interface GoodDatabase extends HybridObject<{ … }> { getAsBlob(): ArrayBuffer } ``` ### Use Hybrid Objects to implement proxy-results If a function returns a large amount of data to JS, but only a sub-set of that data is used, we can implement it as a [Hybrid Object](../types/hybrid-objects) instead of a [struct](../types/custom-structs). This way data will be accessed lazily, and all the data that the user does not access will never be converted to JS, which means Nitro has to do less work: ```ts title="Bad ❌" interface AllData { rows: DataRow[] } interface BadDatabase extends HybridObject<{ … }> { getAllData(): AllData } const database = // ... const data = database.getAllData() const row = data.rows .find((r) => r.name === "Marc") ``` ```ts title="Good ✅" interface AllData extends HybridObject<{ … }> { findRowWithName(name: string): DataRow } interface GoodDatabase extends HybridObject<{ … }> { getAllData(): AllData } const database = // ... const data = database.getAllData() const row = data.findRowWithName("Marc") ``` The **Bad** example is significantly slower than **Good**, because Nitro has to convert **all rows** to JS, and there could be thousands of rows - even if we only use the row with the `name` "Marc". The **Good** example is significantly faster than **Bad** because the result of `getAllData()` is a [Hybrid Object](../types/hybrid-objects), and all the thousands of rows do not have to be converted to JS at all, instead they are simply held in native memory. The function `findRowWithName(...)` iterates through the list on the native side and finds the matching row - only this single row will then have to be converted to JS. ### Properly use `memorySize` Since Hybrid Objects are implemented in native code, the JS runtime does not know the memory size of such objects. To let the JavaScript runtime know about a Hybrid Object's actual size in memory, Nitro exposes a `memorySize` (or `getExternalMemoryPressure()`) API which you can use to give a rough estimation on the native object's memory size (including any heap allocations you perform): ```swift class HybridImage : HybridImageSpec { private var cgImage: CGImage var memorySize: Int { let imageSize = cgImage.width * cgImage.height * cgImage.bytesPerPixel return imageSize } } ``` That way the JS garbage collector knows how big an `Image` is exactly in memory, and can delete any unused `Image` objects sooner to free up the native memory (`cgImage`), potentially avoiding memory warnings or garbage collector panics. ### Implement view recycling (`RecyclableView`) If a view is rendered multiple times throughout an app's lifetime, Fabric can recycle old views and re-use them to display new views. This requires you to reset your internal state to its default values, otherwise stale views may show up, so recycling is disabled by default. To enable recycling, implement the `RecyclableView` interface/protocol, and override the `prepareForRecycle()` method: **Swift** ```swift title="HybridMyView.swift" import NitroModules class HybridMyView: HybridMyViewSpec, RecyclableView { // ... func prepareForRecycle() {} } ``` **Kotlin** ```kotlin title="HybridMyView.kt" import com.margelo.nitro.views.RecyclableView class HybridMyView: HybridMyViewSpec(), RecyclableView { // ... override fun prepareForRecycle() {} } ``` See [View Components: Recycling](view-components#recycling) for more information. ### Avoid too many native calls While Nitro is insanely fast, there is still an unavoidable overhead associated with calling native code from JS. In general, it is a good practice to stay within one environment (here; JavaScript) as long as possible, and only call into native when really needed. Some things (like the `Math.add(...)` function I often use) are faster in JavaScript, as the overhead of calling into native might be greater than the overall execution time of the function. ### Use C++ if possible If possible, write Nitro Modules in C++. This is faster as bridging to Swift or Kotlin is not required. ### Avoid large arrays Large arrays have to be deep-copied, as they are immutable memory in JS. If possible, avoid sending large arrays back and forth - this is the same principle as ["Use Hybrid Objects to implement proxy-results"](#use-hybrid-objects-to-implement-proxy-results). --- ## Running the Example app This guide will help you understand the [Nitro Example app](https://github.com/margelo/nitro/tree/main/example)'s role, and teach you how to run it both in debug and in release. ### The Example app's purpose Nitro's development always targets the example app (under `apps/example/`). It narrows down user bugs by the following criteria; - If you have a bug in your code, but the same code works in the Nitro example app, it's a **user error** and won't/can't be fixed. - If you have a bug in your code that can also be reproduced inside the Nitro example app, it's a **Nitro bug**, which might get fixed in a future PR. A bug report will only be taken seriously when there is a **clear reproduction**, and you can also reproduce the same bug in the example app. Anything else is considered a **user error** and **will be closed**. The example app contains 4 screens; 1. A **"Tests"** screen where over 170 different test cases for Nitro Modules are covered. 2. A **"Benchmarks"** screen where a simple Nitro Module is benchmarked against a Turbo Module. 3. A **"Views"** screen where hundreds of Nitro Views mount and unmount periodically. 4. A **"Eval"** screen where you can run custom code using Nitro APIs. These should allow you to test all Nitro features in a single app, which is useful for finding bugs or regressions - e.g. by running specific code and comparing results with older versions of the example app. ### Run the pre-built example app (release) Each release of Nitro contains a pre-built release version of the [Nitro Example app](https://github.com/margelo/nitro/tree/main/example). You can download the pre-built `.app`/`.apk` from [the latest Nitro release](https://github.com/margelo/nitro/releases/latest), and run it on your iOS Simulator, or Android Simulator/Device. #### iOS ##### Simulator To run the iOS app, first start an iOS Simulator; ```sh open -a Simulator ``` Then, simply drag the `.app` into the Simulator's window. #### Android ##### Emulator To run the Android app, start an Android Emulator; ```sh emulator -list-avds emulator -avd ``` Then, simply drag the `.apk` into the Emulator's window. ##### Device To run the `.apk` on a physical Android device, simply download the `.apk` from the GitHub releases on your phone, and open it. Your OS will install & run it. ### Build the example app yourself (debug) To run the example app yourself, make sure you have a [working React Native environment](https://reactnative.dev/docs/environment-setup) set up, and have installed [Bun](https://bun.com). Then, simply clone (potentially also fork?) the Nitro repository and install its dependencies: **macOS** ```sh git clone https://github.com/margelo/nitro cd nitro bun bootstrap ``` #### iOS To run the iOS app of Nitro Example, open `apps/example/ios/NitroExample.xcworkspace` in Xcode, and hit run. The `bun bootstrap` command should have ensured that Pods are installed. #### Android To run the Android app of Nitro Example, first open Android Studio: ```sh open -a "Android Studio" ``` Then, open `apps/example/android/` in Android Studio. **Windows/Linux** ```sh git clone https://github.com/margelo/nitro cd nitro bun i bun run build ``` #### Android To run the Android app of Nitro Example, first open Android Studio: ```sh open -a "Android Studio" ``` Then, open `apps/example/android/` in Android Studio. #### Reproducing bugs In addition to `packages/nitrogen` ([nitrogen](../concepts/nitrogen) CLI) and `packages/react-native-nitro-modules` ([Nitro Modules](../concepts/nitro-modules) core), the nitro repository also contains two testing libraries; - `packages/react-native-nitro-test`: Contains a lot of tests for C++/Swift/Kotlin Hybrid Objects, functions and types. - `packages/react-native-nitro-test-external`: Contains a Hybrid Object that is used in `react-native-nitro-test` to test cross-library-imports. **You can use those testing libraries to reproduce any bugs you might encounter with Nitro** - if you manage to reproduce your bug, simply open a **draft** PR against Nitro with the necessary changes so I can see what makes it break. Based on that, I have clear runnable reproduction, and can fix bugs faster. ##### Changing specs Every time you change a Nitro spec in one of those two test libraries, you can simply re-generate specs in the root folder; ```sh bun specs ``` This runs [nitrogen](../concepts/nitrogen) for both test libraries. You might need to reinstall pods if any iOS files changed; ```sh bun example pods ``` Then simply run again. --- ## Sync vs Async By default, every method on a [Hybrid Object](../concepts/hybrid-objects) is synchronous and runs on the JS Thread. This means, as long as your native method is executing, the JS Thread is blocked and can not do any other work (like state updates or view changes). ### Light Methods For small/light methods this is great because you can return results to the caller (JS) immediately without having to await a Promise. For example, [react-native-mmkv](https://github.com/margelo/react-native-mmkv) allows you to _get_ values synchronously: ```ts function App() { const mmkv = new MMKV() const name = mmkv.getString('username') // --> Marc } ``` If it would be async, it would be quite cumbersome to use in some contexts: ```ts function App() { const mmkv = new MMKV() const [name, setName] = useState(undefined) useEffect(() => { (async () => { const n = await mmkv.getString('username') // --> Marc setName(n) })() }, []) } ``` ### Heavy Methods For larger/heavy methods that take a while to execute this can be problematic, because the JS Thread will be blocked for a longer duration then. To free up the JS Thread while the long-running method is executing, you can make it **asynchronous** by just returning a `Promise`: ```ts title="MinerSpec.nitro.ts" interface Miner extends HybridObject<{ … }> { mineOneBitcoin(): Promise } ``` On the native side you still start out with a synchronous method, but you can return a Promise: **Swift** ```swift title="HybridMiner.swift" func mineOneBitcoin() throws -> Promise { // 1. synchronous in here, JS Thread is still blocked // useful e.g. for argument checking before starting async Thread return Promise.async { // 2. asynchronous in here, JS Thread is now free return computeBitcoin() } } ``` **Kotlin** ```kotlin title="HybridMiner.kt" override fun mineOneBitcoin(): Promise { // 1. synchronous in here, JS Thread is still blocked // useful e.g. for argument checking before starting async Thread return Promise.async { // 2. asynchronous in here, JS Thread is now free return computeBitcoin() } } ``` **C++** ```cpp title="HybridMiner.hpp" std::shared_ptr> mineOneBitcoin() override { // 1. synchronous in here, JS Thread is still blocked // useful e.g. for argument checking before starting async Thread return Promise::async([]() { // 2. asynchronous in here, JS Thread is now free return computeBitcoin(); }); } ``` ### When should it be async? It's up to you to decide when to make a native method asynchronous. Benchmark the total execution time of your method - a good rule of thumb is: _if it takes longer than 50ms, make it asynchronous_. ### Why isn't everything async? There's two reasons: 1. Jumping from one thread (JS) to another (background) introduces a tiny overhead. Sometimes the total execution time of the method itself is smaller than just the Thread-jump, so it would make more sense to just keep it synchronous right away. 2. For small/fast methods it's more ergonomic to keep them synchronous as the caller receives the return value right away - no awaiting or Promises. --- ## Troubleshooting This guide helps you troubleshoot issues in Nitro and should give you enough context to open a well-formed issue, even if you're not a native developer. ### Minimum requirements First, make sure you meet the [minimum requirements](../getting-started/minimum-requirements) for Nitro. ### Ask for help If you want to ask for help, join our [Margelo Community Discord](https://margelo.com/discord). Be respectful of everyone's time. ### Run the example app Each release of Nitro contains a pre-built release version of the [Nitro Example app](https://github.com/margelo/nitro/tree/main/example). You can run this by following the ["Running the Example app" guide](running-example-app). ### Build error If your app fails to build after installing Nitro or a library powered by Nitro, make sure to post full build logs: **iOS** 1. Build the app with Xcode. 2. When the build fails in Xcode, open the "Report Navigator" tab from within the left sidebar: 3. Then, find the most recent build attempt and click on "Build": 4. Scroll through the build report and find the step(s) that failed to build. They usually have a ❌ icon on the left. Click on the parent item's hamburger menu on the right to open the full logs: 5. Scroll down through the build logs (the long part is just the command invocation) to find the actual error messages: 6. Copy those bottom logs only (not the build command invocation above) and create a GitHub issue with that. **Android** 1. Build the app with Android Studio. 2. When the build fails in Android Studio, open the "Build" tab from within the bottom left sidebar: 3. Find the top-most entry in the Build window (which contains full unfiltered logs) and click it: 4. Copy those full logs and paste them in the GitHub issue (or serve via pastebin). Make sure they actually contain the **error** message and not just something like "BUILD FAILED in 7s" (which is what most people post): ### Nitro was installed twice If your app crashes at startup with: ``` Nitro was installed twice: once with native version X and once with JS version Y. ``` ...then two different copies of `react-native-nitro-modules` ended up in your project - the native build linked one, and Metro bundled the other. The most common cause is **installing a Nitro pre-release** (e.g. `0.37.0-beta.0`). Semver ranges never match pre-release versions, so a library declaring `"react-native-nitro-modules": "*"` as a required peer dependency does not consider the beta a match - and your package manager silently installs a second, stable copy inside that library's `node_modules` to satisfy the peer. To fix it: 1. Check how many copies you have: ```sh npm ls react-native-nitro-modules ``` 2. If a library nests its own copy, ask the library author to mark the peer dependency as [optional](../getting-started/how-to-build-a-nitro-module#11-install-nitro-and-nitrogen). 3. As a workaround until then, force a single version from your app's `package.json`: ```json title="package.json" { "overrides": { "react-native-nitro-modules": "0.37.0-beta.0" } } ``` (use `resolutions` instead of `overrides` for Yarn) 4. Delete `node_modules` and reinstall - package managers often leave the stale nested copy on disk even after the lockfile is fixed. ### Runtime error If your app crashes at runtime, make sure to inspect the native logs. **iOS** 1. Run your app through Xcode 2. If the app hits an unhandled error, it should pause in Xcode. Share the line it stopped in, and also the call-stack (stacktrace) on the left side. 3. If the app didn't pause, then it might have been a handled error - in this case just check the Xcode logs at the bottom: **Android** 1. Run your app through Android Studio by using the Debug button (🪲) 2. If the app hits an unhandled error, it should pause in Android Studio. Share the line it stopped in, and also the call-stack (stacktrace) on the bottom window. 3. If the app didn't pause, then it might have been a handled error - in this case just check the Android Logcat logs at the bottom: --- ## View Components Nitro provides first-class support for creating React Native Views. Such views can be rendered within React Native apps using [Fabric](https://reactnative.dev/architecture/fabric-renderer), and are backed by a C++ ShadowNode. The key difference to a Fabric view is that it uses Nitro for prop parsing, which is more lightweight, performant and flexible. :::note Nitro Views require **react-native 0.78.0** or higher, and require the new architecture. ::: ### Create a Nitro View #### 1. Declaration To create a new Nitro View, declare its props and methods in a `*.nitro.ts` file, and create a type that specializes `HybridView` - here `CameraView`: ```ts title="Camera.nitro.ts" import type { HybridView, HybridViewProps, HybridViewMethods } from 'react-native-nitro-modules' export interface CameraProps extends HybridViewProps { enableFlash: boolean } export interface CameraMethods extends HybridViewMethods { } // highlight-next-line export type CameraView = HybridView ``` #### 2. Code Generation Then, run [nitrogen](../concepts/nitrogen): **npm** ```sh npx nitrogen ``` **yarn** ```sh yarn nitrogen ``` **pnpm** ```sh pnpm nitrogen ``` **bun** ```sh bun nitrogen ``` This will create a C++ ShadowNode, with an iOS (Swift) and Android (Kotlin) interface, just like any other [Hybrid Object](../concepts/hybrid-objects). Additionally, a view config (`CameraViewConfig.json`) will be generated - this is required by Fabric. #### 3. Implementation Now it's time to implement the View - simply create a new Swift/Kotlin class/file, extend from `HybridCameraViewSpec` and implement your `.enableFlash` property, as well as the common `.view` accessor: **Swift** ```swift title="HybridCameraView.swift" class HybridCameraView : HybridCameraViewSpec { // Props var enableFlash: Bool = false // View var view: UIView = UIView() } ``` **Kotlin** ```kotlin title="HybridCameraView.kt" class HybridCameraView(val context: ThemedReactContext) : HybridCameraViewSpec() { // Props override var enableFlash: Boolean = false // View override val view: View = View(context) } ``` #### 4. Autolink Just like any other Hybrid Object, add the Hybrid View to your `nitro.json`'s autolinking configuration: ```json title="nitro.json" { // ... "autolinking": { "CameraView": { "ios": { "language": "swift", "implementationClassName": "HybridCameraView" }, "android": { "language": "kotlin", "implementationClassName": "HybridCameraView" } } } } ``` Now run nitrogen again. ##### 4.1. Android: Register the View Manager On Android, you need to register the generated view manager in your React Native package: ```kotlin title="CameraPackage.kt" // ... public class CameraPackage: BaseReactPackage() { // ... override fun createViewManagers(reactContext: ReactApplicationContext): List> { val viewManagers = ArrayList>() // diff-add viewManagers.add(HybridCameraViewManager()) return viewManagers } } ``` #### 5. Initialization Then, to use the view in JavaScript, use `getHostComponent(..)`: ```ts import { getHostComponent } from 'react-native-nitro-modules' import CameraViewConfig from '../nitrogen/generated/shared/json/CameraViewConfig.json' export const Camera = getHostComponent( 'Camera', () => CameraViewConfig ) ``` #### 6. Rendering And finally, render it: ```jsx function App() { return } ``` ### Props Since every `HybridView` is also a `HybridObject`, you can use any type that Nitro supports as a property - including custom types (`interface`), `ArrayBuffer`, and even other `HybridObject`s! For example, a custom `` component can be used to render custom `Image` types: ```ts title="Image.nitro.ts" export interface Image extends HybridObject<{ ios: 'swift' }> { readonly width: number readonly height: number save(): Promise } ``` ```ts title="ImageView.nitro.ts" import { type Image } from './Image.nitro.ts' export interface ImageProps extends HybridViewProps { image: Image } export type ImageView = HybridView ``` Then; ```jsx function App() { const image = await loadImage('https://...') return } ``` #### Threading Since Nitro bridges props directly to JS, you are responsible for ensuring thread-safety. - If props are set normally via React, they will be set on the UI Thread. - If the user sets props on the view `hybridRef` (e.g. also if the `HybridView` is passed to a `HybridObject` in native), props _could_ be set on a different Thread, like the JS Thread. #### Before/After update To batch prop changes, you can override `beforeUpdate()` and `afterUpdate()` in your views: **Swift** ```swift title="HybridCameraView.swift" class HybridCameraView: HybridCameraViewSpec { // View var view: UIView = UIView() func beforeUpdate() { } func afterUpdate() { } } ``` **Kotlin** ```kotlin title="HybridCameraView.kt" class HybridCameraView(context: ThemedReactContext): HybridCameraViewSpec() { // View override val view: View = View(context) override fun beforeUpdate() { } override fun afterUpdate() { } } ``` #### Callbacks have to be wrapped Whereas Nitro allows passing JS functions to native code directly, React Native core doesn't allow that. Instead, functions are wrapped in an event listener registry, and a simple boolean is passed to the native side. Unfortunately React Native's renderer does not yet allow changing this behaviour, so functions cannot be passed directly to Nitro Views. As a workaround, Nitro requires you to wrap each function in an object, which bypasses React Native's conversion. To simplify this, Nitro exposes the `callback(...)` method: ```tsx export interface CameraProps extends HybridViewProps { onCaptured: (image: Image) => void } export type CameraView = HybridView function App() { // diff-remove return console.log(i)} /> // diff-add return console.log(i))} /> } ``` :::info We are working on a fix here: [facebook/react #32119](https://github.com/facebook/react/pull/32119) ::: #### Recycling For improved performance and lower memory footprint, Nitro Views can be _recycled_. To allow your view to be recycled, implement the `RecyclableView` interface/protocol from Nitro: **Swift** ```swift title="HybridMyView.swift" import NitroModules class HybridMyView: HybridMyViewSpec, RecyclableView { // ... func prepareForRecycle() {} } ``` **Kotlin** ```kotlin title="HybridMyView.kt" import com.margelo.nitro.views.RecyclableView class HybridMyView: HybridMyViewSpec(), RecyclableView { // ... override fun prepareForRecycle() {} } ``` When Fabric decides to re-use a previously created view, the `prepareForRecycle()` method will be called. Inside that method you should reset any internal state to its default values. For example, an asynchronous Image component should reset its displayed image when it is being recycled, otherwise it would display an old image while the new one is still loading: ```swift class HybridImageView: HybridImageViewSpec, RecyclableView { var view: UIView { imageView } private var imageView = UIImageView() func prepareForRecycle() { // highlight-next-line imageView.image = nil } } ``` ### Methods Since every `HybridView` is also a `HybridObject`, methods can be directly called on the object. Assuming our `` component has a `takePhoto()` function like so: ```ts export interface CameraProps extends HybridViewProps { ... } export interface CameraMethods extends HybridViewMethods { takePhoto(): Promise } export type CameraView = HybridView ``` To call the function, you would need to get a reference to the `HybridObject` first using `hybridRef`: ```jsx function App() { return ( { const image = ref.takePhoto() })} /> ) } ``` > Note: If you're wondering about the `callback(...)` syntax, see ["Callbacks have to be wrapped"](#callbacks-have-to-be-wrapped). The `ref` from within `hybridRef`'s callback is pointing to the `HybridObject` directly - you can also pass this around freely. --- ## Worklets/Threading Nitro itself is fully runtime-agnostic, which means every [Hybrid Object](../concepts/hybrid-objects) can be used from any JS Runtime or Worklet Context. This allows the caller to call into native Nitro Modules from libraries like [react-native-worklets-core](https://github.com/margelo/react-native-worklets-core), or [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated). You can use a Nitro [Hybrid Object](../concepts/hybrid-objects) on the default React JS context, on the UI context, or on any other background worklet context. **react-native-worklets (Reanimated)** ```ts const math = NitroModules.createHybridObject('Math') runOnUI(() => { 'worklet' const result = math.add(5, 3) console.log(result) // --> 8 })() ``` **react-native-worklets-core (Margelo)** ```ts const math = NitroModules.createHybridObject('Math') const boxed = NitroModules.box(math) const context = Worklets.createContext('DummyContext') context.runAsync(() => { 'worklet' const unboxed = boxed.unbox() const result = unboxed.add(5, 3) console.log(result) // --> 8 }) ``` ### Dispatcher All synchronous APIs of Nitro work ✨ automagically ✨ on any runtime, but asynchronous APIs (Promises and callbacks) require a `Dispatcher`. If you call an asynchronous API on a runtime that Nitro doesn't know, it likely doesn't have a `Dispatcher`, so it doesn't know how to call back to the JS Thread after the asynchronous operation has finished (Promise resolve or callback call). If **you** created that `jsi::Runtime`, you need to create a `Dispatcher` for it and implement `runSync` and `runAsync`: ```cpp #include using namespace margelo::nitro; class MyRuntimeDispatcher: public Dispatcher { public: void runSync(std::function&& function) override; void runAsync(std::function&& function) override; }; ``` Then, simply install this `Dispatcher` into your runtime so Nitro can use it: ```cpp auto myDispatcher = std::make_shared(); Dispatcher::installRuntimeGlobalDispatcher(myRuntime, myDispatcher); ``` This needs to be done once, ideally immediately after creating the `jsi::Runtime`. Your `runSync` and `runAsync` implementations must run the given `function` on the same Thread that the `jsi::Runtime` was created on - see [`CallInvokerDispatcher.hpp`](https://github.com/margelo/nitro/blob/main/packages/react-native-nitro-modules/cpp/threading/CallInvokerDispatcher.hpp) for an example. ### Boxing A [Hybrid Object](../concepts/hybrid-objects) is a JS object with `jsi::NativeState` and a prototype chain. If you need to interop with legacy APIs or APIs that can't deal with `jsi::NativeState` yet, you can _box_ the Hybrid Object into a `jsi::HostObject`: ```ts const math = NitroModules.createHybridObject('Math') const boxed = NitroModules.box(math) // <-- jsi::HostObject ``` The `boxed` object is a simple `jsi::HostObject` (see [`BoxedHybridObject.hpp`](https://github.com/margelo/nitro/blob/main/packages/react-native-nitro-modules/cpp/core/BoxedHybridObject.hpp)), which can later be _unboxed_ again: ```ts const unboxed = boxed.unbox() // <-- Math const result = unboxed.add(5, 3) // <-- 8 ``` :::info This is how Hybrid Objects are captured inside Worklet Contexts under the hood as well! ::: --- ## Apps using Nitro Nitro is not just a benchmark project. It is already shipping in large-scale production apps across finance, commerce, social, developer tools, food, connectivity, sports, health, music, and productivity. --- ## Awesome Nitro Modules Libraries built with React Native Nitro Modules. A curated collection of community-built Nitro Modules, including production React Native libraries powered by Nitro, Nitrogen, and type-safe JSI bindings. > **Want to add your module?** PRs are welcome! ### Published Modules #### Audio & Media | Module | Description | Links | |--------|-------------|-------| | **react-native-nitro-audio-manager** | Audio session and routing management | [GitHub](https://github.com/ChristopherGabba/react-native-nitro-audio-manager) | | **react-native-video** | Video playback component | [GitHub](https://github.com/TheWidlarzGroup/react-native-video) | | **react-native-nitro-screen-recorder** | Screen recording capabilities | [GitHub](https://github.com/ChristopherGabba/react-native-nitro-screen-recorder) | | **react-native-nitro-image** | Superfast in-memory Image type | [GitHub](https://github.com/mrousavy/react-native-nitro-image) | | **react-native-nitro-web-image** | Web support for Nitro Image | [GitHub](https://github.com/mrousavy/react-native-nitro-image) | | **@corasan/image-compressor** | Image compression using OpenCV | [GitHub](https://github.com/corasan/image-compressor) | | **react-native-nitro-player** | A powerful audio player library for React Native | [GitHub](https://github.com/riteshshukla04/react-native-nitro-player) | | **react-native-nitro-sound** | Audio playback and recording | [GitHub](https://github.com/hyochan/react-native-nitro-sound) | #### Camera & Vision | Module | Description | Links | |--------|-------------|-------| | **react-native-vision-camera** | High-performance camera library | [GitHub](https://github.com/margelo/react-native-vision-camera) | | **react-native-data-scanner** | One-shot QR/barcode/data scanning | [GitHub](https://github.com/mrousavy/react-native-data-scanner) | | **react-native-fast-tflite** | Fast TensorFlow Lite bindings | [GitHub](https://github.com/margelo/react-native-fast-tflite) | #### Animation & Graphics | Module | Description | Links | |--------|-------------|-------| | **@rive-app/react-native** | Rive animations built with Nitro | [GitHub](https://github.com/rive-app/rive-nitro-react-native) | | **react-native-nitro-palette** | Dominant color palette extraction | [GitHub](https://github.com/Ucekay/nitro-palette) | | **react-native-nitro-theme-transition** | GPU-driven theme-change transitions, sixteen effects | [GitHub](https://github.com/saleh2001k/react-native-nitro-theme-transition) | #### Cryptography & Security | Module | Description | Links | |--------|-------------|-------| | **react-native-quick-crypto** | Fast crypto (Node.js compatible) | [GitHub](https://github.com/margelo/react-native-quick-crypto) | | **react-native-nitro-crypto** | Node.js `crypto` implementation using Rust | [GitHub](https://github.com/iwater/react-native-nitro-crypto) | | **react-native-sensitive-info** | Secure storage with biometrics | [GitHub](https://github.com/mCodex/react-native-sensitive-info) | | **react-native-nitro-bip39** | BIP39 mnemonic implementation | [GitHub](https://github.com/ronickg/react-native-nitro-bip39) | | **react-native-nitro-totp** | TOTP/HOTP generation | [GitHub](https://github.com/4cc3ssX/react-native-nitro-totp) | | **react-native-nitro-tor** | Tor daemon and onion routing | [GitHub](https://github.com/niteshbalusu11/react-native-nitro-tor) | #### Device & Hardware | Module | Description | Links | |--------|-------------|-------| | **react-native-nitro-device-info** | Device information and identifiers | [GitHub](https://github.com/l2hyunwoo/react-native-nitro-device-info) | | **react-native-nitro-event-kit** | iOS EventKit (Calendar & Reminders) | [GitHub](https://github.com/VladyslavMartynov10/react-native-nitro-event-kit) | | **@kingstinct/react-native-healthkit** | iOS HealthKit bindings | [GitHub](https://github.com/kingstinct/react-native-healthkit) | | **react-native-nitro-geolocation** | High-performance geolocation | [GitHub](https://github.com/jingjing2222/react-native-nitro-geolocation) | | **react-native-torch-nitro** | Flashlight/torch control | [GitHub](https://github.com/irekrog/react-native-torch-nitro) | | **react-native-ble-nitro** | Bluetooth Low Energy | [GitHub](https://github.com/zykeco/react-native-ble-nitro) | | **react-native-nitro-haptics** | Low-latency haptic feedback | [GitHub](https://github.com/oblador/react-native-nitro-haptics) | | **@renegades/react-native-tickle** | Haptic pattern editor and player | [GitHub](https://github.com/Renegades-Studio/react-native-tickle) | | **@iternio/react-native-auto-play** | Android Auto and Apple CarPlay support for React Native apps. | [GitHub](https://github.com/Iternio-Planning-AB/react-native-auto-play)| #### File System & Storage | Module | Description | Links | |--------|-------------|-------| | **react-native-nitro-fs** | High-performance file system operations | [GitHub](https://github.com/patrickkabwe/react-native-nitro-fs) | | **react-native-nitro-file-system** | Node.js-compatible `fs` with zero-copy binary data | [GitHub](https://github.com/iwater/react-native-nitro-file-system) | | **react-native-nitro-sqlite** | Fast SQLite database | [GitHub](https://github.com/margelo/react-native-nitro-sqlite) | | **react-native-mmkv** | Fast key-value storage (MMKV) | [GitHub](https://github.com/margelo/react-native-mmkv) | | **react-native-superconfig** | Superfast typesafe config library | [GitHub](https://github.com/riteshshukla04/react-native-superconfig) | #### Networking | Module | Description | Links | |--------|-------------|-------| | **react-native-nitro-in-app-browser** | In-app browser with native UI | [GitHub](https://github.com/patrickkabwe/react-native-nitro-in-app-browser) | | **react-native-nitro-fetch** | super-fast network fetching library | [GitHub](https://github.com/margelo/react-native-nitro-fetch) | | **react-native-nitro-dns** | Fast Node.js `dns` supporting DoH/DoT/DoQ | [GitHub](https://github.com/iwater/react-native-nitro-dns) | | **react-native-nitro-http-server** | High-performance HTTP Server | [GitHub](https://github.com/iwater/react-native-nitro-http-server) | | **react-native-nitro-net** | Node.js-compatible `net`, `tls`, `http(s)` | [GitHub](https://github.com/iwater/react-native-nitro-net) | | **react-native-nitro-dgram** | High-performance Node.js `dgram` (UDP) | [GitHub](https://github.com/iwater/react-native-nitro-dgram) | #### Payments & Commerce | Module | Description | Links | |--------|-------------|-------| | **react-native-iap** | In-app purchases for iOS and Android | [GitHub](https://github.com/hyodotdev/openiap) | #### Maps | Module | Description | Links | |--------|-------------|-------| | **react-native-google-maps-plus** | Google Maps SDK wrapper for Android & iOS | [GitHub](https://github.com/pinpong/react-native-google-maps-plus) | | **react-native-google-places** | Google Places SDK wrapper for Android & iOS | [GitHub](https://github.com/kore-koi/react-native-google-places) | #### UI & Styling | Module | Description | Links | |--------|-------------|-------| | **react-native-unistyles** | Universal styling system | [GitHub](https://github.com/jpudysz/react-native-unistyles) | | **react-native-alternate-app-icon** | Dynamic alternate app icon switching | [GitHub](https://github.com/rutvik24/react-native-alternate-app-icon) | #### Text & Content | Module | Description | Links | |--------|-------------|-------| | **react-native-nitro-markdown** | High-performance Markdown parser | [GitHub](https://github.com/JoaoPauloCMarra/react-native-nitro-markdown) | #### Utilities | Module | Description | Links | |--------|-------------|-------| | **@bernagl/react-native-date** | High-performance native date library | [GitHub](https://github.com/bbernag/react-native-date) | | **react-native-nitro-buffer** | 100% Node.js-compatible `Buffer` | [GitHub](https://github.com/iwater/react-native-nitro-buffer) | | **react-native-nitro-zlib** | Node.js `zlib` implementation | [GitHub](https://github.com/iwater/react-native-nitro-zlib) | | **react-native-clusterer** | Marker clustering for maps | [GitHub](https://github.com/JiriHoffmann/react-native-clusterer) | | **react-native-nitro-google-signin** | Universal (One Tap) Google Sign-In | [GitHub](https://github.com/react-native-nitro-google-sign-in/google-signin) | | **react-native-nitro-google-sso** | Google Sign-In | [GitHub](https://github.com/patrickkabwe/react-native-nitro-google-sso) | | **react-native-nitro-ota** | High-performance OTA updates | [GitHub](https://github.com/riteshshukla04/react-native-nitro-ota) | | **react-native-nitro-version-check** | High-performance Version checking utilities | [GitHub](https://github.com/AlshehriAli0/react-native-nitro-version-check) | | **react-native-play-age-range-declaration** | Google Play and Apple age signals APIs | [GitHub](https://github.com/Gautham495/react-native-play-age-range-declaration) | --- ### Work In Progress Modules currently under active development: | Module | Description | Links | |--------|-------------|-------| | **react-native-mlx** | Apple MLX machine learning | [GitHub](https://github.com/corasan/react-native-mlx) | | **react-native-fast-io** | Blazing fast I/O (WebSocket, Fetch, FS) | [GitHub](https://github.com/callstackincubator/react-native-fast-io) | --- ## Compare Nitro, Turbo Modules, Legacy Native Modules, and Expo Modules ## Comparison with other frameworks Nitro is not the only one of its kind. There's multiple ways to build native modules for React Native: - Nitro Modules - [Turbo Modules](#turbo-modules) - [Legacy Native Modules](#legacy-native-modules) - [Expo Modules](#expo-modules) ### Benchmarks [This benchmark](https://github.com/mrousavy/NitroBenchmarks) compares the total execution time when calling a single native method 100.000 times: ExpoModules TurboModules NitroModules 100.000x addNumbers(...) 434.85ms 115.86ms 7.27ms 100.000x addStrings(...) 429.53ms 179.02ms 29.94ms Note: These benchmarks only compare native method throughput in extreme cases, and do not necessarily reflect real world use-cases. In a real-world app, results may vary. See [NitroBenchmarks](https://github.com/mrousavy/NitroBenchmarks) for full context. It's not all about performance though - there are some key differences between Nitro-, Turbo- and Expo-Modules: ### Turbo Modules [Turbo Modules](https://github.com/reactwg/react-native-new-architecture/blob/main/docs/turbo-modules.md) are React Native's default framework for building native modules. They use a code-generator called "[codegen](https://github.com/reactwg/react-native-new-architecture/blob/main/docs/codegen.md)" to convert Flow (or TypeScript) specs to native interfaces, similar to Nitro's nitrogen. ```swift title="Nitro Module (Swift)" class HybridMath : HybridMathSpec { func add(a: Double, b: Double) -> Double { return a + b } } ``` ```objc title="Turbo Module (Objective-C)" @implementation RTNMath RCT_EXPORT_MODULE() - (NSNumber*)add:(NSNumber*)a b:(NSNumber*)b { double added = a.doubleValue + b.doubleValue; return [NSNumber numberWithDouble:added]; } @end ``` Turbo Modules can be built with Objective-C for iOS and Java for Android, or C++ for cross-platform. #### Shipped with react-native core Unlike Nitro, Turbo Modules are actually part of react-native core. This means, users don't have to install a single dependency to build- or use a Turbo Module. #### Implementation details ##### No Swift There is no direct Swift support for Turbo Modules. You could bridge from Objective-C to Swift, but that would still always go through Objective-C, which is comparatively slower than bridging directly from C++ to Swift, like Nitro does. ```mermaid --- title: "Nitro Modules" --- graph LR; JS--> C++ --> Swift; ``` ```mermaid --- title: "Turbo Modules" --- graph LR; JS--> C++ --> Objective-C --> Swift; ``` ##### No properties A Turbo Module does not provide a syntax for properties. Instead, conventional getter/setter methods have to be used. ```swift title="Nitro Module (Swift)" class HybridMath : HybridMathSpec { var someValue: Double } ``` ```objc title="Turbo Module (Objective-C)" @implementation RTNMath { NSNumber* _someValue; } RCT_EXPORT_MODULE() - (NSNumber*)getSomeValue { return _someValue; } - (void)setSomeValue:(NSNumber*)someValue { _someValue = someValue; } @end ``` ##### Not object-oriented While a Turbo Module can represent many types from JavaScript, there is no equivalent to Nitro's **Hybrid Object** in Turbo Modules. Instead, every Turbo Module is a singleton, and every native method is similar to a static method. Native objects, like Image instances, can not be represented in Turbo Modules. Common workarounds include writing the image to a file, converting images to base64 strings, or using Blobs - which all introduce runtime overhead and performance hits _just to pass an image instance to JS_. ```swift title="HybridImageEditor.swift" class HybridImageEditor: HybridImageEditorSpec { func crop(image: HybridImage, size: Size) -> HybridImage { let original = image.cgImage let cropped = original.cropping(to: size) return HybridImage(cgImage: cropped) } } ``` ```objc title="ImageEditor.mm" @implementation ImageEditor - (NSString*)crop:(NSString*)imageUri size:(CGRect)size { UIImage* image = [UIImage imageWithContentsOfFile:imageUri]; CGImageRef cropped = CGImageCreateWithImageInRect([image CGImage], size); UIImage* croppedImage = [UIImage imageWithCGImage:cropped]; CGImageRelease(cropped); NSString* fileName = [NSString stringWithFormat:@"%@.png", [[NSUUID UUID] UUIDString]]; NSString* filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; NSData* pngData = UIImagePNGRepresentation(croppedImage); [pngData writeToFile:tempPath atomically:YES]; return tempPath; } @end ``` Using native objects (like the `HybridImage`) directly is much more efficient and performant, as well as more convenient to use than to write everything to a file. ##### No tuples There are no tuples in Turbo Modules. ```ts type SomeTuple = [number, number] ``` ##### No callbacks with return values Turbo Modules do not allow JS callbacks to return a value. ```ts type SomeCallback = () => number ``` ##### Events Since functions are not first-class citizens in Turbo Modules, you cannot hold onto a JavaScript callback in native code and call it more often, like you could in Nitro. Instead, Turbo Modules has "Events". Events are essentially just native functions that notify JS and potentially also pass data to JS more often. ```swift title="HybridMath.swift (Nitro)" class Math: MathSpec { var listeners: [(String) -> Void] = [] func addListener(listener: (String) -> Void) { listeners.add(listener) } func onSomethingChanged() { for listener in listeners { listener("something changed!") } } } ``` ```objc title="RTNMath.mm (Turbo)" @implementation RTNMath RCT_EXPORT_MODULE(); - (NSArray *)supportedEvents { return @[@"onSomethingChanged"]; } - (void)onSomethingChanged { NSString* message = @"something changed!"; [self sendEventWithName:@"onSomethingChanged" body:@{@"msg": message}]; } @end ``` Events are untyped and have to be natively defined via `supportedEvents`. In Nitro, this would be fully typesafe as functions are first class citizens. (see `addListener(..)`) ##### HostObject vs NativeState As of today, Turbo Modules are implemented using `jsi::HostObject`, whereas Nitro Modules are built with `jsi::NativeState`. NativeState has been proven to be much more efficient and performant, as property- and method-access is much faster - it can be properly cached by the JS Runtime and does not involve any virtual/Proxy-like accessors. Additionally, Nitro Modules properly set up memory pressure per object, so the JS garbage collector actually knows a native module's memory size and can properly delete them when no longer needed. This is not the case with Turbo Modules. #### Codegen Codegen is similar to Nitrogen as it also generates native interfaces from TypeScript specifications. This ensures type-safety on the JavaScript side, as specs have to be implemented on the native side in order for the app to build successfully. This prevents any wrong type errors and ensures undefined/null-safety. ```ts title="Nitrogen" interface Math extends HybridObject<{ … }> { add(a: number, b: number): Promise } export const Math = NitroModules.createHybridObject('Math') ``` ```ts title="Codegen" export interface Spec extends TurboModule { add(a: number, b: number): Promise; } export const Math = TurboModuleRegistry.get('RTNMath') as Spec | null; ``` ##### Codegen runs on app build Nitrogen is executed explicitly by the library developer and all generated interfaces are part of the npm package to always ship a working solution. Codegen on the other hand runs on app build, which causes specs to always be re-generated for every app. ##### Codegen cannot resolve imports While Nitrogen can properly resolve imports from other files, Codegen can not. ##### Codegen supports Flow Codegen also supports [Flow](https://flow.org), while Nitrogen doesn't. ### Legacy Native Modules Prior to [Turbo Modules](#turbo-modules), React Native provided a default approach for building native modules which was just called ["Native Modules"](https://reactnative.dev/docs/native-modules-intro). Instead of using JSI, Native Modules were built on top of a communication layer that sent events and commands using JSON messages, both asynchronous and batched. Because Turbo Modules are just an evolution of Native Modules, their API is almost identical: ```swift title="Nitro Module (Swift)" class HybridMath : HybridMathSpec { func add(a: Double, b: Double) -> Double { return a + b } } ``` ```objc title="Native Module (Objective-C)" @implementation RTNMath RCT_EXPORT_MODULE() - (NSNumber*)add:(NSNumber*)a b:(NSNumber*)b { double added = a.doubleValue + b.doubleValue; return [NSNumber numberWithDouble:added]; } @end ``` They are now deprecated in favor of [Turbo Modules](#turbo-modules). ### Expo Modules [Expo Modules](https://docs.expo.dev/modules/overview/) is an easy to use API to build native modules by Expo. Unlike both Nitro- and Turbo-, Expo-Modules does not have a code-generator. All native modules are considered untyped, and TypeScript definitions can be written afterwards. An Expo Module can be written using a declarative syntax (_DSL_) where each function and property is declared inside the `definition()` function: ```swift title="Nitro Module (Swift)" class HybridMath : HybridMathSpec { func add(a: Double, b: Double) -> Double { return a + b } } ``` ```swift title="Expo Module (Swift)" public class MathModule: Module { public func definition() -> ModuleDefinition { Name("Math") Function("add") { (a: Double, b: Double) -> Double in return a + b } } } ``` #### Implementation details ##### Swift support Just like Nitro, Expo Modules are written in Swift, instead of Objective-C. Expo Modules however bridge through Objective-C, whereas Nitro bridges to Swift directly (using the new Swift <> C++ interop) which has proven to be much more efficient. ```mermaid --- title: "Nitro Modules" --- graph LR; JS--> C++ --> Swift; ``` ```mermaid --- title: "Expo Modules" --- graph LR; JS--> C++ --> Objective-C --> Swift; ``` ##### Kotlin coroutines Asynchronous functions can be implemented using Kotlin coroutines, which is a convenient pattern for asynchronous code. This is similar to Nitro's `Promise.async` function. ```kotlin title="HybridMath.kt (Nitro)" class HybridMath: HybridMathSpec { override fun doSomeWork(): Promise { return Promise.async { delay(5000) return@async "done!" } } } ``` ```kotlin title="Math.kt (Expo)" class MathModule: Module { override fun definition() = ModuleDefinition { Name("Math") AsyncFunction("doSomeWork") Coroutine { delay(5000) return@Coroutine "done!" } } } ``` ##### Properties Expo Modules supports getting and setting properties, just like Nitro. ##### Events Similar to Turbo Modules, Expo Modules also uses Events to notify JS about any changes on the native side. ```swift title="HybridMath.swift (Nitro)" class Math: MathSpec { var listeners: [(String) -> Void] = [] func addListener(listener: (String) -> Void) { listeners.add(listener) } func onSomethingChanged() { for listener in listeners { listener("something changed!") } } } ``` ```swift title="MathModule.swift (Expo)" let SOMETHING_CHANGED = "onSomethingChanged" public class MathModule: Module { public func definition() -> ModuleDefinition { Name("Math") Events(SOMETHING_CHANGED) } private func onSomethingChanged() { sendEvent(SOMETHING_CHANGED, [ "message": "something changed!" ]) } } ``` ##### HostObject vs NativeState As of today, Expo Modules are implemented using `jsi::HostObject`, whereas Nitro Modules are built with `jsi::NativeState`. NativeState has been proven to be much more efficient and performant, as property- and method-access is much faster - it can be properly cached by the JS Runtime and does not involve any virtual/Proxy-like accessors. Expo-Modules do however properly set memory pressure of native objects, just like in Nitro. ##### Shared Objects Expo Modules has a concept of "shared objects", which is similar to Hybrid Objects in Nitro. :::note I could not find any documentation for Shared Objects, so I cannot really compare them here. ::: ##### No tuples There are no tuples in Expo Modules. ```ts type SomeTuple = [number, number] ``` ##### No callbacks with return values Expo-Modules does not allow JS callbacks to return a value. ```ts type SomeCallback = () => number ``` #### No code-generator Since Expo Modules does not provide a code-generator, all native modules are untyped by default. While TypeScript definitions can be written afterwards, it is possible that the handwritten TypeScript definitions are out of sync with the actual native types due to a user-error, especially when it comes to null-safety. ```ts title="Math.ts (Expo JS side)" interface Math { add(a: number, b: number | undefined): number // b can be undefined here: ^ } const math = ... math.add(5, undefined) // code-error // ^ will throw at runtime! ``` ```swift title="HybridMath.swift (Expo Native side)" public class MathModule: Module { public func definition() -> ModuleDefinition { Name("Math") Function("add") { (a: Double, b: Double) -> Double in // b CANNOT be undefined here: ^ return a + b } } } ``` Note: It is also possible for Nitro specs to go out of sync, but only if you forget to run Nitrogen. In both cases, it's a user-error - one more likely than the other. ### Supported Types JS Type Expo Modules Turbo Modules Nitro Modules number ✅ ✅ ✅ boolean ✅ ✅ ✅ string ✅ ✅ ✅ Int64 ✅ ❌ ✅ UInt64 ✅ ❌ ✅ object ✅ ✅ ✅ T? ✅ ✅ ✅ null ❌ ❌ ✅ T[] ✅ ✅ ✅ Promise<T> ✅ ✅ ✅ (T...) => void ✅ ✅ ✅ (T...) => R ❌ ❌ ✅ [A, B, C, ...] ❌ ❌ ✅ A | B | C | ... ✅ ❌ ✅ Record<string, T> ❌ (no codegen) ❌ ✅ ArrayBuffer ✅ ❌ ✅ ..any HybridObject ✅ ❌ ✅ ..any interface ❌ (no codegen) ✅ ✅ ..any enum ❌ (no codegen) ✅ ✅ ..any union ❌ (no codegen) ❌ ✅ ### Correctness of this page Note: If anything is missing, wrong, or outdated, please let me know so I can correct it immediately! --- ## Contributing If you encounter issues with Nitro, want to fix a bug, or reproduce a bug in the example app, you'd need to clone the repo and get it running first. :::info[Contribution flow and PR rules] This page covers **environment setup and reproduction** — how to run Nitro locally. For the contribution flow itself (what PRs we accept, the required test-per-fix rule, the nitrogen workflow, and the PR checklist), see [**CONTRIBUTING.md**](https://github.com/margelo/nitro/blob/main/CONTRIBUTING.md) in the repo root. Read it before opening a PR. ::: The nitro repo is a Bun monorepo, and is set up like this: - `apps/example/`: A react-native app that uses `react-native-nitro-modules` and `react-native-nitro-test`. - `packages/` - `/nitrogen/`: The Node app that generates Nitro bindings. On npm, it is called `nitrogen`. - `/react-native-nitro-modules/`: The core Nitro Modules library which contains mostly C++ code. - `/react-native-nitro-test/`: An example Nitro Module library full of specs used as compile-time and runtime tests. - `/react-native-nitro-test-external/`: A second test module used to cover cross-module behavior between Nitro Modules. - `/template/`: A template for a Nitro Module library. ### Run Nitro Example #### 1. Set up your development environment You need: - [Bun](https://bun.sh) - [CocoaPods](https://cocoapods.org) (installed via Bundler — see below) - Ruby 2.7.2 (matches CI) - Xcode 26.2 or higher - Android Studio #### 2. Clone the repo Clone [margelo/**nitro**](https://github.com/margelo/nitro) using git, and navigate into the `nitro` folder using Terminal. #### 3. Install dependencies Using Bun, install all required dependencies: ```sh bun install bun run build ``` ##### 3.1. (Optional) Install iOS dependencies If you want to work on the iOS codebase, you also need to install the Pods: ```sh cd apps/example bundle install bun pods ``` #### 4. Run the app After installing all dependencies, you can run the React Native app in `apps/example/`: **iOS** 1. Open `apps/example/ios/NitroExample.xcworkspace` in Xcode 2. Select your target (iPhone Simulator) 3. Click Run **Android** 1. Open `apps/example/android` in Android Studio 2. Click Gradle Sync 3. Click Run ### Reproduce something in the Nitro Example app With most issue reports, it is required to reproduce the issue in the Nitro example app (`apps/example/`). Whether it's a build error, a nitrogen error, or a runtime error, there needs to be a way to reproduce it here. Usually, you can reproduce issues like this: 1. Fork the repository 2. Change the code to reproduce the issue 3. Create a PR to the **nitro** repository which demonstrates the issue :::tip[You don't need to ship a fix] A PR that **only reproduces the bug** — and makes CI go red — is a completely valid and very welcome contribution. If you can add a minimal failing test (compile error or runtime error caught by the Harness tests) and open a PR with just that, stop there. You don't have to attempt a fix. A clean, 100% deterministic repro pinned in CI is often more valuable than a guessed patch; the actual fix can be taken from there. See [You don't need to ship a fix](https://github.com/margelo/nitro/blob/main/CONTRIBUTING.md#you-dont-need-to-ship-a-fix--a-clean-repro-is-enough) in `CONTRIBUTING.md` for details and the test-writing rules (reuse existing types, keep it small, don't remove existing tests). ::: #### Reproduce a build error If you encounter a build error, compare your setup to the setup in `apps/example/`. For example, if you have a different setting in your `Podfile`, try changing it here in Nitro `apps/example/` as well to see if it builds here. Submit a PR with the change required to make it fail, and see if the CI fails to build. That alone is enough — you don't need to also fix it. #### Reproduce a nitrogen bug The Nitro `apps/example/` app uses a Nitro Module (`packages/react-native-nitro-test/`) which acts as an example and contains a lot of test code, like `src/specs/TestObject.nitro.ts` ([link](https://github.com/margelo/nitro/blob/main/packages/react-native-nitro-test/src/specs/TestObject.nitro.ts)). If you change something in `TestObject.nitro.ts`, make sure to run nitrogen from the repo root: ```sh bun specs ``` Commit the generated files. If that change causes a compile error downstream (e.g. Swift refuses to build the generated code), that's a valid repro on its own — open the PR with the red CI and leave the fix to a maintainer if you don't have one. When adding a reproduction, follow the rules in [CONTRIBUTING.md](https://github.com/margelo/nitro/blob/main/CONTRIBUTING.md): reuse existing types where possible, don't remove existing test cases, and keep the addition small. Dumping a full user spec into the test module is not the right approach — distill the bug to the minimal type or call that reproduces it. #### Reproduce a runtime error Submit a PR that demonstrates this runtime error or crash in the Nitro `apps/example/` app — ideally with a new assertion in [`apps/example/src/getTests.ts`](https://github.com/margelo/nitro/blob/main/apps/example/src/getTests.ts) so the regression is caught by the Harness CI workflows (iOS and Android). A PR that only adds the failing assertion and makes Harness go red is enough on its own; you don't have to land the fix. ### Run Nitro Docs The Nitro docs ([nitro.margelo.com](https://nitro.margelo.com)) are built with [Docusaurus](https://docusaurus.io). To run the Nitro docs, follow these steps: #### 1. Install dependencies Navigate into the `docs/` folder, and install all dependencies: ```sh cd docs bun install ``` #### 2. Run docs (dev) Then, just run the docs development server using the docusaurus command: ```sh bun start ``` ### Linting We value code quality and consistent styling. For JS/TS, we use ESLint and Prettier: ```sh bun lint ``` For C++, we use clang-format: ```sh bun lint-cpp ``` For Swift, we use swift format: ```sh bun lint-swift ``` For Kotlin, we use ktlint: ```sh bun lint-kotlin ``` Make sure to lint your files everytime before creating a PR. This is also enforced in the CI, but linting beforehand also applies auto-fixes. --- ## Installation ## For library users If you are using a library that is built with Nitro, all you need to do is install the Nitro Modules core package: **npm** ```sh npm i react-native-nitro-modules cd ios && pod install ``` **yarn** ```sh yarn add react-native-nitro-modules cd ios && pod install ``` **pnpm** ```sh pnpm add react-native-nitro-modules cd ios && pod install ``` **bun** ```sh bun i react-native-nitro-modules cd ios && pod install ``` Nitro Modules are lightweight, yet powerful native bindings to native code, so thank the library author for choosing Nitro to make your app faster! 😄 --- ## ArrayBuffers (`ArrayBuffer`) ## ArrayBuffers (`ArrayBuffer`) Array Buffers allow highly efficient access to shared raw binary data from both JS and native. Passing an `ArrayBuffer` between JS and native is zero-copy. **TypeScript** ```ts interface Image extends HybridObject<{ … }> { getData(): ArrayBuffer } ``` :::note The `ArrayBuffer` type is built-in in JavaScript. ::: **Swift** ```swift class HybridImage: HybridImageSpec { func getData() -> ArrayBuffer } ``` :::note Import ArrayBuffer from Nitro: `import NitroModules` ::: **Kotlin** ```kotlin class HybridImage: HybridImageSpec() { fun getData(): ArrayBuffer } ``` :::note Import ArrayBuffer from Nitro: `import com.margelo.nitro.core.ArrayBuffer` ::: **C++** ```cpp class HybridImage: public HybridImageSpec { std::shared_ptr getData(); } ``` :::note Import ArrayBuffer from Nitro: `#include ` ::: It is crucial to understand the ownership and threading concerns around shared memory access. ### Ownership There's two kinds of `ArrayBuffer`s, **owning** and **non-owning**: #### Owning An `ArrayBuffer` that was created on the native side is **owning** (`isOwner = true`), which means you can safely access its data as long as the `ArrayBuffer` reference is alive. It can be safely held strong for longer, e.g. as a class property/member, and accessed from different Threads. ```swift func doSomething() -> ArrayBuffer { // highlight-next-line let buffer = ArrayBuffer.allocate(1024 * 10) print(buffer.isOwner) // <-- ✅ true let data = buffer.data // <-- ✅ safe to do because we own it! self.buffer = buffer // <-- ✅ safe to use it later! DispatchQueue.global().async { let data = buffer.data // <-- ✅ also safe because we own it! } return buffer } ``` #### Non-owning An `ArrayBuffer` that was created in JS cannot be safely kept strong as the JS VM can delete it at any point, hence it is **non-owning** (`isOwner = false`). Its data can only be safely accessed before the synchronous function returned, as this will stay within the JS bounds. ```swift func doSomething(buffer: ArrayBuffer) { print(buffer.isOwner) // <-- ❌ false let data = buffer.data // <-- ✅ safe to do because we're still sync DispatchQueue.global().async { // code-error let data = buffer.data // <-- ❌ NOT safe } } ``` If you need a non-owning buffer's data for longer, **copy it first**: ```swift func doSomething(buffer: ArrayBuffer) { // diff-add let copy = buffer.isOwner // diff-add ? buffer // diff-add : ArrayBuffer.copy(of: buffer) let data = copy.data // <-- ✅ safe now because we have an owning copy DispatchQueue.global().async { let data = copy.data // <-- ✅ still safe now because we have an owning copy } } ``` :::note Not every `ArrayBuffer` received from JS is **non-owning**, eg if the buffer was created in native and then did a JS-roundtrip it is still **owning**! Always check the `isOwner` property to prevent unnecessary copies. ::: ### Threading An `ArrayBuffer` can be accessed from both JS and native, and even from multiple Threads at once, but they are **not thread-safe**. To prevent race conditions or garbage-data from being read, make sure to not read from- and write to- the `ArrayBuffer` at the same time. ### Creating Buffers Buffers can either be created from native (**owning**), or from JS (**non-owning**). #### From native On the native side, an **owning** `ArrayBuffer` can either **wrap-**, or **copy-** an existing buffer: **Swift** ```swift let myData = UnsafeMutablePointer.allocate(capacity: 4096) // wrap (no copy) let wrappingArrayBuffer = ArrayBuffer.wrap(dataWithoutCopy: myData, size: 4096, onDelete: { myData.deallocate() }) // copy let copiedArrayBuffer = ArrayBuffer.copy(of: wrappingArrayBuffer) // new blank buffer let newArrayBuffer = ArrayBuffer.allocate(size: 4096) ``` **Kotlin** ```kotlin val myData = ByteBuffer.allocateDirect(4096) // wrap (no copy) val wrappingArrayBuffer = ArrayBuffer.wrap(myData) // copy let copiedArrayBuffer = ArrayBuffer.copy(myData) // new blank buffer val newArrayBuffer = ArrayBuffer.allocate(4096) ``` **C++** ```cpp auto myData = new uint8_t[4096]; // wrap (no copy) auto wrappingArrayBuffer = ArrayBuffer::wrap(myData, 4096, [=]() { delete[] myData; }); // copy auto copiedArrayBuffer = ArrayBuffer::copy(myData, 4096); // new blank buffer auto newArrayBuffer = ArrayBuffer::allocate(4096); ``` ##### Language-native buffer types ArrayBuffers also provide helper and conversion methods for the language-native conventional buffer types: **Swift** Swift often uses [`Data`](https://developer.apple.com/documentation/foundation/data) to represent Data. ```swift let data = Data(capacity: 1024) let buffer = ArrayBuffer.copy(data: data) let dataAgain = buffer.toData(copyIfNeeded: true) ``` **Kotlin** Kotlin often uses [`ByteBuffer`](https://developer.android.com/reference/java/nio/ByteBuffer) to represent Data. ```kotlin val data = ByteBuffer.allocateDirect(1024) val buffer = ArrayBuffer.copy(data) val dataAgain = buffer.getBuffer(copyIfNeeded = true) ``` **C++** C++ often uses [`std::vector`](https://en.cppreference.com/w/cpp/container/vector) to represent Data. ```cpp std::vector data; auto buffer = ArrayBuffer::copy(data); /* convert back to vector would be a copy. */ ``` #### From JS From JS, a **non-owning** `ArrayBuffer` can be created via the [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) web APIs, and viewed or edited using the typed array APIs (e.g. [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)). ```ts const arrayBuffer = new ArrayBuffer(4096) const view = new Uint8Array(arrayBuffer) view[0] = 64 view[1] = 128 view[2] = 255 ``` ##### Creating a native buffer from JS To create an **owning** `ArrayBuffer` from JS, you can use the `createNativeArrayBuffer(size)` helper: ```ts const arrayBuffer = NitroModules.createNativeArrayBuffer(4096) ``` In contrast to using the default JS `ArrayBuffer` constructor, this allocates a native `ArrayBuffer` using Nitro's implementation, allowing you to use this buffer in native, without performing any copies as it already is **owning**. --- ## Arrays (`T[]`) ## Arrays (`T[]`) Arrays are represented with the most common and efficient native data structures, such as `std::vector` in C++ or `Array` in Swift and Kotlin. **TypeScript** ```ts interface Contacts extends HybridObject<{ … }> { getAllUsers(): User[] } ``` **Swift** ```swift class HybridContacts: HybridContactsSpec { fun getAllUsers() -> Array } ``` **Kotlin** ```kotlin class HybridContacts: HybridContactsSpec() { fun getAllUsers(): Array } ``` **C++** ```cpp class HybridContacts : public HybridContactsSpec { std::vector getAllUsers(); } ``` ### Kotlin `PrimitiveArray` As a performance improvement, the JNI (C++ -> Kotlin interface) provides **Primitive Array** datatypes which can avoid boxing primitives into `Object`s, and provides bulk copy methods. This makes all array operations **a lot faster**, and Nitrogen is smart enough to ✨**automagically**✨ use Primitive Arrays whenever possible. This will replace the following arrays: - `Array` -> [`DoubleArray`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-double-array/) - `Array` -> [`BooleanArray`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-boolean-array/) - `Array` -> [`LongArray`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-long-array/) --- ## Callbacks (`(...) => T`) ## Callbacks (`(...) => T`) Callbacks are functions created in one language and passed to another to provide a way to "call back" later. Nitro has a clever reference counting system to allow users to use callbacks/functions from JS safely, and without any limitations. Each callback holds a strong reference on the native side and can be called as often as needed. Once the callback is no longer used, it will be safely deleted from memory. **TypeScript** In TypeScript, a callback is represented as an anonymous function: ```ts interface Server extends HybridObject<{ … }> { start(onNewUserJoined: (user: User) => void): void } ``` **Swift** In Swift, a callback is represented as a closure: ```swift func start(onNewUserJoined: (User) -> Void) { onNewUserJoined(user) } ``` **Kotlin** In Kotlin, a callback is represented as a lambda: ```kotlin fun start(onNewUserJoined: (User) -> Unit) { onNewUserJoined(user) } ``` **C++** In C++, a callback is represented as a function: ```cpp void start(std::function onNewUserJoined) { onNewUserJoined(user); } ``` ### Events Since callbacks can be safely kept in memory for longer and called multiple times, Nitro does not have a special type for an "event". It is simply a function you store in memory and call later, just like in a normal JS class. ✨ **TypeScript** ```ts type Orientation = "portrait" | "landscape" interface DeviceInfo extends HybridObject<{ … }> { listenToOrientation(onChanged: (o: Orientation) => void): void } const deviceInfo = // ... deviceInfo.listenToOrientation((o) => { console.log(`Orientation changed to ${o}!`) }) ``` **Swift** ```swift func listenToOrientation(onChanged: (Orientation) -> Void) { self.listeners.append(onChanged) } func onRotate() { for listener in self.listeners { listener(newOrientation) } } ``` **Kotlin** ```kotlin fun listenToOrientation(onChanged: (Orientation) -> Unit) { this.listeners.add(onChanged) } fun onRotate() { for (listener in this.listeners) { listener(newOrientation) } } ``` **C++** ```cpp void listenToOrientation(std::function onChanged) { this->listeners.push_back(onChanged); } void onRotate() { for (const auto& listener: this->listeners) { listener(newOrientation); } } ``` ### Callbacks that return a value (`(...) => T`) Since JS callbacks could theoretically be called from any native Thread, Nitro safely wraps the result types of callbacks that return a value in **Promises which need to be awaited**. ```ts title="Math.nitro.ts" interface Math extends HybridObject<{ ios: 'swift' }> { some(getValue: () => number): void } ``` ```swift title="HybridMath.swift" func some(getValue: () -> Promise) { Task { let promise = getValue() let valueFromJs = try await promise.await() } } ``` ### Synchronous Callbacks By default, callback functions in Nitro are _asynchronous_. Their execution is scheduled on the JS Thread, and if they return a value they always return a `Promise` wrapping the value. This ensures that you can call the callback from any Thread, and it safely executes the actual JS function on the correct JS Thread. In addition to that, Nitro also supports fully _synchronous_ callbacks. They are considered dangerous, as the caller is responsible for ensuring Thread safety. To extend the previous example, we can make `getValue()` synchronous by wrapping it in the `Sync` type provided by Nitro: ```ts title="Math.nitro.ts" interface Math extends HybridObject<{ ios: 'swift' }> { some(getValue: Sync<() => number>): void } ``` ```swift title="HybridMath.swift" func some(getValue: () -> Double) { let valueFromJs = getValue() } ``` :::warning The `getValue()` callback can now only be called from the JS Thread. ::: ### How was it before Nitro? Conventionally (in legacy React Native Native Modules), a native method could only have a maximum of two callbacks, one "success" and one "failure" callback. Once one of these callbacks is called, both will be destroyed and can no longer be called later. This is why React Native introduced "Events" as a way to call into JS more than just once. This also meant that an asynchronous function could not have any callbacks, since a Promise's resolve and reject functions are already two callbacks. For example, this was **not possible**: ```ts interface Camera { startRecording(onStatusUpdate: () => void, // code-error onRecordingFailed: () => void, // code-error onRecordingFinished: () => void): Promise } ``` Thanks to Nitro's clever reference system, functions can be safely held in memory and called as many times as you like, just like in a normal JS class. This makes "Events" obsolete, and allows using as many callbacks per native method as required. --- ## Custom Enums (`A | B`) ## Custom Enums (`A | B`) There's two different types of enums - [enums](#typescript-enums) and [unions](#typescript-union). ### TypeScript enums A [TypeScript enum](https://www.typescriptlang.org/docs/handbook/enums.html) is essentially just an object where each key is backed by ascending integer values. Nitrogen will generate a C++ enum natively, which bridges to JS as a simple integer: ```ts enum Gender { MALE, FEMALE } interface Person extends HybridObject<{ … }> { getGender(): Gender } ``` This is efficient because `MALE` is the number `0`, `FEMALE` is the number `1`, and all other values are invalid. ### TypeScript union A [TypeScript union](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#intersection-types) is essentially just a variant of literal strings, which is only "typed" via TypeScript. ```ts type Gender = 'male' | 'female' interface Person extends HybridObject<{ … }> { getGender(): Gender } ``` Nitrogen statically generates hashes for the strings `"male"` and `"female"` at compile-time, allowing for very efficient conversions between JS `string`s and native `enum`s. #### Unions need a name A TypeScript union consisting of only literal strings needs to be aliased to a separate type so that nitrogen can generate the native `enum` accordingly. ```ts title="Bad ❌" interface Person extends HybridObject<{ … }> { getGender(): 'male' | 'female' } ``` ```ts title="Good ✅" type Gender = 'male' | 'female' interface Person extends HybridObject<{ … }> { getGender(): Gender } ``` --- ## Custom Structs (`interface`) ## Custom Structs (`interface`) Any custom `interface` or `type` that does **not** extend `HybridObject` will be represented as a fully type-safe `struct` in C++/Swift/Kotlin. Simply define the type in your `.nitro.ts` spec: ```ts title="Nitro.nitro.ts" interface Person { name: string age: number } interface Nitro extends HybridObject<{ ios: 'swift' }> { getAuthor(): Person } ``` ```swift title="HybridNitro.swift" class HybridNitro: HybridNitroSpec { func getAuthor() -> Person { return Person(name: "Marc", age: 24) } } ``` Nitro enforces full type-safety to avoid passing or returning wrong types. Both `name` and `age` are always part of `Person`, they are never a different type than a `string`/`number`, and never null or undefined. This makes the TypeScript definition the **single source of truth**, allowing you to rely on types! 🤩 ### Prefer `interface` over `type` Since TypeScript flattens types (`type`), their symbols or declarations might get lost. Unfortunately Nitro cannot find a struct name for a flattened type, so it is generally recommended to use `interface` instead of `type`: ```ts title="Bad ❌" type Person = { name: string age: number } ``` ```ts title="Good ✅" interface Person { name: string age: number } ``` ### Combined types (e.g. `Partial`) In TypeScript, it is a common practice to modify and combine types using [utility types like `Partial`](https://www.typescriptlang.org/docs/handbook/utility-types.html). As mentioned in ["Prefer `interface` over `type`"](#prefer-interface-over-type), you should prefer to use `interface` over `type` for those types too: ```ts title="Bad ❌" interface Person { name: string age: number } type PartialPerson = Partial ``` ```ts title="Good ✅" interface Person { name: string age: number } interface PartialPerson extends Partial {} ``` This way TypeScript always keeps the `interface` in-tact, allowing Nitrogen to properly process it. ### Structs are eagerly converted Since structs are just flat value types, each key/value is eagerly converted from a JS value to a native value (and vice-versa) when passing them between JS and native. This is fine for small structs and benefits from low allocation cost, but contains an overhead when your structs growing larger. Use [Hybrid Objects](hybrid-objects) to implement a way to lazily convert each key/value instead. --- ## Custom Types (manually written `T`) ## Custom Types (manually written `T`) The `JSIConverter` is Nitro's implementation for converting JS values to native values, and back. It's implemented as a C++ template, which allows it to be extended with any custom type. For example, if you want to use `float` directly you can tell Nitro how to convert a `jsi::Value` to `float` by implementing `margelo::nitro::JSIConverter`: ```cpp title="JSIConverter+Float.hpp" #pragma once // ... namespace margelo::nitro { template <> struct JSIConverter final { static inline float fromJSI(jsi::Runtime&, const jsi::Value& arg) { return static_cast(arg.asNumber()); } static inline jsi::Value toJSI(jsi::Runtime&, float arg) { return jsi::Value(arg); } static inline bool canConvert(jsi::Runtime&, const jsi::Value& value) { return value.isNumber(); } }; } ``` :::tip Make sure you overload `JSIConverter<...>` within the `margelo::nitro` namespace! ::: Then just use it in your methods: **With Nitrogen ✨** ```ts title="Math.nitro.ts" type Float = CustomType< number, 'float', { include: 'JSIConverter+Float.hpp' } > interface Math extends HybridObject<{ ios: 'c++' }> { add(a: Float, b: Float): Float } ``` ```cpp title="HybridMath.hpp" class HybridMath: public HybridMathSpec { public: float add(float a, float b) override { return a + b; } }; ``` :::info Make sure the `JSIConverter+Float.hpp` header you wrote is included in your project's user-search-path so it can be included. ::: **Manually** ```cpp title="HybridMath.hpp" #pragma once #include "JSIConverter+Float.hpp" // ... class HybridMath : public HybridObject { public: float add(float a, float b) { return a + b; } void loadHybridMethods() { HybridObject::loadHybridMethods(); registerHybrids(this, [](Prototype& prototype) { prototype.registerHybridMethod("add", &HybridMath::add); }); } } ``` :::info Make sure the compiler knows about `JSIConverter` at the time when `HybridMath` is declared, so import your `JSIConverter+Float.hpp` in your Hybrid Object's header file as well! ::: ### Foreign types (e.g. `react::ShadowNodeWrapper`) Since you have full control over the conversion part, you can even safely use foreign types like React Native's core types. For example, let's use `react::ShadowNodeWrapper`, which is stored as a `jsi::NativeState` on a `jsi::Object`: ```cpp title="JSIConverter+ShadowNode.hpp" #pragma once #include // ... namespace margelo::nitro { template <> struct JSIConverter> { static std::shared_ptr fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { jsi::Object obj = arg.asObject(runtime); return obj.getNativeState(runtime); } static jsi::Value toJSI(jsi::Runtime& runtime, std::shared_ptr arg) { jsi::Object obj(runtime); obj.setNativeState(runtime, arg); return obj; } static bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { if (!value.isObject()) return false; jsi::Object obj = value.getObject(runtime); return obj.hasNativeState(runtime); } }; } ``` Then just use the type in your methods: **With Nitrogen ✨** ```ts title="MyHybrid.nitro.ts" type ShadowNode = CustomType< React.Component['state'], 'std::shared_ptr', { include: 'JSIConverter+ShadowNode.hpp' } > interface MyHybrid extends HybridObject<{ ios: 'c++' }> { doSomething(view: ShadowNode): void } ``` ```cpp title="MyHybrid.hpp" class MyHybrid: public MyHybridSpec { public: void doSomething(std::shared_ptr view) override { // ... } }; ``` :::info Make sure the `JSIConverter+ShadowNode.hpp` header is included in your project's user-search-path so it can be included. ::: **Manually** ```cpp title="MyHybrid.hpp" #pragma once #include "JSIConverter+ShadowNode.hpp" // ... class MyHybrid: public HybridObject { public: void doSomething(std::shared_ptr view) override { // ... } void loadHybridMethods() { HybridObject::loadHybridMethods(); registerHybrids(this, [](Prototype& prototype) { prototype.registerHybridMethod("doSomething", &MyHybrid::doSomething); }); } }; ``` :::info Make sure the compiler knows about `JSIConverter` at the time when `MyHybrid` is declared, so import your `JSIConverter+ShadowNode.hpp` in your Hybrid Object's header file as well! ::: And lastly you'd pass the JS value that maps to this native type - in our case, `react::ShadowNodeWrapper` is stored on a View ref's `state` property: ```tsx function App() { const ref = useRef(null) const call = () => { myHybrid.doSomething(ref.current.state) } return } ``` --- ## Dates (`Date`) ## Dates (`Date`) Dates are date and time instances that can be passed between JS and native. Under the hood, conversions take place using milliseconds, so it's essentially a wrapper around `numbers`. **TypeScript** ```ts interface Image extends HybridObject<{ … }> { getCreationDate(): Date } ``` **Swift** ```swift class HybridImage: HybridImageSpec { func getCreationDate() -> Date } ``` **Kotlin** ```kotlin class HybridImage: HybridImageSpec() { fun getCreationDate(): Instant } ``` **C++** ```cpp class HybridImage: public HybridImageSpec { std::chrono::system_clock::time_point getCreationDate(); } ``` --- ## Other Hybrid Objects (`HybridObject`) ## Other Hybrid Objects (`HybridObject`) Since Nitro Modules are object-oriented, a `HybridObject` itself is a first-class citizen. This means you can pass around instances of native `HybridObject`s between JS and native, allowing for safe interface-level abstractions: ```ts title="Camera.nitro.ts" interface Image extends HybridObject<{ ios: 'swift' }> { readonly width: number readonly height: number } interface Camera extends HybridObject<{ ios: 'swift' }> { takePhoto(): Image } ``` ```swift title="HybridCamera.swift" class HybridImage: HybridImageSpec { let uiImage: UIImage var width: Double { uiImage.size.width } var height: Double { uiImage.size.height } } class HybridCamera: HybridCameraSpec { func takePhoto() -> HybridImageSpec { return HybridImage(uiImage: …) } } ``` ### Interface-level abstraction Since Hybrid Objects are declared as interfaces, `Image` could have different implementations... ```swift class HybridUIImage: HybridImageSpec { // ... var uiImage: UIImage } class HybridCGImage: HybridImageSpec { // ... var cgImage: CGImage } class HybridBufferImage: HybridImageSpec { // ... var gpuBuffer: CMSampleBuffer } ``` ...but still be used exactly the same in other places, as it is all a `HybridImageSpec`. Even if they use different implementations under the hood, they all share a common interface with properties like `width`, `height` and more: ```ts title="Cropper.nitro.ts" interface Cropper extends HybridObject<{ ios: 'swift' }> { crop(image: Image, size: Size): Image } ``` ```swift title="Cropper.swift" class HybridCropper: HybridCropperSpec { func crop(image: HybridImageSpec, size: Size) -> HybridImageSpec { let data = image.data let croppedData = cropFunc(data, size) return HybridCGImage(data: croppedData) } } ``` ### `AnyHybridObject` If you don't need a specific type of `HybridObject` but instead just want any `HybridObject`, you can use Nitro's `AnyHybridObject` type. This type only works in C++. ```ts title="Some.nitro.ts" import { HybridObject, AnyHybridObject } from 'react-native-nitro-modules' interface Some extends HybridObject<{ ios: 'c++' }> { something(obj: AnyHybridObject): void } ``` ```cpp title="HybridSome.hpp" class HybridSome: public HybridSomeSpec { public: void something( const std::shared_ptr& obj ) override; }; ``` --- ## Nulls (`null`) ## Nulls (`null`) A `null` is used to refer to the intentional absence of a value. While it can be used as a separate type directly, `null` most commonly used to make an existing type _explicitly nullable_: **TypeScript** ```ts interface Math extends HybridObject<{ … }> { a: number | null } ``` **Swift** ```swift class HybridMath: HybridMathSpec { var a: Variant_NullType_Double } ``` **Kotlin** ```kotlin class HybridMath: HybridMathSpec() { override var a: Variant_NullType_Double } ``` **C++** ```cpp class HybridMath: public HybridMathSpec { std::variant a; }; ``` The `NullType` is a singleton structure in Nitro. To return `null` to JS: **Swift** ```swift func getNull() -> NullType { return .null } ``` **Kotlin** ```kotlin fun getNull(): NullType { return NullType.NULL } ``` **C++** ```cpp NullType getNull() { return nitro::null; } ``` ### Optionals vs `null` In the same way that JavaScript distinguishes between an optional type/`undefined` and `null`, Nitro also has two separate concepts for the two. If you simply want to make an existing type _optional_, use [Optionals](optionals) instead. --- ## Optionals (`T?`) ## Optionals (`T?`) Optional or undefined values can be declared with the question mark operator (`?`) or with an `undefined` variant. **TypeScript** ```ts interface Math extends HybridObject<{ … }> { a?: number b: number | undefined } ``` **Swift** ```swift class HybridMath: HybridMathSpec { var a: Double? var b: Double? } ``` **Kotlin** ```kotlin class HybridMath: HybridMathSpec() { override var a: Double? override var b: Double? } ``` **C++** ```cpp class HybridMath: public HybridMathSpec { std::optional a; std::optional b; }; ``` In Kotlin/Java, nullables have to be boxed in object types. ### Optionals vs `null` In the same way that JavaScript distinguishes between an optional type/`undefined` and `null`, Nitro also has two separate concepts for the two. An optional type (or `undefined`) represents a non-declared value, whereas `null` represents an explicit absence of a value. See [Nulls](nulls) for more information. --- ## Primitives (`number`, `boolean`, `bigint`) ## Primitives (`number`, `boolean`, `bigint`) Primitive datatypes like `number`, `boolean` or `bigint` can use platform-native datatypes directly. For example, a JS `number` is always a 64-bit `double` in C++, a `Double` in Swift, and a `Double` in Kotlin. **TypeScript** ```ts interface Math extends HybridObject<{ … }> { add(a: number, b: number): number } ``` **Swift** ```swift class HybridMath: HybridMathSpec { func add(a: Double, b: Double) -> Double } ``` **Kotlin** ```kotlin class HybridMath: HybridMathSpec() { fun add(a: Double, b: Double): Double } ``` **C++** ```cpp class HybridMath : public HybridMathSpec { double add(double a, double b); } ``` Primitives are very efficient and can be passed with little to no overhead, especially between C++ and Swift, and C++ and Kotlin. ### `bigint` While a `bigint` is technically a primitive in JS, it is not fully representable by any standard library type in C++, Kotlin or Swift. In Nitro, you can use `bigint` as either a signed, or unsigned 64-bit integer, which makes it a primitive, but limits its range to those within 64-bit signed/unsigned values. #### `Int64` A 64-bit signed integer that ranges from -2^63 to 2^63-1. **TypeScript** ```ts interface Math extends HybridObject<{ … }> { add(a: Int64, b: Int64): Int64 } ``` **Swift** ```swift class HybridMath: HybridMathSpec { func add(a: Int64, b: Int64) -> Int64 } ``` **Kotlin** ```kotlin class HybridMath: HybridMathSpec() { fun add(a: Long, b: Long): Long } ``` **C++** ```cpp class HybridMath : public HybridMathSpec { int64_t add(int64_t a, int64_t b); } ``` #### `UInt64` A 64-bit unsigned integer that ranges from 0 to 2^64-1. On 64-bit systems, this is the most convenient type to describe pointer addresses. **TypeScript** ```ts interface Math extends HybridObject<{ … }> { add(a: UInt64, b: UInt64): UInt64 } ``` **Swift** ```swift class HybridMath: HybridMathSpec { func add(a: UInt64, b: UInt64) -> UInt64 } ``` **Kotlin** ```kotlin class HybridMath: HybridMathSpec() { fun add(a: ULong, b: ULong): ULong } ``` **C++** ```cpp class HybridMath : public HybridMathSpec { uint64_t add(uint64_t a, uint64_t b); } ``` :::warning A `UInt64` cannot be stored in an [AnyMap](untyped-maps). ::: #### Anything larger If you want to represent any `bigint` larger than `UInt64`/`Int64`'s maximum value, or smaller than `UInt64`/`Int64`'s minimum value, you must implement your own big integer type. You can achieve this either by just passing the `bigint` as a [`string`](strings) and deserializing it again on the native side, or by rolling your own type via [Custom Types](custom-types) - in this case the `JSIConverter` can convert your custom big integer type from- and to a `bigint`. --- ## Promises (`Promise`) ## Promises (`Promise`) A function can be made asynchronous by returning a `Promise` to JS. This allows your native code to perform heavy-, long-running tasks in parallel, while the JS thread can continue rendering and performing other business logic. **TypeScript** In TypeScript, a `Promise` is represented using the built-in `Promise` type, which can be awaited: ```ts interface Math extends HybridObject<{ … }> { fibonacci(n: number): Promise } const math = // ... await math.fibonacci(13) ``` :::note The `Promise` type is built-in in JavaScript. ::: **Swift** In Swift, a `Promise` can be created via Nitro's [`Promise`](https://github.com/margelo/nitro/blob/main/packages/react-native-nitro-modules/ios/core/Promise.swift) type - for example, to use Swift's new async/await syntax: ```swift func fibonacci(n: Double) -> Promise { return Promise.async { // This runs on a separate Thread, and can use `await` syntax! return try await calculateFibonacciSequence(n) } } ``` :::note Import Promise from Nitro: `import NitroModules` ::: **Kotlin** In Kotlin, a `Promise` can be created via Nitro's [`Promise`](https://github.com/margelo/nitro/blob/main/packages/react-native-nitro-modules/android/src/main/java/com/margelo/nitro/core/Promise.kt) type - for example, to use Kotlin's coroutine syntax: ```kotlin fun fibonacci(n: Double): Promise { return Promise.async { // This runs on a separate Thread, and can use suspending coroutine functions! return calculateFibonacciSequence(n) } } ``` :::note Import Promise from Nitro: `import com.margelo.nitro.core.Promise` ::: **C++** In C++, a `Promise` can be created via Nitro's [`Promise`](https://github.com/margelo/nitro/blob/main/packages/react-native-nitro-modules/cpp/core/Promise.hpp) type - for example, to use an asynchronous Thread pool: ```cpp std::shared_ptr> fibonacci(double n) { return Promise::async([=]() -> double { // This runs on a separate Thread! return calculateFibonacciSequence(n); }); } ``` :::note Import Promise from Nitro: `#include ` ::: Additionally, Nitro statically enforces that **Promises can never go stale**, preventing you from accidentally "forgetting" to resolve or reject a Promise: ```swift title="HybridMath.swift" func saveToFile(image: HybridImage) -> Promise { guard let data = image.data else { return } // code-error ^ // Error: Cannot return void! return Promise.async { try await data.writeToFile("file://tmp/img.png") } } ``` --- ## Raw `jsi::Value` / `jsi::Runtime` ## Raw `jsi::Value` / `jsi::Runtime` Even though Nitro supports virtually any JS type, there are certain use cases where you might want to work with a raw `jsi::Value` or a `jsi::Runtime` directly - and Nitro provides an escape hatch for this. Since JSI is not typed, Nitrogen does not have typing support for a method that takes raw `jsi::Value`s - so you have to define it yourself by **overriding** `loadHybridMethods()` in C++: ```cpp class HybridMath : public HybridMathSpec { public: HybridMath(): HybridObject(TAG) {} public: jsi::Value myRawMethod(jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* args, size_t count); public: void loadHybridMethods() override; } ``` :::warning In JS, `myRawMethod` is not typed. You are expected to provide type-safety for it by safely checking the arguments and return values yourself. ::: The implementation of your `loadHybridMethods()` should call the base's `loadHybridMethods()`, and then register your raw JSI methods: ```cpp void HybridMath::loadHybridMethods() { // 1. Load base methods HybridMathSpec::loadHybridMethods(); // 2. Register own methods registerHybrids(this, [](Prototype& prototype) { prototype.registerRawHybridMethod("myRawMethod", /* args count */ 2, &HybridMath::myRawMethod); }); } ``` :::important If you don't have a base spec (here, `HybridMathSpec` is a nitrogenerated spec), you just want to call `HybridObject::loadHybridMethods()`. ::: And lastly, implement your raw JSI method: ```cpp jsi::Value HybridMath::myRawMethod(jsi::Runtime& runtime, const jsi::Value& thisValue, const jsi::Value* args, size_t count) { double a = args[0].asNumber(); double b = args[1].asNumber(); return jsi::Value(a + b); } ``` :::note The syntax of a raw JSI method is always the same as the one of a `jsi::HostFunctionType`. ::: :::tip You can use Nitro's converters by including `#include `, then using it: ```cpp double a = JSIConverter::fromJSI(runtime, args[0]); ``` ::: --- ## Strings (`string`) ## Strings (`string`) A `string` handles like a [primitive](primitives), but technically isn't one. In C++, a `string` is represented using a UTF-8 `std::string`. **TypeScript** ```ts interface MyHybrid extends HybridObject<{ … }> { concat(a: string, b: string): string } ``` **Swift** ```swift class MyHybrid: MyHybridSpec { func concat(a: String, b: String) -> String } ``` **Kotlin** ```kotlin class MyHybrid: MyHybridSpec() { fun concat(a: String, b: String): String } ``` **C++** ```cpp class MyHybrid : public MyHybridSpec { std::string concat(const std::string& a, const std::string& b); } ``` --- ## Tuples (`[A, B, ...]`) ## Tuples (`[A, B, ...]`) A Tuple is a fixed-length set of items of the given types. Example: ```ts type Point = [number, number] interface Math extends HybridObject<{ … }> { distance(a: Point, b: Point): number } ``` Tuples can also have different types per value: ```ts title="Bad ❌" type Values = (number | string | Person)[] interface Math extends HybridObject<{ … }> { calculate(values: Values): void } ``` The type in the **Bad ❌** example generates an [array](arrays) of [variants](variants), where its size is unknown and each value could be a `number`, a `string` or a `Person`. It is less efficient than a **tuple** because of the variant allocation. ```ts title="Good ✅" type Values = [number, string, Person] interface Math extends HybridObject<{ … }> { calculate(values: Values): void } ``` The type in the **Good ✅** example generates a **tuple**, where its size is guaranteed to be **3** and each value is known at compile-time: `values[0]: number`, `values[1]: string`, `values[2]: Person`. --- ## Typed maps (`Record`) ## Typed maps (`Record`) A typed map is an object where each value is of the given type `T`. For example, if your API returns a map of users with their ages, you _could_ use a `Record`: **TypeScript** ```ts interface Database extends HybridObject<{ … }> { getAllUsers(): Record } ``` **Swift** ```swift class HybridDatabase: HybridDatabaseSpec { func getAllUsers() -> Dictionary } ``` **Kotlin** ```kotlin class HybridDatabase: HybridDatabaseSpec() { fun getAllUsers(): Map } ``` **C++** ```cpp class HybridDatabase: public HybridDatabaseSpec { std::unordered_map getAllUsers(); } ``` :::tip While typed maps are very efficient, Nitro cannot sufficiently optimize the object as keys are not known in advance. If possible, **avoid typed maps** and use [arrays](arrays) for unknown number of items, or [strongly typed objects](custom-structs) for known number of items instead. ::: --- ## Untyped maps (`AnyMap` / `object`) ## Untyped maps (`AnyMap` / `object`) An untyped map represents a JSON-like structure with a value that can either be a `number`, a `string`, a `boolean`, an `Int64`, a `null`, an array or an object. **TypeScript** ```ts interface Fetch extends HybridObject<{ … }> { get(url: string): AnyMap } ``` **Swift** ```swift class HybridFetch: HybridFetchSpec { func get(url: String) -> AnyMap } ``` **Kotlin** ```kotlin class HybridFetch: HybridFetchSpec() { fun get(url: String): AnyMap } ``` **C++** ```cpp class HybridFetch: public HybridFetchSpec { std::shared_ptr get(const std::string& url); } ``` :::tip While untyped maps are implemented efficiently, Nitro cannot sufficiently optimize the object as keys and value-types are not known in advance. If possible, **avoid untyped maps** and use [strongly typed objects](custom-structs) instead. ::: --- ## Variants (`A | B | ...`) ## Variants (`A | B | ...`) A Variant is a type of either one of the values defined in its declaration. Example: ```ts interface Math extends HybridObject<{ … }> { distance(value: number | Point): number } ``` :::tip While variants are still very efficient, they need runtime-checks for type conversions, which comes with a tiny overhead compared to all other statically defined types. If possible, **avoid variants**. ::: ### No literal values A variant can only consist of types, not of literal values. ```ts export interface Person extends HybridObject<{ … }> { // diff-remove getGender(): 'male' | 'female' } ``` If you need variants of literal values, you probably want to use [an enum (_union_)](custom-enums#typescript-union) instead. ### Custom Alias Names Each variant is a unique type in Swift/Kotlin - for example: `string | number` becomes `Variant_String_Double`. Since the generated names are hard to read, it is recommended to declare type-aliases with custom names instead: ```ts title="Bad ❌" interface Math extends HybridObject<{ … }> { calculate(): string | number } ``` ```ts title="Good ✅" type MathOutput = string | number interface Math extends HybridObject<{ … }> { calculate(): MathOutput } ``` This will then use the easier-to-read type-alias name instead of `Variant_String_Double`: ```swift title="nitrogen/generated/ios/HybridMathSpec.swift" public protocol HybridMathSpec: HybridObject { // diff-remove func calculate() -> Variant_String_Double // diff-add func calculate() -> MathOutput } ```