Nitro Modules

repository·main·Indexed 23 days ago

https://github.com/mrousavy/nitro

A framework for building high-performance, type-safe native modules for React Native using statically compiled JSI bindings. It includes Nitrogen, a code-generator that transforms TypeScript interfaces into C++, Swift, and Kotlin code, as well as support for manual Hybrid Objects to bridge C++ bindings to JavaScript.

Tokens
52.1K
Snippets
155
Records
267
Agent score
82%

What's inside Nitro Modules

  1. What is Nitro?

    main

    Nitro is a framework for building high-performance native modules for JavaScript. It allows you to implement JavaScript objects in C++, Swift, or Kotlin instead of JS.

    Key abstractions:

    • Nitro Module: A library built with Nitro containing one or more Hybrid Objects.
    • Hybrid Object: A native object implemented in C++, Swift, or Kotlin that can be accessed directly from JS.
    • Nitrogen: An optional code-generator that creates native bindings from TypeScript interfaces, ensuring type safety between JS and native code.

    Nitro is primarily designed for React Native but works in any environment using JSI (JavaScript Interface).

  2. What are Nitro Modules

    main

    Nitro Modules are fast, type-safe native modules for React Native that use statically compiled bindings to JSI (JavaScript Interface). The ecosystem consists of two primary components:

    • react-native-nitro-modules: The core C++ library that powers all nitro modules.
    • nitrogen: An optional code-generator designed for library authors to streamline the creation of nitro modules.
  3. Compare Nitro Modules with other React Native frameworks

    main

    Nitro Modules are a high-performance alternative to Turbo Modules, Legacy Native Modules, and Expo Modules.

    Performance Benchmarks

    In high-throughput scenarios (e.g., 100,000 calls to a single method), Nitro significantly outperforms other frameworks:

    • addNumbers (numeric): Nitro (~7.27ms) vs Turbo (~115.86ms) vs Expo (~434.85ms).
    • addStrings (string): Nitro (~29.94ms) vs Turbo (~179.02ms) vs Expo (~429.53ms).

    Note: These benchmarks reflect native method throughput and may vary in real-world applications.

    Key Architectural Differences

    | Feature | Nitro Modules | Turbo Modules | Legacy Native Modules | Expo Modules | | :--- | :--- | :--- | :--- | : | | Core Integration | External dependency | Shipped with RN core | Shipped with RN core | Part of Expo ecosystem | | Swift Support | Direct (C++ to Swift) | Indirect (via Objective-C) | Indirect (via Objective-C) | N/A | | Properties | Native syntax support | Manual getter/setter | Manual getter/setter | N/A | | Object Model | Hybrid Objects (OO) | Singletons only | Singletons only | N/A | | Data Types | Supports Tuples | No Tuples | No Tuples | N/A | | Callbacks | Can return values | Cannot return values | N/A | N/A | | Implementation | jsi::NativeState | jsi::HostObject | JSON Bridge | N/A |

  4. Understand the react-native-nitro-template structure

    main

    The template follows a specific directory structure to support cross-platform C++, Kotlin, and Swift implementations, alongside Nitrogen-generated code.

    Core Configuration

    • nitro.json: The central configuration file for Nitrogen. It defines native namespaces and the library name.
    • package.json: The npm package definition. Note that react-native-nitro-modules must be listed as a peerDependency.

    Platform Implementations

    • Android (android/):
      • build.gradle: Configures Kotlin, adds Nitrogen autolinking via $$androidCxxLibName$$+autolinking.gradle, and triggers the C++ build.
      • CMakeLists.txt: Builds the C++ library (named $$androidCxxLibName$$), includes Nitrogen autolinking, and includes cpp-adapter.cpp to autolink C++ HybridObjects.
      • src/main/java/com/margelo/nitro/$$androidNamespace$$/:
        • $$androidCxxLibName$$Package.kt: The required React Native package file used by the CLI for autolinking. This is where you autolink Kotlin HybridObjects.
    • iOS (ios/):
      • $$iosModuleName$$.podspec: The Podspec build file. The Pod name must match the name in nitro.json. It includes your .swift or .cpp files and uses add_nitrogen_files(s) to include Nitrogen-generated files.
    • Cross-Platform (cpp/):
      • Contains shared C++ implementations.

    TypeScript and Nitrogen

    • nitrogen/: Contains files generated by Nitrogen. This folder should be committed to git.
    • src/:
      • The TypeScript codebase that defines HybridObjects and handles runtime loading.
      • src/specs/: Contains *.nitro.ts files. Nitrogen runs on all files in this directory to generate the necessary glue code.
  5. What is Nitrogen and when to use it

    main

    Nitrogen is Nitro's code-generator. It uses an AST parser to read TypeScript definitions and automatically generate native interfaces (C++, Swift, and Kotlin) for React Native Nitro Modules.

    Key Benefits:

    • Type-Safety: If the native implementation does not match the TypeScript spec (e.g., missing methods or incorrect types), the app will fail to compile.
    • Automation: It handles the boilerplate of creating cross-platform bindings.

    When to use it:

    • Library Authors: Should use Nitrogen to generate specs and commit the generated files to their repository. This ensures users of the library don't need to run Nitrogen themselves.
    • App Developers: If you are consuming a library already built with Nitro, you do not need to run Nitrogen.
  6. What is Nitrogen?

    main
    Nitrogen is a code-generator designed for use with Nitro Modules. It takes TypeScript interfaces as input and automatically generates the corresponding C++, Swift, and Kotlin code, along with the necessary native bindings. These bindings are built on top of the react-native-nitro-modules core APIs, automating the bridge between TypeScript and native code.
  7. Register Hybrid Objects via initializeNative()

    main

    Hybrid Objects from your Nitro Module are registered in the HybridObjectRegistry. This registration must be triggered by calling .initializeNative(). The location of this call depends on your environment:

    • In React Native: This is typically handled in your *Package.kt file, which calls .initializeNative().
    • Outside of React Native: You must manually call .initializeNative() within your library's entry point.
  8. Handle bigints larger than 64-bit

    main

    Since Int64 and UInt64 are limited to 64-bit ranges, you must use alternative strategies to represent JS bigint values that exceed these bounds:

    1. String passing: Pass the bigint as a string and deserialize it on the native side.
    2. Custom Types: Implement a custom big integer type and use a JSIConverter to handle the conversion between your custom type and a JS bigint.
  9. Avoid stale Promises with Nitro static enforcement

    main

    Nitro statically enforces that Promises must always be resolved or rejected. You cannot exit a function that returns a Promise<T> without returning a valid Promise instance. This prevents bugs where a developer might accidentally return void or exit early without fulfilling the promise contract.

    func saveToFile(image: HybridImage) -> Promise<Void> {
      guard let data = image.data else { return } // code-error: Cannot return void!
      return Promise.async {
        try await data.writeToFile("file://tmp/img.png")
      }
    }
  10. Use HybridObject for interface-level abstractions

    main

    Because Nitro Modules are object-oriented, HybridObject instances are first-class citizens that can be passed between JavaScript and native code. This allows you to define interfaces in TypeScript that multiple native implementations can satisfy.

    For example, you can define an Image interface. On the native side, you can have multiple classes (e.g., HybridUIImage, HybridCGImage, or HybridBufferImage) that all implement the same HybridImageSpec. This allows other native or JS functions (like a Cropper) to accept any object that conforms to that specification, regardless of its underlying data storage.

    interface Image
      extends HybridObject<{ ios: 'swift' }> {
      readonly width: number
      readonly height: number
    }
    
    interface Camera
      extends HybridObject<{ ios: 'swift' }> {
      takePhoto(): Image
    }
  11. Define Custom Structs using `interface`

    main

    Any TypeScript interface or type that does not extend HybridObject is represented as a fully type-safe struct in C++, Swift, or Kotlin. When you define a struct in your .nitro.ts spec, Nitro ensures that the types are strictly enforced across the JS/Native boundary. For example, a string in TypeScript will always be a string in native code, and fields will never be null or undefined unless explicitly handled.

    interface Person {
      name: string
      age: number
    }
    
    interface Nitro
      extends HybridObject<{ ios: 'swift' }> {
      getAuthor(): Person
    }
  12. Use Promises for asynchronous native tasks

    main

    To perform heavy or long-running tasks in parallel without blocking the JavaScript thread, you can make native functions asynchronous by returning a Promise<T>. This allows the JS thread to continue rendering and executing business logic while the native code runs on a separate thread.

    Nitro provides platform-specific implementations of Promise<T> for Swift, Kotlin, and C++ to integrate with their respective concurrency models (async/await, coroutines, and thread pools).