Skip to content

Shreemanarjun/nitro_ecosystem

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

426 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nitrogen — Zero-overhead FFI Plugins for Flutter

nitro nitro_annotations nitro_generator nitrogen_cli License: MIT

Write one .native.dart spec file. Get type-safe Kotlin, Swift, or C++ — all generated.

No method channels. No manual FFI. No boilerplate.

📖 Full docs, live examples, and API reference: nitro.shreeman.dev


Why Nitrogen?

Method Channel Manual FFI Nitrogen
Call overhead ~107 µs ~0 µs (floor) ~0.26 µs (macOS) – ~2.1 µs (Android)
Type safety stringly-typed hand-written, error-prone generated from one Dart spec, strict
Async manual isolates ✅ generated (@nitroAsync / @nitroNativeAsync)
Streams + backpressure ✅ slow manual SendPort plumbing ✅ zero-copy, 4 backpressure strategies
Zero-copy buffers manual Pointer<T> @HybridStruct(zeroCopy: [...]), @zeroCopy
Desktop (Windows/Linux) method channel only manual FFI ✅ same spec, direct C++
Code you write a lot, on every platform enormous, unsafe 3 files: spec + Kotlin impl + Swift impl (or 1 C++ impl)

Packages

Package Role pubspec.yaml section
nitro Runtime — base classes, FFI helpers, codec dependencies
nitro_annotations All annotations (zero deps, works without Flutter) dependencies
nitro_generator build_runner code generator dev_dependencies
nitrogen_cli CLI — init, generate, link, doctor dart pub global activate
# pubspec.yaml
dependencies:
  nitro: ^0.5.7

dev_dependencies:
  nitro_generator: ^0.5.7
  build_runner: ^2.4.0
dart pub global activate nitrogen_cli  # one-time

Requirements

Tool Minimum version
Flutter SDK 3.22.0+
Dart SDK 3.3.0+
Android NDK 26.1+ (r26b)
Kotlin 1.9.0+
iOS Deployment Target 13.0+
Swift 5.9+ (Xcode 15+)
Xcode 15.0+
Windows (desktop C++) Visual Studio 2022 + CMake 3.14+
Linux (desktop C++) GCC/Clang + CMake 3.10+

Quick Start

1. Scaffold a plugin

nitrogen init my_plugin
# Creates a fully-wired Flutter plugin with a starter spec, Kotlin impl, and Swift impl.

2. Define your API in a .native.dart spec

// lib/src/math.native.dart
import 'package:nitro/nitro.dart';
part 'math.g.dart';

@NitroModule(
  lib: 'math',
  ios: AppleNativeImpl.swift,
  android: AndroidNativeImpl.kotlin,
)
abstract class Math extends HybridObject {
  static final Math instance = _MathImpl();

  double add(double a, double b);
  String greet(String name);
  int get precision;
  set precision(int value);
}

3. Generate native bindings

nitrogen generate
# Runs build_runner, then syncs generated files to ios/Classes/ and android/src/

4. Implement on each platform

Kotlin (android/.../MathImpl.kt):

class MathImpl : HybridMathSpec {
    override fun add(a: Double, b: Double): Double = a + b
    override fun greet(name: String): String = "Hello, $name"
    override var precision: Long = 6
}

Swift (ios/Classes/MathImpl.swift):

class MathImpl: NSObject, HybridMathProtocol {
    func add(a: Double, b: Double) -> Double { a + b }
    func greet(name: String) -> String { "Hello, \(name)" }
    var precision: Int64 = 6
}

5. Wire the build system

nitrogen link    # wires CMake, Podspec, .clangd
nitrogen doctor  # health-check every layer

6. Use from Dart

final sum = Math.instance.add(3.14, 2.71);
print(Math.instance.greet('World')); // "Hello, World"

Or: the interactive dashboard

Running nitrogen with no arguments launches a TUI dashboard covering every command above (init, generate, watch, link, doctor, migrate, clean, update) with live progress, plus one-click Open in VS Code / Open in Antigravity buttons:

Nitrogen Dashboard

Every command also works headlessly for CI — pass --no-ui, or just pipe the output (non-TTY auto-detects and switches to plain-text [nitro]/[nitro:warn]/[nitro:error] logging):

nitrogen generate --no-ui --fail-on-warn   # exit 2 on spec warnings
nitrogen doctor --no-ui                    # exit 1 on any health-check error

Implementation Paths

Platform field Constant Bridge When to use
ios: / macos: AppleNativeImpl.swift Swift @_cdecl iOS/macOS platform APIs
ios: / macos: AppleNativeImpl.cpp Direct C++ Shared C++ logic
android: AndroidNativeImpl.kotlin Kotlin JNI Android platform APIs
android: AndroidNativeImpl.cpp Direct C++ Shared C++ logic
windows: WindowsNativeImpl.cpp Direct C++ Windows desktop
linux: LinuxNativeImpl.cpp Direct C++ Linux desktop
web: WebNativeImpl.wasm WASM/JS interop Web

NativeImpl.swift, .kotlin, .cpp, .wasm are backward-compatible shorthands. The explicit per-platform constants catch invalid combinations (e.g. Kotlin on macOS) at compile time.

Direct C++ path

When both ios: and android: use *NativeImpl.cpp, a single C++ class serves all platforms with no JNI or Swift shim:

@NitroModule(
  lib: 'math',
  ios: AppleNativeImpl.cpp,
  android: AndroidNativeImpl.cpp,
  macos: AppleNativeImpl.cpp,
  windows: WindowsNativeImpl.cpp,
  linux: LinuxNativeImpl.cpp,
)
abstract class Math extends HybridObject {
  static final Math instance = _MathImpl();
  double add(double a, double b);
}

After nitrogen generate, subclass the abstract C++ interface:

// src/HybridMathImpl.cpp  (you write this)
#include "math.native.g.h"

class HybridMathImpl : public HybridMath {
public:
    double add(double a, double b) override { return a + b; }
};

static HybridMathImpl g_math;
__attribute__((constructor))
static void math_auto_register() { math_register_impl(&g_math); }

All Annotations

@NitroModule — define your native API

@NitroModule(
  lib: 'camera',             // shared library name
  ios: AppleNativeImpl.swift,
  android: AndroidNativeImpl.kotlin,
  macos: AppleNativeImpl.cpp,
)
abstract class Camera extends HybridObject {
  static final Camera instance = _CameraImpl();
  bool isAvailable();
}

@HybridEnum — enum at the C boundary

Maps a Dart enum to int64_t. Values are contiguous from startValue by default; use nativeValues for non-contiguous OS enums.

@HybridEnum(startValue: 0)
enum DeviceStatus { idle, busy, error }

// Non-contiguous (e.g. mirror an OS SDK enum with gaps):
@HybridEnum(nativeValues: [0, 50, 100])
enum Quality { low, medium, high }

@HybridStruct — zero-copy C struct

Passes all fields as a packed C struct across the FFI boundary in a single call. Best for hot-path numeric data (frames, sensor readings). Fields may be int, double, bool, or another @HybridStruct.

@HybridStruct(packed: true)
class SensorReading {
  final double temperature;
  final double humidity;
  final int timestampMs;
  const SensorReading({required this.temperature, required this.humidity, required this.timestampMs});
}

Note: String fields in a @HybridStruct cost ~100–500 ns each (heap copy via strdup). If your struct carries string fields used frequently, prefer @HybridRecord.

@HybridRecord — binary-encoded complex data

For infrequent, complex transfers (device lists, configs, API responses). Supports strings, nested records, lists, and nullable fields — encoded as a compact little-endian binary protocol (no JSON).

@HybridRecord()
class UserProfile {
  final String name;
  final int age;
  final List<String> tags;
  const UserProfile({required this.name, required this.age, required this.tags});
}

// Wire format (little-endian):
// [4B payload_len][4B utf8_len][utf8_bytes][8B int64][4B count][...]

@NitroVariant — discriminated union (sealed class)

Marks a sealed class as a tagged union. Each concrete subclass is one variant case. Cases with fields encode them using the @HybridRecord binary codec.

@NitroVariant()
sealed class FilterResult { const FilterResult(); }

class FilterAccepted extends FilterResult {
  final String id;
  const FilterAccepted({required this.id});
}
class FilterRejected extends FilterResult { const FilterRejected(); }

// Usage:
final result = await filter.apply(input);
switch (result) {
  case FilterAccepted(:final id): print('accepted: $id');
  case FilterRejected(): print('rejected');
}

Limit: @NitroVariant supports up to 255 cases. Exceeding this is rejected at generation time.

@NitroTuple — named positional record type

Annotate a Dart 3 positional record typedef. Fields are accessed via $1, $2, etc. in Dart; Kotlin gets a data class and Swift a struct.

@NitroTuple()
typedef Point2D = (double, double);

@NitroTuple()
typedef NamedPair = (String, int);

@NitroModule(lib: 'geometry', ios: AppleNativeImpl.cpp, android: AndroidNativeImpl.cpp)
abstract class Geometry extends HybridObject {
  static final Geometry instance = _GeometryImpl();
  Point2D midpoint(Point2D a, Point2D b);
}

// Usage:
final mid = Geometry.instance.midpoint((1.0, 2.0), (3.0, 4.0));
print('${mid.$1}, ${mid.$2}'); // "2.0, 3.0"

@nitroAsync — background-thread dispatch

Offloads a synchronous native call to Nitrogen's pre-warmed isolate pool and returns a Future. Overhead: ~28 µs on macOS (persistent-worker isolate dispatch — roughly at parity with a method channel round-trip; see Performance).

@nitroAsync
Future<String> processImage(String path);

// With timeout:
@NitroAsync(timeout: 5000)
Future<Uint8List> fetchData(String url);

@nitroNativeAsync — zero-hop native async

The native side runs its own async work (Swift async/await, Kotlin coroutine, C++ thread pool) and calls Dart_PostCObject_DL to post the result directly. Dart opens a ReceivePort and awaits it — no Dart isolate is spawned. Overhead: ~27 µs on macOS — no isolate hop, so no dispatch overhead beyond the native call itself.

@nitroNativeAsync
Future<String> fetchDataNative(String url);

@nitroNativeAsync
Future<int> heavyComputation(int n);

Swift implementation (uses native async):

// Generated protocol method:
func fetchDataNative(url: String, port: Int64) {
    Task {
        let result = await URLSession.shared.dataTask(url: URL(string: url)!)
        Nitro.postString(to: port, value: String(data: result.0, encoding: .utf8)!)
    }
}

Kotlin implementation (uses coroutines):

override fun fetchDataNative(url: String, port: Long) {
    scope.launch {
        val result = httpClient.get(url).bodyAsText()
        NitroBridge.postString(port, result)
    }
}

Use @nitroNativeAsync when: the native side already has async infrastructure (coroutines, Swift async, thread pool) — it skips the isolate hop entirely. @nitroAsync exists for the opposite case: a blocking native call with no async infrastructure of its own, dispatched off the main isolate via a persistent worker pool.

@NitroStream — native-to-Dart event stream

Configures a native-to-Dart event stream with built-in backpressure. The native side emits items from any thread; Dart receives them as a typed Stream<T>.

@NitroStream(backpressure: Backpressure.dropLatest)
Stream<SensorReading> get sensorStream;

@NitroStream(backpressure: Backpressure.batch, batchMaxSize: 64)
Stream<double> get audioSamples;

Backpressure strategies:

Strategy Behaviour When to use
Backpressure.dropLatest Drop the newest item if Dart is behind Camera frames, sensors — stale data is useless
Backpressure.bufferDrop Ring buffer; oldest item dropped when full Logging, monitoring — prefer recent, tolerate loss
Backpressure.block Block the emitter until Dart consumes Reliable delivery, emitter is interruptible
Backpressure.batch Accumulate up to batchMaxSize before one bridge crossing High-frequency primitives (IMU, audio samples)

Zero-copy proxy streaming for @HybridStruct items

When a @NitroStream item type is a @HybridStruct, the generator emits a proxy class that extends the value type and reads every field lazily from native heap memory — no fields are copied until accessed, and a NativeFinalizer frees the native struct automatically on GC:

// Declared type is unchanged — Stream<SensorReading>, not some proxy type.
sensorModule.sensorStream.listen((reading) {
  // reading is SensorReadingProxy at runtime (IS-A SensorReading).
  // Reading a field is a single native-heap load — zero allocation.
  print(reading.temperature); // → native heap load, no copy, no malloc

  // Need an immutable copy that outlives this callback?
  final snapshot = (reading as SensorReadingProxy).toDartAndRelease();
});
Approach Field read Allocation per item Memory management
Eager .toDart() on arrival All fields copied upfront 1 Dart object Manual malloc.free in unpack
Zero-copy proxy (default) Lazy — only accessed fields 0 extra allocations NativeFinalizer on GC

@NitroResult — method-level error return

The native implementation signals failure by returning an error tag + message instead of throwing. Dart receives NitroResultValue<T> (either NitroOk<T> or NitroErr) — exception-free error handling.

@NitroResult()
@nitroNativeAsync
Future<NitroResultValue<String>> login(String user, String password);

// Dart usage — no try/catch needed:
final result = await auth.login('alice', 'secret');
switch (result) {
  case NitroOk(:final value): print('token: $value');
  case NitroErr(:final message): print('failed: $message');
}

@zeroCopy — zero-copy buffer parameter

Marks a Uint8List parameter as a raw native pointer. The callee must not retain the pointer past the function call — it points to pinned Dart memory.

void processPixels(@zeroCopy Uint8List pixels);
// C: void processPixels(const uint8_t* pixels, int64_t pixels_length)
// Kotlin: fun processPixels(pixels: ByteBuffer)
// Swift: func processPixels(pixels: UnsafeMutablePointer<UInt8>?, pixelsLength: Int64)

@NitroOwned — native heap pointer with auto-release

The native side heap-allocates a resource and Dart takes ownership. A NativeFinalizer calls the generated _release C symbol when the NativeHandle is GC'd.

@NitroOwned
NativeHandle<Void> acquireFrame();

// Usage:
final frame = camera.acquireFrame();
// frame is automatically released when GC'd.
// Or release eagerly:
frame.release();

@NitroCustomType — user-defined FFI codec

Registers a Dart class as a custom bridge type with a user-provided NitroFfiCodec. The generator emits codec.encode() / codec.decode() calls wherever the type appears in a spec. Native implementations receive raw bytes.

class ColorCodec extends NitroFfiCodec<Color> {
  const ColorCodec();
  @override int get encodedSize => 5; // 1B hasValue + 4B RGBA
  @override Pointer<Uint8> encode(Color? v, Arena alloc) {
    final p = alloc<Uint8>(5);
    p[0] = v != null ? 1 : 0;
    if (v != null) { p[1] = v.r; p[2] = v.g; p[3] = v.b; p[4] = v.a; }
    return p;
  }
  @override Color? decode(Pointer<Uint8> ptr) {
    if (ptr[0] == 0) return null;
    return Color(ptr[1], ptr[2], ptr[3], ptr[4]);
  }
}

@NitroCustomType(codec: ColorCodec, encodedSize: 5)
class Color {
  final int r, g, b, a;
  const Color(this.r, this.g, this.b, this.a);
}

Complete Type Support

Primitive scalars

Dart C Kotlin Swift
int int64_t Long Int64
double double Double Double
bool int8_t Boolean Bool
String const char* / std::string String String
void void Unit Void

Nullable primitives

Nullable primitives are bridged using @Packed(1) structs — the same in-memory layout as C++ std::optional<T>. No sentinels, no heap allocation on sync paths.

Dart C struct Wire size
int? NitroOptInt64 { uint8_t hasValue; int64_t value; } 9 bytes
double? NitroOptFloat64 { uint8_t hasValue; double value; } 9 bytes
bool? NitroOptBool { uint8_t hasValue; uint8_t value; } 2 bytes
String? const char* (null pointer = absent) pointer

Note: int?, double?, and bool? inside callback parameters use sentinel values (not NitroOptXxx structs) because NativeCallable function pointers have no Arena available. Full-range nullable prim callbacks should wrap params in a @HybridRecord.

TypedData buffers

All variants are supported: Uint8List, Int8List, Int16List, Uint16List, Int32List, Uint32List, Int64List, Uint64List, Float32List, Float64List.

Each TypedData param expands to (pointer + length) at the C boundary:

// Dart spec:
void processAudio(Float32List samples);
// C bridge: void processAudio(const float* samples, int64_t samples_length)
// Kotlin:   fun processAudio(samples: FloatArray)
// Swift:    func processAudio(samples: UnsafeMutablePointer<Float>?, samplesLength: Int64)

Collections

Dart Encoding
List<int> / List<double> / List<bool> / List<String> @HybridRecord binary codec
List<@HybridRecord> Indexed binary blob (LazyRecordList<T> — O(1) random access)
List<@HybridEnum> @HybridRecord binary codec
List<@NitroVariant> @HybridRecord binary codec
Map<String, T> JSON via dart:convert (String key only)
Map<String, @HybridRecord> Binary tag-5 blob
Map<String, @NitroVariant> Binary tag-5 blob

Custom types

Annotation C type Use case
@HybridEnum int64_t Enum constants
@HybridStruct void* (packed struct) Hot-path numeric structs
@HybridRecord uint8_t* (binary blob) Complex / infrequent data
@NitroVariant uint8_t* (tag + payload) Discriminated unions
@NitroTuple uint8_t* (binary blob) Named positional record
@NitroCustomType uint8_t* (codec bytes) Any user-defined type
NativeHandle<Void> void* Opaque pointer with auto-release

Special runtime types

Type Description
AnyNativeObject Opaque native object handle (pointer stored as int64)
NitroAnyValue Dynamic variant: null / bool / int / double / String / List / Map
NitroAnyMap Map<String, NitroAnyValue> — equivalent to a JSON object
NitroPromise<T> Dart-side future that a native side can resolve/reject; wraps a ReceivePort

Default Parameter Values

Named parameters with default values are preserved in generated Dart FFI bindings — callers get the default, no wrapper needed:

@HybridEnum()
enum PrintQuality { draft, normal, high }

abstract class Printer extends HybridObject {
  void print(String text, {PrintQuality quality = PrintQuality.normal, int copies = 1});
}

Generated Dart FFI (callers see the defaults):

void print(String text, {PrintQuality quality = PrintQuality.normal, int copies = 1}) { ... }

Supported default literal types: int, double, bool, String, @HybridEnum.


Cross-File Type Sharing

Types defined in one .native.dart can be imported and used in another. The generator tracks ownership and emits correct #include directives:

// types.native.dart  ← type-only file (no @NitroModule)
import 'package:nitro/nitro.dart';
part 'types.g.dart';

@HybridEnum()
enum DeviceStatus { idle, busy, error }

@HybridRecord()
class DeviceInfo {
  final String name;
  final DeviceStatus status;
  const DeviceInfo({required this.name, required this.status});
}
// scanner.native.dart
import 'package:nitro/nitro.dart';
import 'types.native.dart';  // import shared types
part 'scanner.g.dart';

@NitroModule(lib: 'scanner', ios: AppleNativeImpl.swift, android: AndroidNativeImpl.kotlin)
abstract class Scanner extends HybridObject {
  static final Scanner instance = _ScannerImpl();
  List<DeviceInfo> scanDevices();
}

Error Handling

Exceptions from native

All bridge paths catch native errors and rethrow as HybridException:

try {
  await myModule.doWork();
} on HybridException catch (e) {
  print('Native error: ${e.message}');
}
  • Kotlin/JNI path: Java ExceptionHybridException
  • Swift path: Swift Error + NSExceptionHybridException
  • C++ direct path: std::exceptionnitro_report_error()HybridException

Exception-free error results (@NitroResult)

For fallible operations that should not throw, @NitroResult() gives you a typed result:

@NitroResult()
Future<NitroResultValue<UserProfile>> fetchUser(String id);

// Dart switch is exhaustive — no uncaught exceptions:
final r = await api.fetchUser('123');
switch (r) {
  case NitroOk(:final value): useProfile(value);
  case NitroErr(:final message): showError(message);
}

NitroAnyValue / NitroAnyMap

NitroAnyValue is a dynamic variant type. It bridges arbitrary JSON-like data without a schema:

// Spec:
AnyNativeObject? getMetadata(String key);

// Dart usage:
final meta = module.getMetadata('config');
if (meta is Map<String, NitroAnyValue>) {
  final name = meta['name']?.asString;
  final count = meta['count']?.asInt;
}

Use NitroAnyMap (a typedef for Map<String, NitroAnyValue>) when the native side returns a dictionary with mixed value types. Prefer @HybridRecord when the schema is known — it is significantly faster.


Performance

Call latency (OnePlus 11, Android 14, release)

Bridge Latency vs Method Channel
Method Channel 107.7 µs
Nitrogen (Swift/Kotlin) 2.1 µs 51×
Nitrogen (Direct C++) 1.7 µs 64×

Call latency (macOS, Apple Silicon, profile build)

Measured against a raw dart:ffi leaf call as the theoretical floor — the entire delta is codegen safety (instance registry, error slot, typed marshalling), not JIT/AOT noise:

Bridge Latency vs raw FFI vs Method Channel
Raw FFI (leaf) 0.011 µs 1.0× (floor) 2318× faster
Nitrogen (Direct C++) 0.272 µs 23.6× 98× faster
Nitrogen (Swift) 0.261 µs 22.7× 102× faster
Method Channel 26.7 µs 2318×

At 60 fps, that's ~63,000 Nitrogen calls per frame budget vs ~625 for a method channel — 101× more headroom for per-frame native work (sensors, codecs, game state).

Async overhead (macOS, computeStats/computeStatsNative benchmark cases)

Annotation Latency vs Method Channel Mechanism
Method Channel 26.8 µs
@nitroAsync ~28 µs ~1.05× (slightly slower) Persistent-worker isolate pool dispatch
@nitroNativeAsync ~27 µs ~1.0× (parity) Native Dart_PostCObject_DL, no isolate hop

@nitroAsync's isolate pool was rewritten to use a persistent reply port and least-busy worker scheduling (no per-call ReceivePort allocation) — current overhead is dominated by the inherent cost of an isolate message round-trip, not by pool bookkeeping. @nitroNativeAsync skips that hop entirely because native already owns the async work; use it whenever the native side has its own async infrastructure (coroutines, Swift async, a thread pool). Use @nitroAsync for the opposite case — a blocking native call that just needs to run off the main isolate.

High-bandwidth throughput (1 GB @zeroCopy Uint8List, Android)

Bridge Time Throughput
Method Channel ~117 ms ~854 MB/s
Nitrogen (Swift/Kotlin) ~59 ms ~1,676 MB/s
Nitrogen (Direct C++) ~8 ms ~11,792 MB/s

High-bandwidth throughput (16 MiB @zeroCopy Uint8List, macOS)

Bridge Bandwidth vs Method Channel
Method Channel (copies every byte) 4,623 MB/s
Nitrogen pinned buffer (zero-copy) 31,876 MB/s 6.9× bandwidth

The gap is the copy itself: Method Channel always serializes the buffer; Nitrogen pins the Dart-managed memory and hands native code a direct pointer — no copy, regardless of payload size.


Spec Validation

The generator validates your spec before emitting any code:

Code Severity Condition
E001 Error Map<K, V> where K is not String
E002 Error @nitroAsync on a non-Future return type
E014 Error @NitroVariant with more than 255 cases
W001 Warning Non-nullable int/double/bool named param with no default
W002 Warning Non-nullable @HybridEnum named param with no default
W003 Warning Non-nullable @HybridStruct named param with no default
W004 Warning Stream<T> getter without @NitroStream annotation

Errors stop generation. Pass --fail-on-warn to also stop on warnings (recommended in CI).


Special Notes

dart:isolate no longer needed in spec files (0.5.0+)

Specs that use callbacks (methods with function parameters) previously required import 'dart:isolate' in the spec file because generated .g.dart part files use ReceivePort for the callback-release port.

As of 0.5.0, package:nitro/nitro.dart re-exports ReceivePort and SendPort conditionally (with a web stub). You no longer need this import:

// ❌ Before 0.5.0 — required for callback specs:
import 'dart:isolate';

// ✅ 0.5.0+ — not needed; covered by package:nitro/nitro.dart
import 'package:nitro/nitro.dart';
part 'my_spec.g.dart';

@HybridRecord wire format

[4B payload length][fields in declaration order]

int      → 8 bytes, little-endian int64
double   → 8 bytes, IEEE 754 float64
bool     → 1 byte  (0 = false, 1 = true)
String   → [4B UTF-8 length][UTF-8 bytes]
nullable → [1B null tag][value bytes if present]
list     → [4B count][elements]

Nullable primitive wire format

int?, double?, and bool? use @Packed(1) Dart FFI structs that are binary-compatible with C++ std::optional<T>. No heap allocation on sync paths:

// Generated in C bridge header:
typedef struct __attribute__((packed)) { uint8_t hasValue; int64_t  value; } NitroOptInt64;
typedef struct __attribute__((packed)) { uint8_t hasValue; double   value; } NitroOptFloat64;
typedef struct __attribute__((packed)) { uint8_t hasValue; uint8_t  value; } NitroOptBool;

Known Limitations

ID Limitation Workaround
L6 @HybridStruct and @HybridRecord cannot be returned from a callback (function parameter). Callbacks that need to return complex data should return void and call back via a method. Use a method channel or reverse callback pattern
L7 TypedData? (nullable Uint8List, etc.) is not supported in sync/async params or returns. The two-param C ABI (pointer + length) makes optional transport ambiguous. Use a @HybridRecord wrapper: @HybridRecord() class MaybeBuffer { final Uint8List? data; }
L8 Web (WebNativeImpl.wasm) does not support dart:ffi. ReceivePort and SendPort are replaced by stubs that throw UnsupportedError. Streams and callbacks are unavailable on web. Use package:nitro/nitro.dart's conditional re-exports; guard platform-specific code
L10 Map<String, @HybridStruct> is not supported. Use Map<String, @HybridRecord> instead
L12 @NitroVariant callbacks (function parameters returning a variant) are not supported. Return void from callback; use a reverse method call

CLI Reference

nitrogen              # no args → interactive TUI dashboard
nitrogen init    [--name <name>] [--org <id>] [--platforms <list>]
nitrogen generate [--no-ui] [--fail-on-warn] [--check] [--dry-run] [--targets <list>]
nitrogen link    [--yes] [--no-ui]
nitrogen doctor  [--no-ui]
nitrogen watch   [--no-ui]
nitrogen clean
nitrogen migrate [--dry-run] [--no-backup]
nitrogen update
nitrogen open    [--editor code|antigravity]

Every command accepts --no-ui for CI (auto-enabled when stdout isn't a TTY). See packages/nitrogen_cli/README.md for full flag documentation and CI examples.


License

MIT

About

⚡ Nitro Ecosystem: The zero-overhead, type-safe FFI bridge for Flutter. Achieve sub-microsecond native performance (~1.5µs) with zero boilerplate. Define your API in Dart and let Nitrogen generate type-safe Swift, Kotlin, and C++ bindings automatically. No Method Channels, no manual plumbing, just pure speed.

Topics

Resources

License

Stars

4 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors