Technical design
Status: living document. Scope: foundation + Android → Flutter vertical slice.
Decisions#
| # | Decision | Rationale |
|---|---|---|
| D1 | Generator core and CLI written in Dart (pub workspace) | Same ecosystem as package:jni/ffigen; distribution via dart pub global activate; no extra runtime for Flutter users. |
| D2 | Android signatures read by a pure-Dart JVM class-file parser over android.jar |
Machine-readable, first-party, offline, deterministic. No JVM required to generate. HTML documentation is never parsed. |
| D3 | Availability from platforms/android-N/data/api-versions.xml; external annotations from data/annotations.zip; nullability from class-file Runtime(In)VisibleAnnotations |
All shipped inside the official SDK platform package. |
| D4 | Flutter Android output = Dart extension types over package:jni JObject using the public JClass / method-ID API; interface implementation uses JImplementer (the mechanism package:jni provides for jnigen) |
Officially recommended Dart↔Java interop path; no MethodChannel; zero-cost wrappers; tree-shakable. |
| D5 | We do not invoke jnigen; our own emitter consumes our IR |
The IR must remain the single source of truth for all targets (Flutter and React Native). |
| D6 | Overloads → deterministic names: the first overload in canonical order keeps the Java name, later ones get $1, $2…; constructors are new$, new$1, … ; mapping is recorded in binding_map.json and the doc comment |
Same scheme as jnigen output, familiar to Dart JNI users; stable across runs because canonical order is by JVM descriptor. |
| D7 | Default distribution policy local-only, documentation mode links-only |
Safest legal default; see docs/legal. |
| D8 | Threading defaults to unspecified |
Never claim thread-safety without metadata. |
| D10 | React Native: one pure C++ Turbo Module installs global.__nab; generated C++ is data (member tables keyed by Java name + JNI descriptor) interpreted by one runtime |
Official New Architecture mechanism (reactnative.dev pure C++ modules); no per-API TurboModule/Codegen surface; small, uniform C++ |
| D11 | React Native TS output: one module per Java package, no extends between generated classes; inherited members copied onto prototypes lazily on first instantiation ($anc thunks) and typed via declaration merging; parameters use brand types |
Import cycles are harmless (no cross-module work at load); avoids TypeScript override/static-side conflicts; output linear in API size (full android-36: 187 MB flattened → 97.7 MB); Metro transforms modules independently |
| D12 | RN callbacks via java.lang.reflect.Proxy + CallInvoker; context via NabContext.init(app) |
Public SDK APIs only (no hidden ActivityThread) |
| D9 | Apache-2.0 for project source | Permissive with explicit patent grant. This is a project/legal decision the owner may revisit. |
IR#
See architecture/ir.md. Key properties: stable symbol IDs (android.content.Intent#setData(android.net.Uri)), canonical JSON (sorted keys, sorted members), provenance on every type, support status + reason code on every excluded symbol.
Android parsing#
- Zip reading is lazy: the central directory is indexed; class bytes are inflated only for classes in the requested closure (
android.jarhas ~6k classes; the slice needs tens). - Class-file parser: magic/version check, constant pool (all tags incl. dynamic/module), access flags, this/super/interfaces, fields, methods, attributes
Signature,Exceptions,InnerClasses,ConstantValue,Deprecated,RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations,RuntimeInvisibleParameterAnnotations,MethodParameters. Every read is bounds-checked; limits: 64 MiB per class, 65535 cp entries, annotation nesting depth 32. - Generic signatures parsed per JVMS §4.7.9.1 (class, method, field signatures; wildcards; type variables; inner-class suffixes).
- Visibility: only
public/protectedmembers ofpublicclasses are API. Synthetic/bridge members are excluded with reasonE002-family notes. Symbols present inandroid.jarbut absent fromapi-versions.xmlare flaggedhidden_or_non_sdk(they are stubs not part of the published API list) and never emitted.
Flutter Android generation#
- One library per Java package (
android/content.dart), umbrellaandroid.dart. - Each class:
extension type Intent._(JObject _$this) implements Object$(superclass chain and interfaces areimplementsclauses when generated; otherwiseJObject). - Members: constructors (
factory), static/instance methods, static/instance fields (getter/setter), compile-time constants inlined fromConstantValuewith the exact value. - Primitive return/param mapping via the shared
TypeMapper; Javaint/short/byte/char/floatare passed withJValueIntetc. so JNI receives exact widths. - Nullability:
@NonNull/@RecentlyNonNull→ non-nullable Dart type;@Nullable→T?; unknown →T?(safe default) withnullability: unknownin IR. - Availability: if
introduced > minApi, a guardNativeApi.requireAndroidApi(n, symbol)is emitted that throwsNativeApiUnavailableException(E012) on older devices instead of crashing in JNI. - Exceptions: Java exceptions surface as
package:jniJThrowable; the runtime converts them toNativeJavaException(class name, message, Java stack trace, original throwable) viaNativeApi.guard. - Callbacks: Java interfaces whose methods all map get
implement(...)factories built onJImplementer; the Dart handler is retained until the Java proxy is garbage-collected (port closed bypackage:jni).
Out of scope (stage 1)#
React Native, iOS/Objective-C/Swift, Kotlin metadata (suspend/Flow), native UI integration layer, website. The CLI returns explicit diagnostics for these.
Source:
docs/technical-design.md