Nitro FFI Framework

website·Indexed 31 days ago

https://nitro.shreeman.dev

A zero-overhead, type-safe FFI framework for building high-performance plugins with Dart, Kotlin, Swift, and C++. It features a toolchain for scaffolding plugins via `nitrogen init`, a type system supporting `@HybridEnum` and `@HybridStruct`, and a C++ Direct Path architecture for dispatching method calls directly to native code via generated FFI bindings.

Tokens
36.8K
Snippets
204
Records
264
Agent score
57%

What's inside Nitro

  1. Overview of the Nitro FFI Framework

    Nitro is a zero-overhead, type-safe FFI framework for Flutter. It allows developers to define a native API once in a .native.dart spec file, and then automatically generates the corresponding Kotlin, Swift, and C++ bindings. It is designed to be significantly faster than MethodChannels (approximately 0.26 µs per native call) and supports iOS, Android, macOS, Windows, and Linux.
  2. Use NativeHandle for zero-overhead native pointer passing

    NativeHandle<T> allows passing opaque native pointers through the Nitro bridge without codec overhead. The type parameter T is for Dart-side documentation; the actual wire format is a void* (or Long in Kotlin / UnsafeMutableRawPointer? in Swift). Use this when native code owns a heap-allocated resource (e.g., GPU buffers, camera frames) that Dart needs to reference without copying.
    @NitroModule(ios: NativeImpl.swift, android: NativeImpl.kotlin)
    abstract class Camera extends HybridObject {
      // Borrowed handle: valid only for the duration of the call
      NativeHandle<Void> peekLatestFrame();
    
      // Owned handle: Dart takes ownership
      @NitroOwned
      NativeHandle<Void> acquireFrame();
    
      // Pass handle back to native
      void processFrame(NativeHandle<Void> handle);
    }
  3. Utilize Flutter Nitro exclusive features

    Flutter Nitro introduces several capabilities not present in RN Nitro:

    • Streaming: Stream<T> with backpressure modes (dropLatest, block, bufferDrop, batch).
    • Native-Driven Async: @NitroNativeAsync allows native code to drive async operations and post results back without an isolate hop.
    • Desktop Support: First-class support for Windows and Linux via C++.
    • Rich Types: @HybridRecord for binary-encoded nested types and @HybridStruct for zero-copy views over native memory.
    • Error Handling: @NitroResult<T> provides discriminated NitroOk/NitroErr results to avoid C++ exceptions.
  4. Understand the Nitro Swift bridging layers

    Nitro uses a multi-layer bridging system to connect Dart to Swift. The @_cdecl layer is critical because it must use C-ABI-compatible types, which differ from native Swift types used in the final protocol implementation.

    Data Flow:

    1. Dart (dart:ffi): Uses types like Pointer<Utf8>, Int64, Double.
    2. C++ Bridge: Uses extern "C" types like const char*, int32_t, double.
    3. @_cdecl Swift Layer: Uses C-ABI types like UnsafePointer<CChar>?, Int64, Double.
    4. Swift Protocol Implementation: Uses native Swift types like String, Int, Double, Bool.
  5. Map React Native Nitro concepts to Flutter Nitro

    Flutter Nitro is inspired by react-native-nitro-modules. For developers experienced with RN Nitro (v0.35.x), the core concepts are nearly identical in Flutter Nitro (v0.5.x), with two primary architectural differences: the bridge is transitioned from JSI to Dart FFI, and the specification language is transitioned from TypeScript to Dart.
  6. Use NitroPromise<T> for observable async state

    NitroPromise<T> is a Completer-backed promise used in Dart/FFI as the equivalent of RN Nitro's C++ Promise<T>. It is preferred over a raw Completer<T> when you need observable state, multiple subscribers, or chaining combinators. It follows 'first-wins' semantics: once resolved or rejected, subsequent calls to resolve() or reject() are ignored.
    final promise = NitroPromise<int>();
    
    // Native side completes it:
    promise.resolve(42);
    promise.reject(StateError('too late'));   // no-op — already settled
    
    final value = await promise.future;       // 42
  7. Migrate Nitro from 0.2 to 0.3

    To upgrade Nitro from 0.2 to 0.3, update the dependencies in pubspec.yaml, regenerate the bridges, and optionally adopt new typed platform markers. If you previously used manual struct release calls, these can now be removed as cleanup is handled automatically via NativeFinalizer.
    dependencies:
      nitro: ^0.3.0
    
    dev_dependencies:
      build_runner: ^2.4.0
      nitro_generator: ^0.3.0
      nitrogen_cli: ^0.3.0
    # Regenerate all bridges
    cd your_plugin/
    dart run nitrogen_cli generate
    dart run nitrogen_cli link
  8. Return @HybridStruct types from Swift

    Types marked as @HybridStruct are heap-allocated and returned as an UnsafeMutableRawPointer? (void*). The Dart side then unpacks this pointer into the generated Struct subclass.
    @_cdecl("_call_getSensorData")
    public func _call_getSensorData() -> UnsafeMutableRawPointer? {
        guard let result = MyRegistry.impl?.getSensorData() else { return nil }
        let ptr = UnsafeMutablePointer<SensorData>.allocate(capacity: 1)
        ptr.initialize(to: result)
        return UnsafeMutableRawPointer(ptr)
    }
  9. Manage Hybrid Object lifecycles and disposal

    Every Nitro module extends HybridObject. Each Dart instance maps to a unique native instance. Instances register in the NitroInstanceRegistry upon construction and unregister during dispose(). In debug mode, checkDisposed() is used to guard calls and throw errors if an instance has already been disposed to prevent native state corruption.
  10. Fix Linux build failures (GTK3 and C++ standards)

    If the Flutter Linux build fails, check the following:

    1. Missing GTK3 headers: Install them via apt install libgtk-3-dev.
    2. undefined reference to 'pthread_create': Add dl pthread to target_link_libraries in CMake.
    3. 'std::make_unique' is not a member of 'std': Upgrade to GCC 9+ or add -std=c++17 to CMAKE_CXX_FLAGS.
    apt install libgtk-3-dev
  11. Configure Android build requirements for Nitro plugins

    Nitro plugins require specific Android Gradle configurations, including a minimum SDK of 24, specific ABI filters for 64-bit architectures, and a link to the CMakeLists.txt file. The kotlinx-coroutines-android dependency is required for asynchronous operations. The NDK path must be specified in local.properties.
    // android/build.gradle
    android {
        compileSdk 34
        defaultConfig {
            minSdk 24
            ndk {
                abiFilters 'arm64-v8a', 'x86_64'
            }
        }
        externalNativeBuild {
            cmake {
                path "../../src/CMakeLists.txt"
            }
        }
    }
    
    dependencies {
        implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0"
    }

    // android/local.properties ndk.dir=/Users/you/Library/Android/sdk/ndk/26.1.10909125

  12. Install nitrogen_cli

    Install the nitrogen_cli tool globally via Dart pub and ensure the pub-cache bin directory is added to your system PATH.
    dart pub global activate nitrogen_cli
    
    # Add to ~/.zshrc or ~/.bashrc:
    export PATH="$PATH:$HOME/.pub-cache/bin"
    
    # Verify installation
    nitrogen --version