nitro_generator 0.1.2
nitro_generator: ^0.1.2 copied to clipboard
Code generator for Nitro Modules (Nitrogen). Converts *.native.dart specs to Dart FFI, Kotlin, Swift, and C++ bindings.
nitro_generator 🔬 #
Code generator for Nitrogen plugins. nitro_generator is a build_runner builder that reads your *.native.dart spec file and generates type-safe Dart FFI bindings, Kotlin JNI bridges, Swift @_cdecl bridges, and C headers — all in one pass.
Requirements #
| Tool | Minimum |
|---|---|
| Dart SDK | 3.3.0+ |
| build_runner | 2.4.0+ |
Installation #
Add to your plugin package's pubspec.yaml (not the end-user app):
dependencies:
nitro: ^0.1.0 # runtime annotations
dev_dependencies:
nitro_generator: ^0.1.0 # code generator
build_runner: ^2.4.0
Tip: Prefer the CLI instead —
nitrogen_clilets you generate without addingbuild_runnerto dev dependencies.
How it works #
my_plugin.native.dart
│
▼ nitro_generator (build_runner / CLI)
│
├── my_plugin.g.dart Dart FFI impl class
├── my_plugin.bridge.g.kt Kotlin JNI bridge + spec interface
├── my_plugin.bridge.g.swift Swift @_cdecl bridge + protocol
└── my_plugin.bridge.g.h C header (extern "C" declarations)
Regenerating is always safe — all generated files begin with:
// Generated by Nitrogen Modules. Do not edit.
Commit them to VCS so consumers without build_runner can build normally.
Usage #
Step 1 — write my_plugin.native.dart #
import 'dart:typed_data';
import 'package:nitro/nitro.dart';
part 'my_plugin.g.dart'; // generated output
// ── Optional: zero-copy data struct ──────────────────────────────────────────
@HybridStruct(zeroCopy: ['data'])
class VideoFrame {
final Uint8List data; // zero-copy — no malloc, no memcpy
final int stride; // bytes per row (auto-detected as byte length)
final int width;
final int height;
final int timestampNs;
VideoFrame(this.data, this.stride, this.width, this.height, this.timestampNs);
}
// ── Optional: native enum ─────────────────────────────────────────────────────
@HybridEnum(startValue: 0)
enum Quality { low, medium, high, ultra }
// ── Module spec ───────────────────────────────────────────────────────────────
@NitroModule(ios: NativeImpl.swift, android: NativeImpl.kotlin)
abstract class MyPlugin extends HybridObject {
static final MyPlugin instance = _MyPluginImpl();
// Synchronous — direct FFI, sub-microsecond
int add(int a, int b);
double multiply(double x, double y);
// Async — dispatched on a background isolate
@nitroAsync
Future<String> processFile(String path, Quality quality);
// Stream — native events pushed to Dart via SendPort (zero overhead)
@NitroStream(backpressure: Backpressure.dropLatest)
Stream<VideoFrame> get frames;
// Properties — get/set via named C symbols
double get zoom;
set zoom(double value);
}
Step 2 — generate #
# With build_runner:
dart run build_runner build --delete-conflicting-outputs
# Or with the CLI (recommended):
dart pub global activate nitrogen_cli
nitrogen generate
Step 3 — look at what was generated #
my_plugin.g.dart — Pure Dart FFI implementation:
// Generated by Nitrogen Modules. Do not edit.
part of 'my_plugin.native.dart';
// dart:ffi struct layout matching the C struct
final class _VideoFrameFfi extends Struct {
external Pointer<Uint8> data;
@Int64() external int stride;
@Int64() external int width;
@Int64() external int height;
@Int64() external int timestampNs;
}
extension _VideoFrameFfiExt on _VideoFrameFfi {
VideoFrame toDart() => VideoFrame(
data.asTypedList(stride), // ← zero-copy backed Uint8List
stride, width, height, timestampNs,
);
}
class _MyPluginImpl extends MyPlugin {
// ... FFI lookupFunction pointers, implementations
@override
Stream<VideoFrame> get frames => NitroRuntime.openStream<VideoFrame>(
register: (port) => _registerFramesPtr(port),
unpack: (rawPtr) => Pointer<_VideoFrameFfi>.fromAddress(rawPtr).ref.toDart(),
release: (port) => _releaseFramesPtr(port),
backpressure: Backpressure.dropLatest,
);
}
my_plugin.bridge.g.kt — Kotlin contract + JNI bridge:
// Generated by Nitrogen Modules. Do not edit.
@Keep
data class VideoFrame(
val data: java.nio.ByteBuffer, // ← DirectByteBuffer, zero-copy
val stride: Long, val width: Long, val height: Long, val timestampNs: Long
)
interface HybridMyPluginSpec {
fun add(a: Long, b: Long): Long
suspend fun processFile(path: String, quality: Int): String
val frames: Flow<VideoFrame>
var zoom: Double
}
@Keep object MyPluginJniBridge {
fun register(impl: HybridMyPluginSpec) { ... }
// JNI @JvmStatic methods, Flow collection coroutines, Job cancellation
}
my_plugin.bridge.g.swift — Swift protocol + C-bridge stubs:
// Generated by Nitrogen Modules. Do not edit.
import Foundation
import Combine
public struct VideoFrame {
public var data: UnsafeMutablePointer<UInt8>? // ← zero-copy pointer
public var stride: Int64
public var width: Int64; public var height: Int64; public var timestampNs: Int64
}
public protocol HybridMyPluginProtocol: AnyObject {
func add(a: Int64, b: Int64) -> Int64
func processFile(path: String, quality: Int64) async throws -> String
var frames: AnyPublisher<VideoFrame, Never> { get }
var zoom: Double { get set }
}
// Registry — pure Swift, no @objc / NSObject needed
public class MyPluginRegistry {
public static var impl: HybridMyPluginProtocol?
public static func register(_ impl: HybridMyPluginProtocol) { ... }
}
// C-bridge stubs — exported as C symbols, called by the generated .cpp shim
@_cdecl("_call_add")
public func _call_add(_ a: Int64, _ b: Int64) -> Int64 {
return MyPluginRegistry.impl?.add(a: a, b: b) ?? 0
}
// Async functions use DispatchSemaphore + Task.detached to bridge async → sync C ABI
@_cdecl("_call_processFile")
public func _call_processFile(_ path: String, _ quality: Int64) -> String {
guard let impl = MyPluginRegistry.impl else { return "" }
let sema = DispatchSemaphore(value: 0)
var result: String? = nil
Task.detached { result = try? await impl.processFile(path: path, quality: quality); sema.signal() }
sema.wait()
return result ?? ""
}
What @HybridStruct generates #
For a zero-copy field like Uint8List data with a sibling int stride field, the generator automatically detects stride as the byte-length source and emits:
data.asTypedList(stride), // zero-copy; lifetime = this struct
Auto-detected length field names: length, size, stride, bytelength, bytelen, len
If no matching sibling exists, it emits a TODO comment so you can fill in the correct expression.
Spec validator rules #
The generator validates your spec before generating and will exit with helpful errors:
| Level | Rule |
|---|---|
| ERROR | @HybridStruct field type is not int/double/bool/String/Uint8List |
| ERROR | @nitroAsync method does not return Future<T> |
| ERROR | @NitroStream getter does not return Stream<T> |
| ERROR | Class does not extend HybridObject |
| ERROR | zeroCopy field name in @HybridStruct does not match any declared field |
| WARNING | Large @HybridStruct returned synchronously — consider @nitroAsync |
| WARNING | High-frequency stream with String item type — prefer Uint8List |
File naming convention #
| File | Author | Notes |
|---|---|---|
Foo.native.dart |
Plugin author | Spec — triggers codegen |
Foo.g.dart |
Generated | Dart FFI impl — commit to VCS |
Foo.bridge.g.kt |
Generated | Kotlin stub — commit to VCS |
Foo.bridge.g.swift |
Generated | Swift stub — commit to VCS |
Foo.bridge.g.h |
Generated | C header — commit to VCS |
FooImpl.kt |
Plugin author | Real Kotlin implementation |
FooImpl.swift |
Plugin author | Real Swift implementation |
Related packages #
- nitro — Runtime (base classes, annotations, runtime helpers)
- nitrogen_cli — CLI tool (generate / init / doctor)