nitro_generator 0.5.15
nitro_generator: ^0.5.15 copied to clipboard
Code generator for Nitro Modules (Nitrogen). Converts *.native.dart specs to Dart FFI, Kotlin, Swift, and C++ bindings.
0.5.15 #
- Fixed: generated Swift record
fromReaderinitializers no longer emit a trailing comma in the argument list (#22) — trailing commas in argument lists are Swift 6.1+ syntax (SE-0439), so every record constructor failed to parse on Xcode ≤ 16.2 toolchains (including GitHub'smacos-14runners) with "Unexpected ',' separator", while newer local toolchains accepted it silently. The two emission sites (recordfromReaderand the struct-embedded-in-record RecordExt) now join arguments with commas instead of suffixing every line. A new cross-emitter syntax invariant test generates a maximal spec (records, variants, streams, async flavors, callbacks, maps, handles) and asserts NO comma-before-)anywhere in generated Swift or C++ output — locking out the whole bug class for future emitters, not just these two sites.
0.5.14 #
0.5.13 #
The nitro_webgpu feedback batch (#13, #14, #17, #18, #19) — every item below came out of a real wgpu-native binding project.
- Fixed: nullable native-async results now accept
Dart_CObject_kNull(#17) — a C++ impl resolving aFuture<T?>with kNull crashed the generated unpack withtype 'Null' is not a subtype of type 'int'; only the undocumented kInt64-0 convention worked. Every nullable unpack (record, variant, struct, enum, custom type,bool?/int?/double?/uint64?/DateTime?,AnyNativeObject?) now decodes BOTH kNull and 0 to Dartnull, and a non-nullable unpack turns a posted null into a descriptiveStateErrornaming the method and the fix instead of the opaque cast error. Both conventions are documented in the generated.impl.g.cppstarter comment anddoc/advanced/async.md. - Fixed: all-C++ plugins emitted an uncompilable Swift record bridge (#14) — the cpp-module Swift bridge emitted
public struct X: NitroEncodablerecords whose codec types (NitroEncodable,NitroRecordWriter,NitroRecordReader) were only ever declared by a Swift-impl sibling's bridge; with zero Swift-impl modules in the plugin, every Apple build broke on undeclared types. The cpp-module bridge is now SELF-CONTAINED (emits the full codec boilerplate); when a Swift-impl sibling shares the module, nitrogen's existing sync dedup pass keeps exactly one copy of each shared declaration. - New:
@NitroOwned(release: 'wgpuBufferRelease')custom release symbol (#13) — the<symbol>_releasethunk calls the namedextern "C"function instead offree()(forward-declared with the handle-pointer signature, deduped when several methods share one release function). The wrapper symbol, DartNativeFinalizerbindings, and header declaration are unchanged — fully backward compatible, and the validator rejects non-identifier symbols at generation time. - New:
Future<NativeHandle<T>>on the native-async path (#18) —@nitroNativeAsyncfactories can now return handles: the native side posts the raw pointer address as kInt64 (KotlinLong; SwiftUnsafeMutableRawPointer?; C++(int64_t)(uintptr_t)handle), and Dart attaches theNativeFinalizer+ release callback the moment an@NitroOwnedhandle arrives — ownership transfer is atomic, with no un-owned window. Composes withrelease:above:@nitroNativeAsync @NitroOwned(release: 'wgpuAdapterRelease') Future<NativeHandle<Void>> requestAdapter(...)is now one annotation pair instead of ~40 lines of hand-rolled address plumbing. Previously this silently generated broken code on all three layers (Dart cast TypeError, Kotlin discard-and-post-null, uncompilable Swift). - New:
@mainThreaddispatch (#19) — Kotlin hops throughDispatchers.Main.immediate(sync:runBlocking(...)around the impl call; async paths:withContext(...)inside the existing coroutine, composing with@NitroAsync(timeout:)); Swift wraps the whole sync dispatch body in one_nitroMainSynchop (Thread.isMainThreadguard +DispatchQueue.main.sync) and marks async protocol requirements@MainActorso conforming impls infer main-actor isolation and their bodies genuinely run on main. Re-entrancy safe everywhere — already-on-main callers execute inline, no self-deadlock. New validator warningMAIN_THREAD_NO_EFFECTwhen a C++ impl platform can't honor the annotation. - Changed: the Swift bridge's
import <Module>Cpp(emitted for@nitroNativeAsync) is now wrapped in#if canImport(...)— resolves against the module's own SPM C++ target created bynitrogen link0.5.13 (issue #15), and keeps the same generated file compiling under CocoaPods where no such Swift module exists.
0.5.12 #
Two fixes for the zero-copy TypedData paths, both in under-exercised configurations:
- Fixed: the pure-C++-everywhere bridge never DEFINED
<lib>_release_typed_data_return—CppHeaderGeneratordeclares the symbol whenever a spec has@zeroCopyTypedData returns, and the mixed-platform bridge (JNI / Apple shim / desktop sections) each emit a definition, but the all-platforms-C++ direct path (cpp_direct_emitter) emitted none. Generated Dart binds the symbol for itsNativeFinalizerat startup, so any all-C++ module with a zero-copy return failed at runtime with symbol-not-found. The direct path now emits the same envelope-plus-payload-freeing body as the other three sections (including 0.5.11's payload-release and empty-buffer-sentinel handling). - Fixed: Swift struct conversions dropped the synthesized
<field>Lengthfor@zeroCopyTypedData fields — thefromSwift/toSwiftbridging for a@HybridStructonly wired the synthesized length parameter for non-zero-copy TypedData fields; zero-copy fields lost their length across the Swift boundary (wrong view length on the other side). Both directions now carry it:fromSwiftreads the public struct's<field>Length,toSwiftpasses it through.
0.5.11 #
- Locked in: platform-mix independence and no-duplicate-definition guarantees — no generator changes; a 21-test matrix over 10 platform combinations now pins two user-facing invariants against regression: (1) any subset of platforms can use the C++ backend without affecting the others — e.g. switching only
macos:toNativeImpl.cppleaves the Kotlin bridge byte-identical, keeps the iOS Swift bridge intact, and routes macOS through C++ dispatch behindTARGET_OS_OSX(Dart identical modulo the intentional lockstep checksum); and (2) every generated class/struct/enum is defined exactly once per artifact and referenced elsewhere — C structs behindNITRO_STRUCT_<NAME>_DEFINEDguards, guard-wrappedNitroError/NitroOpt*for multi-TU inclusion, the bridge#include-ing the interface header rather than re-emitting types, and imported types (importedTypeFiles/isImported) skipped by every definition emitter so multi-spec plugins keep one canonical definition per user-facing class. - Ecosystem sync — Released alongside
nitrogen_cli0.5.11's desktop developer-experience fixes (#10, #11, #12) — see its changelog.
0.5.10 #
Two memory-safety root causes found by the first CI run whose desktop builds actually executed (earlier runs failed to compile) — one Windows-only heap corruption, one wire-format corruption on record/variant streams:
-
Fixed: on Windows, every native-owned pointer freed by generated Dart corrupted the heap — generated code freed native returns (strings, record/variant/list/map blobs, struct copies, callback args, posted async results, stream items) with package:ffi's
malloc.free, which on Windows binds toCoTaskMemFree, while the native side allocates with C-runtimemalloc/strdup. Mismatched allocators are undefined behavior; Windows CI died at exactly its 15th test —echoString(''), the first call that frees a native pointer from Dart (the 14 preceding int/double/bool tests return scalars). POSIX platforms never noticed becausemalloc.freeis plainfree()there. The generated bridge now unconditionally exports<lib>_nitro_free(void*)(a plain C-runtimefree) in every platform section (JNI, Apple shim, desktop C++, pure-C++), and generated Dart binds it once per impl class (_nitroFree) and uses it for all native-owned frees — includingLazyRecordList's finalizer, structfreeFields(...)(signature now takes the free function),toDartStringFreedBy(_nitroFree)(replacingtoDartStringWithFree()in generated output), the S8 error-slot string fields (throwIfOutParamError(_nitroErr, nativeFree: _nitroFree)), and the struct proxy'stoDartAndRelease()(which additionally used to leak the struct's string/nested fields by freeing only the shell instead of calling the generated release symbol). Dart-allocated memory (arena params,calloc'd error slots, decode scratch buffers) stays with package:ffi's allocators — the rule is simply that memory is freed by the allocator that produced it. -
Fixed: desktop C++ record/variant stream emits double-prefixed the wire payload and leaked the source block —
emit_<stream>()forStream<@HybridRecord>/Stream<@NitroVariant>items expected a non-owning payload view and re-wrapped it in a fresh[4B len]prefix, but the natural impl-side call isemit_x(record.toNativeBuffer())— a self-describing, already-prefixed heap block (the exact convention record returns use). The result on Linux CI was a deterministicFormatException: Missing extension byte (at offset 33)on everyconfigStreamevent (Dart'sreadStringconsumed the inner length prefix as string bytes and choked onthreshold = 0.5's0xE0 0x3FIEEE-754 tail), a failed §30.1, and a follow-on stream test that hung the job for 33 minutes — plus a per-emit leak of thetoNativeBuffer()block even when decoding had worked. The emit contract is now ownership transfer, identical to record returns: passrecord.toNativeBuffer()(ornitro_<Variant>_to_native(...)); the bridge posts the block's address directly (zero copies) and Dart frees it via<lib>_nitro_free; the emit itself frees the block when the port is closed or the post fails, and postskNullfor a{nullptr, 0}item on nullable streams. Verified by a standaloneclang++ -fsanitize=address,undefinedharness replaying the exact §30.1 scenario (seed"printer",threshold: 0.5) through the new emit and a byte-faithful reimplementation of Dart's decode — round-trips exactly, no leaks on any path. Breaking only if your hand-written desktop C++ impl passed awriter.toBuffer()view to anemit_*helper — switch it towriter.toNativeBuffer(); the generated interface header, starter.impl.g.cpp, andNitroRecordWriterdoc comments now all state the ownership rule explicitly. -
Dependency floor:
ffi: ^2.2.0. -
Fixed: on Windows, String/record/variant callback returns corrupted the heap from the other direction — the mirror image of the
<lib>_nitro_freefix: Dart callback trampolines allocated their return values with package:ffi (toNativeUtf8()/toNative(malloc)— CoTaskMemAlloc on Windows) while the native wrapper releases them with C-runtimefree(). On Windows CI this froze the app solid at the first String-returning callback (§32.2) — reproduced at the identical test in two consecutive runs, and immune to test timeouts because the whole app hangs, not one test. The bridge now also exports<lib>_nitro_alloc(size_t)(plain C-runtimemalloc), and generated trampolines allocate these returns through the newNitroNativeAllocator(in thenitropackage) so both directions obey the one rule: memory is released by the runtime that allocated its allocator pair.
Three more desktop C++ fixes from the very next CI run — the first ever to get past §30.1 on Linux, which immediately exposed three never-executed paths:
- Fixed: desktop record/variant callback arguments had the same double-prefix bug as stream emits — the generated
std::functionwrapper copy-wrapped the impl'sNitroCppBufferin a fresh[4B len]prefix, but the natural impl-side call ishandler(nitro_<Variant>_to_native(...))— an already-prefixed heap block. Dart then read the inner length byte as the variant tag (ArgumentError: Unknown TcEvent tag: 17), and the impl's block leaked on every invocation. The wrapper now forwardsitem.datastraight through (ownership transfers to Dart, freed via<lib>_nitro_free; a{nullptr,0}item reaches a nullable callback param asnull) — one consistent rule everywhere: everyNitroCppBuffercrossing into Dart is a self-describing heap block whose ownership transfers. - Fixed: desktop streams supported only ONE subscriber — the bridge stored a single
int64_tport per stream, so a second concurrent Dart subscriber overwrote the first, which then received nothing ("multiple subscribers independent" got[]). Kotlin/Swift have always kept a port per subscription. The desktop sections (mixed-platform and pure-C++) now share a mutex-guarded per-stream port registry: register appends, release removes, and emits fan out to every subscriber — record/variant/struct items post a fresh heap copy per port (each freed independently by Dart), scalar items re-post the sameDart_CObject, and a port whose post fails is dropped from the registry. - Covered by 16 new/updated tests (
nitro_free_and_stream_emit_test.dart,benchmark_spec_test.dart), plus standaloneclang++ -fsanitize=address,undefinedround-trip harnesses for the emit and map wire formats.
A cluster of desktop C-bridge (Windows/Linux NativeImpl.cpp) dispatch
fixes, all found via real builds rather than dart test — most via
#9 and a real
Windows/Linux CI run of nitro_type_coverage that followed it. All are
pre-existing bugs in the desktop-C++ dispatch generator, unrelated to
0.5.9's native-async error-propagation work. No breaking changes —
regenerate your plugin to pick these up.
- Fixed: two desktop C-bridge bugs affecting
@NitroResult<record>and optional record/variant params:@NitroResult<record>(and enum/variant) desktop dispatch didn't compile: the interface header correctly declares a record-returning@NitroResultmethod asvirtual NitroCppBuffer fn(...) = 0;(matching the general "records bridge asNitroCppBuffer" convention), but the dispatch'sisResultbranch only special-caseddouble/int/booland fell through tostd::string _val = g_impl->fn(...)for everything else — a genuineno viable conversion from 'NitroCppBuffer' to 'std::string'compile error. String@NitroResulthappened to work by coincidence (the fallback'sstd::stringassumption is only correct for that one case), which is why this went unnoticed. Record/variant returns now consume the impl's already-encodedNitroCppBufferdirectly (prepending the[1B tag=0]byte to the existing[4B len][payload]block rather than re-encoding throughNitroRecordWriter, which record/variant impls don't use); enum returns now correctly write theint64_trawValue.- Optional record/variant params on
@nitroNativeAsyncdesktop dispatch could segfault: an omitted optional record param (e.g.PrintSettings?) arrives asnullptrfrom Dart, but the native-async param-conversion branch unconditionally did*(int32_t*)ptrto read the length prefix — a null-pointer deref before the impl ever runs. The sibling sync-path param branch in the same file already null-checks correctly (NitroCppBuffer{ nullptr, 0 }for the null case); only the native-async branch — in bothcpp_bridge_generator.dart's mixed-platform dispatch andcpp_direct_emitter.dart's pure-C++ dispatch — was missing the guard. - Covered by 5 new tests (
nitro_result_test.dart,native_async_test.dart) plus standaloneclang++ -fsanitize=addresscompiles of the exact generated C++ snippets, confirming both the wire-format round-trip and the null-guard are memory-safe.
- Fixed:
@nitroNativeAsyncdesktop dispatch never wrappedList<T>/Map<K,V>/callback params correctly — found the momentnitro_type_coveragefirst got a real Windows/Linux desktop-C++ implementation and its CI actually compiled these code paths for the first time (no existing unit test exercised list/map/callback native-async params together with a Windows/LinuxNativeImpl.cpptarget — every prior fixture used either the JNI or pure-C++-everywhere path). The param-conversion loop's record/variant-detection check (recordNames.contains(base) || variantNames.contains(base)) only matched bare record/variant type names, soList<TcConfig>,Map<String,int>, etc. fell through to a raw-pointer passthrough instead of being wrapped inNitroCppBuffer{ ptr+4, len }— a hardcannot convert argument 1 from 'void *' to 'NitroCppBuffer'compile error. Widened top.type.isRecord || variantNames.contains(base), matching the already-correct sync-path condition (p.type.isRecordis a broader, wire-format-driven flag that's also true forList/Mapregardless of item/value type). Callback params on native-async desktop dispatch had no handling at all (no declared type, nostd::functionwrapping) incpp_bridge_generator.dart's mixed-platform path — added, reusing the same_callbackParamToC/_emitDesktopCallbackWrapperhelpers the working sync path already uses. Fixed in bothcpp_bridge_generator.dart(mixed-platform dispatch) andcpp_direct_emitter.dart(pure-C++ dispatch, list/map only — its callback handling was already correct). - Fixed:
@NitroCustomTypeparams on desktop C-bridge dispatch declared a different C type than the header — found via an audit prompted by the bug above, not a real crash report:cpp_header_generator.dart's.hdeclaration explicitly special-cases@NitroCustomTypeparams asconst uint8_t*, but neithercpp_bridge_generator.dart(sync or native-async) norcpp_direct_emitter.darthad an equivalent check — both fell through to the generic_typeToCdefault ofvoid*. Declaring the sameextern "C"symbol with two different parameter types is a hard MSVC/Clang "conflicting types" compile error, identical in kind to the callback-param bug fixed in 0.5.9. Fixed at all four sites (sync + native-async, both generator files). - Covered by 11 new tests across
native_async_test.dartandnitro_custom_type_test.dart; all confirmed to fail without the fix and pass with it.
Real error propagation for @nitroNativeAsync — previously a thrown native
exception was silently discarded and the Future resolved successfully with
null (invisible for Future<void> methods entirely: expect(call(), throwsA(...)) would fail because the call never threw at all — the
native-async unpack for void is literally (_) {}, ignoring the posted
value). Breaking for hand-authored C++ desktop (NativeImpl.cpp)
native-async implementations only — see the entry below; Kotlin/Swift/Dart
callers need no source changes, just a regenerate.
@nitroNativeAsyncnow propagates thrown exceptions to Dart as a realHybridException, instead of silently discarding them — previously, the Kotlin trampoline'scatch (_: Throwable) { postNullToPort(dartPort) }and Swift's per-branchtry? await impl.fn(...)both swallowed a thrown exception and posted a "successful" null/default value. Mirrors theNitroError*out-param mechanism sync/@nitroAsyncalready use (seeNitroRuntime.throwIfOutParamError), but with one difference: native-async calls aren't serialized (several can be in flight concurrently on the same instance), so Dart now allocates a freshNitroErrorFfistruct per call — via the newNitroRuntime.throwIfOutParamErrorAndFree(in thenitropackage) — rather than reusing sync's one instance-owned slot. The struct's address is threaded through every native-async signature as a newNitroError* _nitro_errparam, right before the existing trailingdart_portparam.- Kotlin: the shared catch block (and the impl-not-found early-exit) now call a new JNI-exported
reportNativeAsyncError(errPtr, name, message)before posting, whichstrdups the exception's class name/message into the struct. - Swift: every native-async return branch's
try? await impl.fn(...)(+ its type-specific?? defaultcollapsing a throw into a fake success value) is nowtry await impl.fn(...)inside a shareddo { ... } catch { ... }wrapping the whole dispatch chain — a throw now writes into the error struct via the newerrPtr: Int64@_cdeclparam and postsDart_CObject_kNull, instead of silently substituting a default value. - C++ desktop-direct (
NativeImpl.cppon Windows/Linux, and macOS-via-C++) — breaking: the generated wrapper now takes aNitroError* _nitro_errparam and forwards it to your implementation method (after your declared params, beforedart_port), and wraps the synchronous portion of the call intry/catchso at least setup-time exceptions are caught automatically. The framework doesn't own your async completion thread on this path, though — truly-async errors still require your own background code to populate_nitro_errbefore posting, exactly likedart_portposting is already your responsibility. Existing hand-written/starter-generated native-async C++ impl methods need a trailingNitroError*parameter added; regenerate to pick up the updated starter template. - Covered by 19 new tests across
native_async_test.dart(Kotlin/Swift/C++-JNI/C++-desktop-direct/Apple-C++-direct/Dart-FFI) plus a new unit-test group forNitroRuntime.throwIfOutParamErrorAndFreein thenitropackage. Two pre-existings8_out_param_test.darttests that asserted native-async does not receive aNitroError*were inverted — that was the exact gap this closes. Verified end-to-end on Android, iOS, and macOS vianitro_type_coverage's§69integration test (athrowNativeNativeAsyncmethod whose impl throws now correctly rejects withHybridExceptionon all three platforms, matching the original bug report).
- Kotlin: the shared catch block (and the impl-not-found early-exit) now call a new JNI-exported
0.5.8 #
Closes every @nitroNativeAsync gap left deferred by 0.5.7 — Map<String,V>/
NitroAnyMap params on both platforms, bare @HybridStruct returns on
Kotlin, and NitroAnyMap entirely on Swift (previously unimplemented on
any dispatch path, not just native-async). No breaking changes —
regenerate your plugin to pick these up.
- Confirmed, not fixed: bare
@HybridStructparams on Kotlin already worked correctly with zero changes needed — the JNI bridge already delivers a fully-typed Kotlin object at the_callboundary for structs (unlike records/variants/enums, which arrive as rawByteArray/Long). Added regression tests to lock this in, since it was easy to miss. Map<String,V>/NitroAnyMapnative-async params (Kotlin + Swift): ported the sync path's per-value-type decode (int/double/bool/enum/record/variant/string) into two new native-async-specific decode helpers per platform, with per-param-namespaced temp variables so multiple map params on one function don't collide. Wired into the same_buildCallParams/callArgs-closure substitution point 0.5.7's param fixes used.- Bare
@HybridStructnative-async returns on Kotlin: previously had no wire format at all (structs are plain Kotlin data classes at the JNI boundary, notByteArray-encoded, so nothing in the generic dispatch chain applied). Added a per-struct-typepost${Struct}ToPortJNI helper (declared only for structs actually used in a native-async return position) that reuses the existingpack_${Struct}_from_jniconversion — the same one the sync-return/stream/callback paths already use — and posts the malloc'd result, with the address-0-for-null convention consistent with every other pointer-backed native-async return. The Dart-side unpack for this exact wire shape (Pointer<${Struct}Ffi>.fromAddress(...)) already existed and needed no changes. NitroAnyMapon Swift — new feature, not a native-async-specific fix:isAnyMapwas never referenced anywhere in the Swift emitter before this — no encode, no decode, on sync,@nitroAsync, or native-async. Added a new recursive binary codec (_nitroEncodeAnyMapBinary/_nitroDecodeAnyMapBinary+_nitroWriteAnyValue/_nitroReadAnyValue) matching the exact wire contract Dart'sNitroAnyValueand Kotlin'sNitroAnyMapCodecalready use (tags 0–6: null/bool/int64/float64/string/list/object), and wired it into all three dispatch paths' return handling plus native-async param decode. Also fixedSwiftTypeMapper.cdeclParamType/cdeclReturnType, which had noisAnyMapcase and fell back to the protocol-levelAnytype — not C-ABI-compatible, so any@_cdeclfunction with an AnyMap param/return wouldn't have compiled even with the codec in place. In the process, found and fixed a latent bug in the Dart FFI generator's native-async unpack that affected AnyMap on both platforms (not just this Swift work):isAnyMapis a separate flag fromisRecord(spec_extractor never sets both), so_nativeAsyncUnpack'sisRecordcheck never matched it and any@nitroNativeAsyncmethod returningNitroAnyMap— Kotlin included — would have throwntype 'int' is not a subtype of type 'NitroAnyMap'the first time it was actually called, despite generating without error.NSNull()is used as Swift's in-memory null marker instead ofnil, sincedict[k] = nildeletes the key in a[String: Any]dictionary rather than storing a null value (Kotlin'sMap<String, Any?>has no equivalent gotcha).- Covered by 3 new Kotlin unit tests, 1 new C++ bridge test, and Swift/Dart-FFI tests for the AnyMap param, return, and codec-presence assertions, all in
native_async_test.dart. - Three more bugs found end-to-end building
nitro_type_coveragefor all three platforms with this batch of fixes wired in (Android/iOS/macOS,§68in the example app's integration test) — none reachable fromdart testalone, same lesson as 0.5.7's real-device pass:spec_validator.dart's E010 "unknown type" check excludedisRecord/isPointer/isNativeHandlebut notisAnyMap, so any function usingNitroAnyMap— return, param, sync or async, not just native-async — failed generation outright withunknown return type "NitroAnyMap". This is likely why NitroAnyMap saw so little real usage that the Swift gap above went unnoticed for as long as it did.KotlinTypeMapper.type()had noisAnyMapcase (onlyretType()did), so aNitroAnyMapparameter's interface type was the genericAny?fallback while the return type was the more preciseMap<String, Any?>— an avoidable asymmetry now fixed to match.- The C++ bridge's
_typeToCmatchedMap<String,T>by name prefix for theuint8_t*C parameter type but not the literal nameNitroAnyMap, so it fell to the genericvoid*default — while a separately-generated header declaration for the same parameter already correctly useduint8_t*, producing a C++ "conflicting types" compile error the moment a realNitroAnyMapnative-async parameter was compiled.
0.5.7 #
Callback NativeCallable memory-leak fix. No breaking changes — regenerate
your plugin (dart run build_runner build) to pick it up; only the
generator's output changes, plus one small runtime addition
(NitroRuntime.deferredClose, used internally by generated code — not
something you call directly).
- Fixed: every callback-typed parameter leaked a
NativeCallableon every re-registration (Android, iOS, and desktop C++) — a callback setter (e.g.module.onDeviceFound((event) { ... })) cached its native callback keyed by(paramName, closure); since idiomatic Flutter code almost always passes a fresh closure literal, the cache key never matched a previous entry, so a newNativeCallablewas allocated and never released on every single call. The generated cache is now a per-(methodName.paramName)slot: re-registering replaces the slot and closes the previousNativeCallablevia the newNitroRuntime.deferredCloseruntime helper (deferred to a microtask, after native has synchronously switched to the new function pointer). The old Kotlin/JNI-only_release_$paramNamemechanism — declared but never actually invoked by any generated code — has been removed rather than completed on Swift/direct-C++, since replace-on-reassign leaves no gap for it to fill. A newE016validation error rejects a callback param on a plain@NitroAsyncmethod (the registering call would run on a different isolate, breaking the ordering guaranteedeferredCloserelies on);@NitroNativeAsyncand sync methods are unaffected. Covered by rewrittencallback_release_test.dartand updatedcallback_type_test.dartassertions. benchmarkpackage: added a@nitroNativeAsyncbenchmark case and a CI regression gate for both async paths — there was previously no benchmark coverage for@nitroNativeAsyncat all. Seenitro's changelog for the corrected async performance figures this surfaced.- Fixed:
@nitroNativeAsyncmethods returning a@HybridRecorddiscarded the result and always posted null (Kotlin and Swift) — the native-async trampoline's return-type dispatch had no record-aware branch, so it fell through to a generic path: Kotlin ran the impl insiderunBlocking, threw the result away, and unconditionally calledpostNullToPort; Swift attempted to coerce the record struct through the genericInt64branch ((try? await ...) ?? 0), which doesn't type-check for a struct. Both platforms now encode the record through the same wire format every other record-returning path uses (result.encode()on Kotlin,result.toNative()on Swift) and post it — a new nativepostBytesToPortJNI helper mallocs a buffer for the KotlinByteArrayand posts its address; Swift posts the encoded pointer directly. Nullable records post address0(notDart_CObject_kNull) on both platforms, since the Dart-side unpack for nullable records always does an unconditionalraw as intcast before checking for a null pointer. Covered by new fixtures/tests innative_async_test.dart(record and nullable-record specs, Kotlin and Swift). - Fixed:
@nitroNativeAsyncmethods with a non-primitive parameter (enum,@HybridRecord/@NitroTuple,@NitroVariant,List<T>of any of those, or a callback) generated Kotlin/Swift that failed to compile — the record-return fix above surfaced a wider, pre-existing gap:@NitroNativeAsync's trampoline only ever decoded nullable-primitive parameters; every other parameter category was forwarded as its raw undecoded bridge value (aByteArray,Long, or raw pointer) into a call site expecting the fully-decoded type. Kotlin now shares the same param-resolution logic (_buildCallParams) and decode step (_emitParamDecodes) the synchronous/@nitroAsyncpath already used, plus a ported nullable-enum sentinel decode. Swift now pre-decodes records/tuples/variants/structs/lists into owned local values beforeTask.detachedstarts (mirroring the existing nullable-primitive_dec-local pattern — the Dart arena backing these pointers is freed synchronously right after the C function returns, before the detachedTaskever runs) and wires up callback params (callbackWrapper) and TypedData params (the decoded local existed but was never referenced — dead code).Map<String,V>/NitroAnyMapand bare@HybridStructparameters on Kotlin remain unfixed (deferred — architecturally entangled with return-type dispatch and Kotlin's struct-param representation needs its own investigation); Swift struct params are fixed as a side effect of reusing the existing sync-path decode expression. Covered by 21 new tests innative_async_test.dart(one consolidated spec covering every parameter category, both generators). - Fixed: several more
@nitroNativeAsyncreturn-type categories were discarded/miscoerced, plus a regression the record-return fix itself introduced — following up on the return-type dispatch gap above:- Regression fix:
List<@HybridEnum>/List<@NitroVariant>returns on Kotlin were being routed into the record-return fix's single-record fallback, generatingresult.encode()on a KotlinList(not a member — compile error). They now get their own dedicated encoders mirroring_emitEnumListBody/_emitVariantListBody. - Kotlin: bare
@NitroVariant,Map<String,V>,NitroAnyMap, and@NitroCustomTypereturns were all discarded and always posted null (no dispatch branch existed). All four now encode via the same wire formats their sync/@nitroAsynccounterparts use and post viapostBytesToPort; custom types post the impl's own byte array directly (no generator-side encoding exists for them). - Swift: bare
@NitroVariant, bare@HybridStruct,Map<String,V>, TypedData, and@NitroCustomTypereturns all fell to the generic(try? await ...) ?? 0coercion, which doesn't type-check against a non-Int64-convertible Swift value (compile failure). All five now encode via their sync-path equivalents and post the resulting pointer askInt64(mirroring the record-return fix's convention: a thrown/absent result posts address0, neverDart_CObject_kNull).NitroAnyMapreturn is deliberately not fixed on Swift — it has no working return-encode path anywhere in the Swift emitter (sync or@nitroAsynceither), a pre-existing bug unrelated to native-async and out of scope here. - Swift silent bugs (compiled fine before, but wrong at runtime):
uint64?returns collapsed a thrown/nil result to0via the generic fallback, indistinguishable from an actual0value — now uses the same pointer-encode approach asint?/double?/DateTime?. NullableAnyNativeObjectreturns used0instead of the-1"no value" sentinel every otherAnyNativeObjectpath (params, sync return) already uses. - Kotlin struct returns and
Map/AnyMap/struct parameters remain open gaps — see the param-fix entry above and the generator'snative_async_test.dartfor what's covered. - Covered by 12 new tests in
native_async_test.dart(one consolidated returns spec, both generators).
- Regression fix:
- Fixed three more
@nitroNativeAsyncbugs, found by building and running the fixes above end-to-end (Android/iOS/macOS) in a real plugin (nitro_type_coverage) rather than only asserting generator-output strings — the C++/JNI bridge and Dart FFI generator layers had never been exercised for these categories at all:- C++/JNI signature builder crashed the generator outright for any variant or custom-type native-async param (
Bad state: Unknown JNI signature type "TcEvent") —_jniNativeAsyncSignever threadedvariantNames/customTypeNamesthrough to_jniParamSig(its sync-path counterpart,_jniSig, already did). Worse than the Kotlin/Swift gaps above, which at least produced something. - C++/JNI per-param marshaling for native-async never learned records, variants, custom types,
NativeHandle,AnyNativeObject, or callbacks — onlyString/struct/TypedData/Map/nullable-prim params were converted to their JNI-expected shape; everything else (including the enum C-parameter declaration, which needsint64_tnotvoid*) was forwarded as a raw pointer/void*where the JNI method signature expected ajbyteArrayorint64_t. Ported the exact conversions_emitJniRegularFuncBody(the sync/@nitroAsyncpath) already had for these categories. - Dart FFI generator, param side: a nullable enum native-async parameter was passed as the raw enum object instead of
.nativeValue/-1— the native-async-onlyplainCallArgshelper (a duplicate of the correct arena-basedcallArgslogic) checkedspec.isEnumName(t)without stripping the type's?suffix first, so it silently never matched and fell through to a raw passthrough. - Dart FFI generator, return side: bare
@NitroVariantanduint64?native-async returns had nounpackbranch —raw(the posted pointer address / packed-struct pointer) was cast directly to the wrong Dart type instead of being decoded. Variant crashed outright (type 'int' is not a subtype of type 'TcEvent');uint64?was worse — it silently "succeeded" (sinceuint64?isint?under the hood) but returned the raw pointer address as if it were the decoded value. - Covered by 11 new tests in
native_async_test.dart(CppBridgeGeneratorandDartFfiGeneratorgroups) plus a new§67integration-test section innitro_type_coverage's example app, run and passing on Android, iOS, and macOS.
- C++/JNI signature builder crashed the generator outright for any variant or custom-type native-async param (
0.5.6 #
Android zero-copy memory-leak fix. No breaking changes — regenerate your
plugin (dart run build_runner build) to pick it up; the runtime packages are
unchanged.
- Fixed: JNI global-reference leak on every zero-copy stream event (Android/Kotlin backends) — for
@HybridStruct(zeroCopy: [...])structs delivered through a@NitroStream, the generated C++ bridge pinned the backing Kotlin object withNewGlobalRef(stored ing_zero_copy_refs) so the borrowed buffer stays alive while Dart reads it — but the generated struct release function onlyfree()d the C struct and never deleted the global ref. ART's global-reference table (51,200 slots) fills at the stream's frame rate and the process aborts withglobal reference table overflowafter ~25 minutes of continuous streaming (measured with a 30 fps camera frame stream). The bridge now emits a<libStem>_zero_copy_releasehelper (declared in the JNI prologue) that erases the pinned ref, and the struct release function calls it before freeing. Covered by newcpp_bridge_generator_test.dart/proxy_generation_test.dartcases asserting the release path deletes the ref exactly once.
0.5.5 #
Desktop C++ (NativeImpl.cpp on Windows/Linux) repair release. The desktop
path had never been compiled end-to-end; wiring the full nitro_type_coverage
suite through it (with CI) surfaced and fixed the following. No breaking
changes for published users — the Kotlin, Swift, JNI, and Dart outputs are
byte-identical (modulo source-line comments); only the desktop C++ artifacts
(*.native.g.h, the desktop dispatch in bridge.g.cpp, *.impl.g.cpp)
changed, and no shipped plugin has a non-stub desktop implementation.
- Fixed:
@NitroVariantC++ decoder emitted invalid C++ —auto x = *reinterpret_cast<...>(ptr), ptr += 8;does not parse. The codec is rewritten onNitroRecordReader/NitroRecordWriter, now wire-correct for nullable fields (presence flags), enum fields (Dartenum.index— per-enumnitro_<Enum>_fromIndex/toIndexhelpers are generated), inline record fields,List<T>fields, and TypedData fields.nitro_encode_<V>is now writer-based, with anitro_<V>_to_nativeconvenience for method returns. - Fixed: header/bridge/starter type-mapping drift —
*.native.g.h, the desktop dispatch, and*.impl.g.cppwere generated from three separate, diverged type mappings (mismatchedemit_*signatures, raw function pointers vsstd::function,void*vs typed buffers). All C++ artifacts now shareCppInterfaceGenerator's public type helpers. - Fixed: nullable property getters could not represent null —
double?getters returned plaindouble; they now returnstd::optional<T>end-to-end (getter, setter, dispatch encode). - Fixed: stream emitters posting
kNullinstead of values — String, uint64, and all nullable-item streams now post real values (kString,kInt64,kDouble) withkNullreserved forstd::nullopt. Record/variantemit_*now take non-owning payload views and the bridge copies into a malloc'd length-prefixed block. Batch streams post the[count, items…]kArray shape Dart expects. - Fixed: desktop dispatch gaps — nullable String/enum/struct params and returns (previously crashed on null or failed to compile),
DateTime?/uint64?NitroOpt handling,Map<String, T>ABI mismatch (uint8_t*vsvoid*), variant returns,@NitroResultblob encoding ([1B tag][4B len][payload], impl signals errors by throwing), and@NitroNativeAsyncnullable-primitive parameter decoding. - Fixed: callback ABI wrapping — the desktop dispatch now adapts raw Dart
NativeCallablefunction pointers (everything routed through Int64 registers: double bits, bool 0/1, flattened structs, malloc'd variant blobs, malloc'd Utf8 returns) into clean impl-facingstd::functionsignatures (std::function<std::string(int64_t)>,std::function<void(const TcPoint&)>, …). - Fixed: struct-in-record C++ decode —
@HybridStructfields inside records called a non-existentT::fromReaderon plain C typedefs; free-function codecs (nitro_<Struct>_fromReader/encodeInto) are now generated. - Added: C++ record encoders — every generated record struct now has
encodeInto(NitroRecordWriter&)andtoNativeBuffer()(mirroringfromReader/fromNative), so desktop impls can construct records without hand-rolling the wire format.NitroNullableInt/Double/BoolC++ structs are now emitted (they exist as library types on other platforms but had no C++ definition). Record structs are emitted in dependency order (by-value embedding compiles). - Added:
NitroRecordReader.readInt8/NitroRecordWriter.writeInt8/writeBytes/toNativeBuffer— required by the variant codec and blob helpers. - Fixed:
uint64/DateTimemapped tovoid*in the C++ interface — nowuint64_tandint64_t(ms-epoch) respectively. - Changed (cosmetic, no behavior change): instance-key Utf8 pointer now allocated and freed with
callocinstead ofmalloc—_instanceKey.toNativeUtf8(allocator: calloc)+calloc.free(_keyPtr). Note for anyone auditing this:package:ffi'smalloc.free/calloc.freeboth resolve to the same OS-level free (CoTaskMemFreeon Windows,free()elsewhere) regardless of which allocator produced the pointer, so the priormalloc/malloc.freepairing was never a bug — this is a style-only change (zero-initialized allocation as defense in depth).
0.5.4 #
- Fixed:
cpp_record_generator.dart— library record types excluded from C++ forward declarations — Types in_nitroLibraryRecordTypes(NitroOptInt64,NitroOptFloat64,NitroOptBool,NitroNullableInt,NitroNullableDouble,NitroNullableBool) are now filtered out before generating C++ struct forward declarations and definitions. These types are provided as C anonymous typedefs in the generatedbridge.g.h; re-declaring them as named C++ structs caused a compilation error in multi-spec plugins. - Fixed:
swift_record_generator.dart— library record types skipped inNativeImpl.cppbridges — WhenemitBoilerplate: false(cpp module bridge path), library record type struct definitions are now omitted. This preventsNitroNullableIntand similar types from being defined twice in the same Swift SPM module when a plugin contains both a Swift-backed and a C++-backed spec. - Fixed:
cpp_direct_emitter.dart— Meyers' Singleton prevents static initialization order fiasco — All C++ registry globals (g_instances,g_instances_mtx,g_next_instance_id,g_factory) are now generated as function-local statics accessed via wrapper functions. This guarantees thread-safe initialization before first use, preventing the SIOF crash (std::__next_primeabort) that occurred when__attribute__((constructor))callbacks fired before the globals were initialized.
0.5.3 #
- Ecosystem sync — Aligned with
nitrogen_cli0.5.3.
0.5.2 #
- Ecosystem sync — Aligned with
nitrogen_cli0.5.2.
0.5.1 #
- New: Generator structural invariant tests for multi-spec Swift deduplication —
swift_bridge_dedup_invariants_test.dart(15 tests) verifies that generated Swift bridge files always conform to the structural contract relied upon bynitrogen_cli'sstripSharedSwiftPreamble:NitroEncodableis always emitted, it precedes the/**doc-comment that marks the spec-specific boundary, the declaration is unindented (soline.startsWith(...)matching works), and the preamble is correctly stripped across 2- and 3-spec plugins. - Ecosystem sync — Aligned with
nitrogen_cli0.5.1.
0.5.0 #
- Fixed: Nullable callback return types silently stripped of
?—spec_extractor.dartcalledreturnType.getDisplayString(withNullability: false)for function (callback) types, silently dropping?from nullable return types. This caused three downstream bugs: (1) the generated helper acceptedT Function()instead ofT? Function(), producing a type mismatch at the call site; (2)isNullableRetwas alwaysfalsein_callbackExceptionalReturn, so all nullable callback returns used the wrong exceptional-return sentinel (e.g.0instead of-1forAnyNativeObject?); (3)_callbackReturnExpressionnever generated the null-guard wrapper for nullable returns, causing aNull check operator used on a null valuecrash at runtime. Fixed by changing togetDisplayString()(nullability preserved). - Fixed: Unused local variable
isNullablewarnings — Removed deadisNullablevariable declarations indart_callback_helpers.dart(two sites) andkotlin_callback_emitter.dartwhere the variable was computed but never read (theisNullableNitroPrimcheck on theBridgeTypewas used directly instead). - Fixed: Local variable lint
no_leading_underscores_for_local_identifiers— Renamed_isOptPrim→isOptPrimand_isOptPrimNA→isOptPrimNAinkotlin_function_emitter.dart. - Fixed: Unnecessary string interpolation braces lint —
'(${fieldTypes})'→'($fieldTypes)'indart_record_generator.dart. - Fixed: Unused/redundant imports in test — Removed shadowed
bridge_spec.dartimport and unusedcpp_bridge_generator.dartimport fromtuple_type_test.dart.
0.4.6 #
- New Annotations — Added generation support for
@NitroVariant,@NitroResult,@nitroNativeAsync,@zeroCopy, and@NitroOwned.
0.4.5 #
- Fixed:
@NitroOwnedSwift bridge emitsreturn nilinstead ofreturn ()—_emitSyncBodyinswift_function_emitter.dartnow checksisNativeHandlebefore theisVoidbranch. Previously aNativeHandle<T>-returning function fell through to thevoiddefault and emittedreturn (), causing a compile-time type error (cannot convert '()' to 'UnsafeMutableRawPointer'). - Fixed:
@NitroOwned_releasesymbol compiled on all platforms — The generated${lib}_${method}_releaseC function is now emitted in the global section of.bridge.g.cpp(before any#ifdef __ANDROID__/#elif __APPLE__platform guard). Previously it was inside the Apple-only block, so Android builds failed withundefined symbol: …_release. The function body uses an inner#ifdef __ANDROID__guard: no-op on Android (Kotlin handle is ajlong),free(handle)on Apple (Swift allocates viaUnsafeMutableRawPointer.allocate). - Fixed:
@NitroVariantSwift protocol uses concrete type, notAny—swift_protocol_registry_emitter.dartnow resolves variant parameter and return types to their concrete Swift enum name viaSwiftTypeMapper._variantNames, so generated protocols carryTcEventinstead ofAny. Theswift_type_mapper.dartO(1) lookup set was also extended to cover all registered variant types. - Fixed:
@NitroResultSwift protocol generatesthrows -> T— Methods annotated with@NitroResultnow emit a throwing Swift protocol signature (throws -> InnerType) rather than returning the rawNitroResultValuebuffer. Theswift_variant_emitter.dartextracts the inner type parameter and strips theNitroResultValue<…>wrapper for the protocol declaration. - Fixed: Duplicate
_releaseremoved fromswift_shim_emitter.dart— The now-redundant Apple-only_releaseemission that was previously added to_emitSwiftBridgeSectionhas been removed to avoid a duplicate-symbol linker error when building for Apple targets. - Added: Generator tests for
@NitroVariant,@NitroOwned, and@NitroResult—nitro_variant_test.dartgains 9 new test cases covering: concrete variant type in protocol,@NitroResultthrowssignature,@NitroOwnedreturn nilguard,_releasesymbol name/placement/platform guard, andBridgeType(name: 'NativeHandle<Void>', isNativeHandle: true)fixture (corrected from the erroneousname: 'void').
0.4.4 #
- Fixed:
List<@HybridStruct T>return type wire format mismatch on iOS/macOS — The Swift bridge now callsNitroRecordWriter.encodeIndexedListinstead ofencodeListfor struct-list return values. The DartLazyRecordList.decodeexpects the indexed format ([int32 count][int64×n offsets][item bytes...]); the old sequential format caused it to interpret item bytes as offset values, producing aRangeError: Value not in rangeat runtime when accessing any element. - Added:
NitroRecordWriter.encodeIndexedListin the Swift codec template —record_generator.dartnow includesencodeIndexedListalongsideencodeListin the_swiftRecordWriterReaderconstant, so every freshly generated Swift bridge has the method available without needing a manual patch. - Fixed: Swift generator emits correct call site for struct-list returns — Both the sync and async
isRecordListpaths inswift_generator.dartnow emitencodeIndexedListfor@HybridStruct-element lists. Primitive-element lists (List<int>,List<String>, etc.) continue to useencodeListunchanged, since those are decoded byRecordReader.decodePrimitiveList(sequential format). - Tests: Updated 4 test assertions —
struct_list_test.dart(2) andall_generators_type_coverage_test.dart(2) updated fromencodeListtoencodeIndexedListfor struct-list Swift output expectations.
0.4.3 #
- Fixed: Optional primitive parameters (
int?,double?,bool?) across the FFI bridge — Dart now encodesnullas a sentinel value (-1forint?/bool?,NaNfordouble?) when calling the C bridge, and Kotlin decodes those sentinels back tonullbefore forwarding to your implementation. Previously, passingnullfor an optional primitive caused a JVM method descriptor mismatch crash at runtime. - Fixed: Nullable struct parameters in JNI bridges — Null-checks are now emitted before calling
unpack_*_to_jni, so passing anullstruct reference no longer causes a segfault. - Fixed: Non-zero-copy
TypedDatafields in struct JNI bridges — Non-ZC typed-data struct fields (e.g.Uint8ListwithoutzeroCopy) now correctly allocate a malloc'd C buffer from the Java array on unpack, and create a Java array from the C buffer on pack, with properDeleteLocalRefcleanup. - Fixed: Type-only spec files generate correctly — Specs that only declare enums/structs (no class bridge) now early-return and produce only type declarations instead of an empty or broken bridge file.
- Fixed: Enum and record JVM method descriptors — JNI method descriptor builder now emits
Jfor enum params and[Bfor record params, matching what the JVM expects. - Fixed: Nullable type stripping in JNI descriptors —
?is stripped from type names before struct/enum descriptor lookup soPoint?maps to the correctL.../Point;descriptor. - Fixed: Named parameters with default values in Dart FFI signature — Generated bridge function signatures now include the default literal (e.g.
int copies = 1) so callers can omit them. - Fixed:
StateErroron non-optional null struct/record returns — AStateErroris now thrown immediately when a native call returnsnullptrfor a non-nullable struct or record return type, instead of crashing later with a null-dereference. - Fixed: Cross-file type includes in C++ header — When a spec references types from other
.native.dartfiles, the generated header now emits the corresponding#includedirectives. - New:
specTesttesting API — Newtest/spec_tester.dartandtest/spec_from_source.darthelpers let you test any generator against an inline source string in onespecTest(...)call — nobuild_runneror on-disk spec file needed. Supports per-language checks (has,hasNot,beforeordering),all:cross-language assertions,skip:, anddebugPrint:. - Tests: 65 new edge-case tests —
spec_tester_test.dartcovers parsing defaults, annotation args, function kinds, parameters, sentinel encoding, properties, streams, enums, structs, error cases, and the specTest harness itself.
0.4.2 #
- Fixed: Nullable return types in Swift bridge —
spec_extractor.dart_makeBridgeTypenow readstype.nullabilitySuffix == NullabilitySuffix.questionand propagatesisNullable: trueinto every returnedBridgeType. PreviouslyisNullablewas alwaysfalsefor function return types, so methods likeint? maxLevel()silently generatedreturn impl.maxLevel()(invalid Swift —Int64?not assignable toInt64). - Fixed: Swift
@_cdeclstubs for every nullable return kind —swift_generator.dartnow emits correct fallback code for all nullable return types:int?→?? 0,double?→?? 0.0,bool?→?? falsethen ternary? 1 : 0,String?→strdup("") ??guard,Enum?→?.rawValue ?? 0,Struct?→ double-guard pattern (guard let impl = …, let result = impl.method() else { return nil }) with bare struct name inUnsafeMutablePointer,Record?→ explicit impl guard +?.toNative(). - Fixed: Nullable type name stripping for
isString/isStruct/isRecorddetection — Strip trailing?before matching against type names so nullable return types (e.g.,Point?,Reading?) are routed to the correct generator branch. - Fixed:
knownTypeNamespropagation to_extractFunctionsand_extractPropertiesAndStreams— Struct and enum names are now included in theknownTypeNamesset passed to_makeBridgeType, enabling correct type classification for user-defined return types. - Fixed: Swift typed-data pointer conversion — Replaced the broken
!= nil ? param! …pattern with the correct.map { … } ?? fallbackidiom forUnsafeBufferPointertyped-data parameters. - Fixed: Dart FFI nullable
String?parameter — Added!null assertion intoNativeUtf8call for nullable String params so the generated code compiles correctly. - Fixed: JNI bridge class local-ref leak —
cpp_bridge_generator.dartinitialize()now callsenv->DeleteLocalRef(localClass)afterNewGlobalRef, preventing a local reference from accumulating in the JNI frame on every re-initialization. - Tests: 10 new nullable return type tests —
swift_generator_test.dartcoversint?,double?,bool?,String?,Enum?,Struct?(nullable + non-nullable), andRecord?(nullable + non-nullable) return paths. - Ecosystem sync — Aligned with
nitro,nitro_annotations, andnitrogen_cli0.4.2.
0.4.1 #
- Fixed: Struct size calculation —
@HybridStructfield sizes are now computed correctly for nested struct types and aligned to pointer boundaries, preventing silent memory corruption when structs are passed across the FFI boundary. - Fixed: Optional parameter support — Methods with optional positional or named parameters now generate correct Dart FFI signatures and C++ bridge stubs; previously optional params were treated as required, causing compile errors.
- Fixed: C++ bridge release-mode compilation — Generated
*.bridge.g.cppand*.bridge.g.hnow include theNITRO_EXPORTmacro fromnitro.hunconditionally, fixing linker errors when building in release/archive mode with LTO. - Fixed: Mixed Apple platform linking — Generated C++ bridges now emit correct per-platform
#if TARGET_OS_OSX/#elseguards for modules that use different implementation languages on iOS vs macOS (e.g.ios: NativeImpl.swift+macos: NativeImpl.cpp). Swift protocol generation handles mixed targets without emitting aHybridXxxProtocolfor the wrong platform. - Fixed: Android stabilization — C++ bridge generator no longer emits duplicate
JNI_OnLoadregistrations on multi-module builds;build.yamlinput exclusions prevent stale outputs from prior runs. - Fixed: Generated code lint —
_initSwrenamed toinitSwin the generated Dart FFI impl constructor; eliminates theno_leading_underscores_for_local_identifierswarning in every generated.g.dartfile. - Ecosystem sync — Aligned with
nitro,nitro_annotations, andnitrogen_cli0.4.1.
0.4.0 #
- New: Mixed Apple platform implementation targets — A single module can now use different implementation languages per Apple platform. For example,
macos: NativeImpl.cppwithios: NativeImpl.swiftgenerates a single bridge with#if TARGET_OS_OSX/#elseguards — no manual patching required. Supports all combinations: both Swift, both C++, or mixed. - Fixed: Swift
@nitroNativeAsyncprotocol signature — Methods annotated with@nitroNativeAsyncnow correctly declareasync throwsin the generatedHybridXxxProtocol. - SPM and CocoaPods support — Generated C++ bridges compile correctly under both Swift Package Manager (
.mmforwarder vianitrogen link) and CocoaPods (ios/Classes/andmacos/Classes/forwarders). - Ecosystem sync — Aligned with
nitro,nitro_annotations, andnitrogen_cli0.4.0.
0.3.3 #
- Fixed: JNI crash (ART abort) with nested
@HybridStructfields —GetFieldIDwas called with"Ljava/lang/Object;"for nested struct fields, posting aNoSuchFieldErrorthat ART turned into a fatal runtime abort on the next JNI call. Fixed by generating the correct class descriptor (e.g.Lnitro/nitro_ar_module/Vector3;) in the constructor signature,GetFieldIDcalls,pack_*_from_jni,unpack_*_to_jni, and the release function. Both the already-generated file and the generator itself were fixed to prevent regression. - New:
@HybridStructtypes usable as@HybridRecordlist fields —@HybridRecord() class PackageBoxes { final List<BoundingBox> boxes; }now serializes correctly end-to-end.spec_extractor.dart:_recordFieldKindnow recognises@HybridStruct-annotated types, classifyingList<BoundingBox>aslistRecordObject(notlistPrimitive).struct_generator.dartgenerateKotlin: Every Kotlindata classfor a struct now includescompanion object { decodeFrom(buf) / decode(bytes) },writeFieldsTo(out, buf), andencode(): ByteArrayso structs can be embedded inline in record binary payloads.record_generator.dartgenerateDartExtensions: Auto-generatesRecordExtextensions (withfromNative,fromReader,writeFields,toNative) for every@HybridStructtype referenced in a record field, including transitive closure for nested struct types.
- Fixed: Kotlin record list field wire format — The
writeIndexedListhelper (which discarded list items via{ _ ->}and referenced an undefinedit) has been replaced with a simplewriteInt32(size) + forEach { e -> e.writeFieldsTo(out, buf) }. The corresponding Kotlin read no longer skips a phantom offset table; both sides now use the same count-then-items format as the Dart codec. - Tests: 50 new tests in
struct_in_record_test.dart— Cover: DartRecordExtfor struct list items, Kotlin struct codec methods, Kotlin record using struct codecs, transitive nested-struct closure,recordObject(non-list) struct fields, all primitive field types, wire-format consistency, and negative cases (unreferenced structs produce noRecordExt).
0.3.2 #
- Fixed: Nested struct fields generate typed pointers — Fields whose type is another
@HybridStructnow usePointer<NestedFfi>instead ofPointer<Void>.toDart(),toNative(),freeFields(), and proxy lazy getters all handle nested pointers correctly. - Fixed: Proxy
super()for nested struct fields — Zero-value defaults are now generated recursively (e.g.Vector3(x: 0.0, y: 0.0, z: 0.0)) instead ofnull, which was invalid for non-nullable types. - New: Positional constructor param support —
BridgeFieldgainsisNamedandisRequiredflags. The generator emits positional args before named args intoDart()and proxysuper(), matching the struct's actual constructor signature. The spec extractor reads these flags automatically. - Fixed: TypedData length-field matching is case-sensitive — Only exact lowercase names (
length,size,stride,bytelength,bytelen,len) match. A field namedStride(capital S) now correctly falls back toasTypedList(0). - Tests: 135 new tests across 3 files —
nested_struct_test.dart,struct_constructor_params_test.dart, andstruct_field_types_test.dartcover nested structs, all constructor styles, String/enum/TypedData fields,freeFields()combinations, zeroCopy, nullable stripping, and more.
0.3.1 #
- New: macOS targeting in
BridgeSpec—BridgeSpecnow accepts an optionalmacosImplfield (NativeImpl?) and exposestargetsMacosandtargetsAppleCppgetters.targetsAppleCppis true when eitheriosormacos(or both) useNativeImpl.cpp, enabling a single#ifdef __APPLE__guard in the C++ bridge instead of separate iOS/macOS guards. - New:
INVALID_MACOS_IMPLvalidator error —SpecValidatoremits an error with codeINVALID_MACOS_IMPLand severityerrorwhenmacos: NativeImpl.kotlinis specified, since Kotlin is not a valid native language on macOS. - Improved:
isCppImplgetter — Updated to account formacosImpl; a spec is considered cpp-only when all specified platforms useNativeImpl.cpp. - Improved:
CppBridgeGeneratorplatform guard — The Apple-platform#ifdefblock now uses__APPLE__(covers both iOS and macOS) instead of__APPLE__ && TARGET_OS_IOS, so generated C++ bridges compile correctly in both iOS and macOS targets. - New: Edge-case tests in
spec_validator_expansion_test.dart— 5 new cyclic struct detection edge cases: struct with no fields, struct with only primitive fields, two independent mutual cycles (each reported exactly once), four-struct transitive cycle, and a struct referencing a primitive type (not treated as cycle). - Fixed: stale
DeleteLocalReftest assertions —cpp_bridge_generator_test.dartandedge_cases_test.dartexpected explicitenv->DeleteLocalRef(j_param)calls that are no longer emitted; the generator now wraps every JNI call inPushLocalFrame(16)/PopLocalFrame(nullptr)which frees all local refs automatically. Tests updated to assert thePushLocalFrame/PopLocalFramepattern and guard against regressing to manualDeleteLocalRef. - Fixed: record-return exception-ordering test snippet size — the 400-char substring was too small to contain
GetByteArrayRegionafter the extraPopLocalFrameerror paths were added; extended to 700 chars and added presence guards for both substrings. - New: Zero-copy proxy streaming —
StructGenerator.generateDartProxiesnow emitsfinal class ${Name}Proxy extends ${Name} implements Finalizable. Every getter is@overrideand reads lazily from aPointer<${Name}Ffi>; super fields are zeroed and never read. BecauseProxy <: ValueType,Stream<Proxy>satisfiesStream<Value>via Dart covariant generics — no.map()or API change required. - New: Generated C release symbols —
CppBridgeGeneratoremits avoid ${lib}_release_${Struct}(void* ptr)function for every@HybridStructinsideextern "C"blocks on both the direct-C++ and JNI+Swift paths. - New:
NativeFinalizerwith generated release symbol — Each proxy'sstatic NativeFinalizer? _finalizeris lazily bound todylib.lookup('${lib}_release_${Struct}')via an idempotentstatic void _init(DynamicLibrary dylib). The impl constructor calls${Name}Proxy._init(_dylib)for each struct. - New:
isLeaf: trueon sync primitive bindings — All synchronous FFI bindings with primitive-only return types (including read/write property accessors) are emitted with.asFunction<...>(isLeaf: true), skipping the Dart VM safepoint transition. - New: Indexed
@HybridRecordlist encoding —DartFfiGeneratorencodes list record params withRecordWriter.encodeIndexedListand decodes list record returns withLazyRecordList.decode. Kotlin and Swift encode with awriteIndexedListhelper; the decode path skips the offset table. - New:
_superDefaulthelper inStructGenerator— Returns a safe zero-value Dart literal for each field type so the proxy'ssuper(...)call compiles without touching native data. - Breaking fix: struct stream override type — Generated struct stream overrides now emit
Stream<${ValueType}>(matching the spec) while usingopenStream<${Proxy}>internally. Previously the impl emittedStream<${Proxy}>which was an invalid override. - Fixed: missing struct release symbols on Android — Struct release functions (used by Dart's
NativeFinalizer) were previously incorrectly guarded by platform preprocessor blocks inCppBridgeGenerator, causing them to be missing from Android builds. They are now generated in a commonextern "C"block for all platforms. - Fixed: memory leaks in struct return paths — Implemented deep release of heap-allocated native fields (e.g., native strings
char*allocated viastrdup) in the struct release functions. - Fixed: struct property getter leaks — Updated the Dart FFI generator to correctly convert and deeply release struct properties, matching the safety logic used for method returns.
- Fixed: struct release exports in C++ header —
CppHeaderGeneratornow includesNITRO_EXPORTdeclarations for all struct release functions, ensuring they are correctly exported and visible to the Dart FFI layer. - New:
freeFields()for FFI structs — Generated FFI struct extensions now include afreeFields()method to safely release internal native resources. - Improved: memory safety in
toDart()— Struct conversion now performs an eager copy ofTypedDatafields (usingUint8List.fromList), preventing use-after-free errors when the native buffer is quickly released. - Improved: cleaner bridge code — Struct release functions are now coalesced into a single
extern "C"block in the generated bridge C++ source. - Tests: regression coverage for struct release — Added unit tests to
cpp_header_generator_test.dart,cpp_bridge_generator_test.dart, anddart_ffi_generator_test.dartto verify memory safety and correct symbol generation across all layers.
0.3.0 #
-
New: Direct C++ Implementation support — Generator produces
*.native.g.h,*.mock.g.h, and*.test.g.cppwhen@NitroModule(ios: NativeImpl.cpp, android: NativeImpl.cpp)is specified. -
New: Thread-safe bridge —
CppBridgeGeneratorusesstd::atomicforg_impland stream port storage;CppBridgeGeneratornow uses direct virtual dispatch with no platform#ifdefblocks. -
New:
std::optional<T>for nullable types —CppInterfaceGeneratoremits nullable Dart types asstd::optional<T>. -
New: Precise
Pointer<T>C++ mapping —Pointer<SomeEnum>→SomeEnum*,Pointer<SomeStruct>→SomeStruct*,Pointer<Void>→void*. -
New:
#ifndefstruct guards —generateCStructswraps each C struct in an include guard to preventtypedef redefinitionerrors in CocoaPods umbrella builds. -
New:
BridgeSpec.isCppImpl— convenience getter for detecting pure-C++ modules. -
Improved: FFI memory safety —
DartFfiGeneratoremitstry { ... } finally { malloc.free(...); }for all record/struct return paths. -
Improved:
checkDisposed()guards — Added to all generated methods includingFast(leaf) functions; annotated@pragma('vm:prefer-inline'). -
Fixed: Builder diagnostics —
builder.printWarningnow shows actual file paths instead of literal placeholders. -
New: Single-platform targeting —
@NitroModulenow accepts optionaliosandandroidparameters. A module can target iOS only, Android only, or both. Generators skip output for untargeted platforms. -
New:
BridgeSpec.targetsIos/targetsAndroid— convenience getters derived from nullableiosImpl/androidImpl. -
New:
NO_TARGET_PLATFORMvalidation error —SpecValidatoremits an error when neitheriosnorandroidis specified. -
Improved:
BridgeSpec.isCppImpl— correctly handles single-platform C++ specs (ios: NativeImpl.cppwithandroidomitted, and vice versa). -
Improved:
SwiftGenerator— returns a placeholder comment when iOS is not targeted instead of generating an empty/broken file. -
Improved:
KotlinGenerator— returns a placeholder comment when Android is not targeted. -
Improved:
CppBridgeGenerator— omits#ifdef __ANDROID__/#elif __APPLE__/#endifplatform guards for single-platform specs; routes to the appropriate single-platform code path. -
Tests: 72 new tests — platform targeting unit tests, single-platform generator output,
isCppImpledge cases, additional validator rules, JNI parameter handling, CMake variable indirection, C++ mock and interface edge cases. -
Tests: 100+ new regression tests — Benchmark spec tests,
Pointer<T>param/return tests, nullable type tests, and stream backpressure isolation tests across all generators.
0.2.3 #
- Fix: Array — Updated the Swift generator to correctly bridge
Uint8Listparameters asDatawhen matching native Swift signatures, ensuring type-safe binary data transfer without manual casts. - Improved: Header Generator — The generated C++ bridge headers now automatically include
nitro.h. - Improved: Dependency Sync: Synchronized the Nitro ecosystem to version 0.2.3.
0.2.2 #
- Fix: stable annotation resolution — updated
SpecExtractorto useTypeChecker.fromRuntimefor all Nitro annotations, ensuring they are correctly identified when re-exported through thenitroruntime package. This resolves "No @NitroModule annotated classes found" and "UNKNOWN_RETURN_TYPE" errors for enums/structs in complex specifications. - Improved: spec-level type registration — ensured that all enums and structs defined in a spec library are correctly added to the valid type set before function, property, and stream validation.
0.2.1 #
- Fix: non-zero-copy TypedData function parameters now produce correct JNI arrays — previously the raw C pointer (
float*,int32_t*, …) was passed directly as the JNI call argument, causing a crash at runtime. The generator now emitsNewFloatArray/NewIntArray/SetFloatArrayRegion/ etc. and passes the properjarrayreference. ADeleteLocalRefis emitted after the call in every return-type path. - Fix: removed redundant
env->ExceptionClear()at JNI call sites —nitro_report_jni_exceptionalready callsExceptionClear()internally; the duplicate call at each call site was a no-op and has been removed. - Fix:
_streamJobsmap now uses a compositePair<String, Long>key — keying only ondartPortmeant two simultaneous subscriptions on different streams could theoretically overwrite each other's coroutine job if they received the same port value. The key is nowPair(streamName, dartPort). - Polish: spec-path attribution in all generated files — every generated file (Dart, Kotlin, Swift, C++, CMake) now includes
// Generated from: <spec>.native.dartat the top, making it easy to trace any generated file back to its source spec when working with multiple modules. - Polish:
checkDisposed()annotated@pragma('vm:prefer-inline')— the single-field_disposedcheck is now inlined by the Dart VM/AOT compiler, eliminating the call overhead on every generated method invocation. - Performance: single-pass AST extraction in
SpecExtractor—_extractRecordTypespreviously calledlibrary.annotatedWithtwice (once to collect names, once to build types); it now collects class elements in one pass and reuses the list._extractPropertiesand_extractStreamspreviously made two separate loops overelement.accessors; they are now merged into a single combined pass. - Decoupled from the
nitroruntime package to resolvepub.devplatform warnings. - Now depends on the pure-Dart
nitro_annotationspackage. - This ensures the generator is recognized as a cross-platform Dart package.
0.2.0 #
- New:
@HybridRecordBinary Bridge Generator — Generated extensions now use a compact binary protocol (uint8_t*/Pointer<Uint8>) instead of UTF-8 JSON strings, significantly reducing serialization overhead.- Breaking: Extension methods renamed to standard codec names:
fromJson→fromNative/fromReader,toJson→writeFields/toNative. - Full support for
@HybridRecordin Kotlin (.bridge.g.kt) via@Keep data classwith companiondecode/encodemethods. Swift support updated fortoNativeandRecordReaderintegrations.
- Breaking: Extension methods renamed to standard codec names:
- New: Comprehensive Collection Bridging — Added binary-first support for:
List<primitive>(int, double, bool, String) viaRecordWriter.encodePrimitiveList.Map<String, T>using the UTF-8 JSON path (dynamic values).- Nested lists and nullable record fields.
- Improved: Swift Stream Stability — Fixed a compiler error in
_register_*_streamby heap-allocating@HybridStructitems before passing them to the C emit callback. - Improved: Code Quality & Lints — Generated code now follows strict Dart linting rules:
- Cleaned up unbraced for-loops and unused local variable declarations.
- Renamed internal variables to follow public naming conventions (e.g.,
_rawResult→rawResult).
- Testing: Added 28+ regression tests for Kotlin record emission and updated 200+ existing tests to match the binary wire format.
0.1.3 #
- Swift generator: fixed
@_cdeclString type crash (EXC_BAD_ACCESS) —Stringparameters now useUnsafePointer<CChar>?(Cconst char*) and return values useUnsafeMutablePointer<CChar>?(malloc'dchar*), withString(cString:)conversion at the boundary andstrdup()for returns so Dart'stoDartStringWithFree()/free()pairs correctly. - Swift generator: async
String-returning methods useDispatchSemaphore+Task.detachedwith astrdup(result)return. - Swift generator:
Stringproperty getters returnstrdup-allocated C strings; setters acceptUnsafePointer<CChar>?and convert withString(cString:).
0.1.2 #
- Swift generator: replaced
@objc public static func _call_*pattern with top-level@_cdecl("_call_*") public funcstubs. Swift structs and Swift-only protocols cannot cross the Objective-C boundary. - Swift generator:
boolreturn type now maps toInt8(matching C'sint8_t) instead ofBool. - Swift generator: struct-returning functions now return
UnsafeMutableRawPointer?(heap-allocated, caller frees) instead ofAny?.
0.1.1 #
- Renamed package from
nitrogentonitro_generatorto avoid a naming conflict onpub.dev.
0.1.0 #
- Initial release of Nitro code generator.
- Generates Dart FFI, Kotlin, Swift, and C++ bindings.
- Support for
HybridObject,HybridStruct, andHybridEnum. - Support for
@nitroAsyncmethods. - Support for
@NitroStreamwith Backpressure strategies.