nitro_generator
A high-performance code generator for Nitro Modules (Nitrogen). Converts .native.dart interface specs into optimized native bindings for Android, iOS, and direct C++.
Features
- Three implementation paths: Swift (
@_cdecl), Kotlin (JNI), or direct C++ virtual dispatch — chosen per-module via@NitroModule(ios:, android:). - NativeImpl.cpp: generates an abstract C++ interface (
HybridX), a direct virtual-dispatch bridge (no JNI/Swift), GoogleMock stubs, and a test starter — everything needed to implement and test in pure C++. - Type-safe: strict Dart-to-native type mapping with validation before generation.
- Zero-copy structs:
@HybridStructpasses packed C structs directly across the FFI boundary. - Binary records:
@HybridRecorduses a compact little-endian binary protocol (no JSON) for complex infrequent data. - Async:
@nitroAsyncoffloads blocking native calls to a background thread. - Streams:
@NitroStreamwith configurable backpressure strategies; C++ modules emit viaDart_PostCObject_DLfrom any thread. - Default param literals: Named params with default values (
{int timeout = 30},{MyEnum quality = MyEnum.normal}) are preserved in the generated Dart FFI signature — no boilerplate wrapper needed. - Cross-file type sharing:
@HybridEnum,@HybridStruct, and@HybridRecordtypes defined in one.native.dartcan be imported and used in another; the generator tracks which declarations belong to which file and emits correct#includedirectives in the C header. - Source-map comments: Each generated method in Swift, Kotlin, and C++ includes a
// source: file.native.dart:42comment pointing back to the originating spec line. - Pre-generation validation: The generator reports errors (E) and warnings (W) before emitting any code, so invalid specs surface early with clear messages.
Usage
- Define your API in a
.native.dartfile. - Choose the implementation path:
// Swift/Kotlin (platform-specific APIs)
@NitroModule(lib: 'camera', ios: NativeImpl.swift, android: NativeImpl.kotlin)
abstract class Camera extends HybridObject { ... }
// Direct C++ (shared logic, max performance)
@NitroModule(lib: 'math', ios: NativeImpl.cpp, android: NativeImpl.cpp)
abstract class Math extends HybridObject { ... }
- Run the generator:
flutter pub run build_runner build
# or via the CLI:
nitrogen generate
Generated Outputs
Swift/Kotlin path
| File | Description |
|---|---|
lib/src/*.g.dart |
Dart FFI bindings |
lib/src/generated/kotlin/*.bridge.g.kt |
Kotlin JNI bridge + HybridXxxSpec interface |
lib/src/generated/swift/*.bridge.g.swift |
Swift @_cdecl bridge + HybridXxxProtocol |
lib/src/generated/cpp/*.bridge.g.h |
C header (extern "C" declarations) |
lib/src/generated/cpp/*.bridge.g.cpp |
C++ JNI & Apple bridge |
lib/src/generated/cmake/*.CMakeLists.g.txt |
CMake include fragment |
NativeImpl.cpp path (additional outputs)
| File | Description |
|---|---|
lib/src/generated/cpp/*.native.g.h |
Abstract HybridX C++ class — subclass and implement this |
lib/src/generated/cpp/test/*.mock.g.h |
GoogleMock MockX class for unit tests |
lib/src/generated/cpp/test/*.test.g.cpp |
Test starter with smoke test + main() |
For cpp modules, .bridge.g.cpp uses direct virtual dispatch (g_impl->method()) instead of JNI/Swift, and .bridge.g.kt / .bridge.g.swift contain a "Not applicable" placeholder.
NativeImpl.cpp — Quick Start
// spec
@NitroModule(lib: 'math', ios: NativeImpl.cpp, android: NativeImpl.cpp)
abstract class Math extends HybridObject {
double add(double a, double b);
String greet(String name);
int get precision;
set precision(int value);
}
After nitrogen generate, you get math.native.g.h:
class HybridMath {
public:
virtual ~HybridMath() = default;
virtual double add(double a, double b) = 0;
virtual std::string greet(const std::string& name) = 0;
virtual int64_t get_precision() const = 0;
virtual void set_precision(int64_t value) = 0;
protected:
HybridMath() = default;
};
void math_register_impl(HybridMath* impl);
HybridMath* math_get_impl(void);
Your implementation:
#include "math.native.g.h"
class HybridMathImpl : public HybridMath {
public:
double add(double a, double b) override { return a + b; }
std::string greet(const std::string& name) override { return "Hello, " + name; }
int64_t get_precision() const override { return precision_; }
void set_precision(int64_t v) override { precision_ = v; }
private:
int64_t precision_ = 6;
};
static HybridMathImpl g_math;
// at startup: math_register_impl(&g_math);
Unit test with the generated mock:
#include "math.mock.g.h"
TEST(MathTest, Add) {
MockMath mock;
math_register_impl(&mock);
EXPECT_CALL(mock, add(::testing::_, ::testing::_)).WillOnce(::testing::Return(3.0));
EXPECT_EQ(math_add(1.0, 2.0), 3.0);
math_register_impl(nullptr);
}
Default Param Literals
Named parameters with default values are preserved in generated Dart FFI bindings:
// .native.dart
@HybridEnum()
enum PrintQuality { draft, normal, high }
abstract class Printer {
void print(String text, {PrintQuality quality = PrintQuality.normal, int copies = 1});
}
Generated Dart FFI:
// .g.dart — callers get the default, no wrapper needed
void print(String text, {PrintQuality quality = PrintQuality.normal, int copies = 1}) { ... }
Enum, int, double, bool, and String default literals are all supported.
Cross-File Type Sharing
Types declared in one spec can be used by another:
// enums.native.dart — type-only file (no @NitroModule class)
@HybridEnum()
enum DeviceStatus { idle, active, error }
@HybridStruct()
class Reading { final double value; final int timestamp; }
// sensor.native.dart
import 'enums.native.dart'; // import the type file
@NitroModule(lib: 'sensor', ios: NativeImpl.swift, android: NativeImpl.kotlin)
abstract class Sensor {
DeviceStatus getStatus();
Reading getReading();
}
The generator:
- Emits a type-only Swift/Kotlin/C++ output for
enums.native.dart(enums + structs, no bridge scaffolding). - Emits
#include "enums.bridge.g.h"insensor.bridge.g.hso C++ sees both files. - Avoids re-declaring imported types in
sensor's native outputs.
Spec Validation
Before emitting any code, the generator validates the spec and reports issues with clear codes:
| Code | Severity | Condition |
|---|---|---|
| E001 | Error | Map<K, V> where K is not String — only Map<String, V> is supported |
| E002 | Error | isAsync: true on a function whose return type is not void or Future<T> |
| W001 | Warning | Non-nullable int/double/bool named param with no defaultLiteral — callers must always pass it |
| W002 | Warning | Non-nullable @HybridEnum named param with no default |
| W003 | Warning | Non-nullable @HybridStruct named param with no default |
| W004 | Warning | Stream<T> return declared without @NitroStream annotation |
Errors (E*) stop generation. Warnings (W*) produce output but flag the spec for review. Use nitrogen generate --fail-on-warn to treat warnings as errors in CI.
Source-Map Comments
Every generated method includes a comment linking back to its spec origin:
// source: sensor.native.dart:12
public func getStatus() -> DeviceStatus { ... }
// source: sensor.native.dart:12
fun getStatus(): DeviceStatus
// source: sensor.native.dart:12
virtual DeviceStatus get_status() = 0;
This makes it easy to navigate from generated native code back to the Dart spec.
Type Mapping (C++ direct path)
| Dart | C++ method signature | C bridge type |
|---|---|---|
int |
int64_t |
int64_t |
double |
double |
double |
bool |
bool |
int8_t |
String |
std::string / const std::string& |
const char* |
Uint8List |
const uint8_t* buf, size_t buf_length |
uint8_t*, int64_t |
MyEnum |
MyEnum (C enum) |
int64_t |
MyStruct |
const MyStruct& (param) / MyStruct (return) |
void* |
MyRecord |
NitroCppBuffer |
void*, int64_t |
Stream<T> |
emit_name(T item) helper |
register/release port |
Documentation
For full documentation and getting started guides, visit nitro.shreeman.dev.
License
MIT