Skip to main content

Architecture overview

One runtime, one transport, one layout engine. The same NativeAOT library, the same typed struct protocol and the same Yoga tree run everywhere — which is why both platforms can be made to agree on the numbers.

[Your Blazor components] plain Razor/C# — BnView / BnRow / BnColumn / BnScroll …
↓
[BlazorNative.Renderer] headless NativeRenderer + the RenderPatch model
↓
[BlazorNative.Runtime] NativeAOT composition root + the C-ABI exports
typed-struct frames — no JSON, no interpreter
↓ one native library per platform
↓
Android: a .so, cross-compiled iOS: a static archive, linked into the app
↓ ↓
JNA (cdecl callback) direct static link (C-ABI, no JNA)
↓ ↓
[BlazorNative.Jni] [BlazorNative.Apple]
Kotlin shell Swift/UIKit shell
↓ ↓
└────── each shell builds TWO MIRRORED TREES ──────┘
↓
[view tree] [Yoga node tree]
real platform widgets Facebook's C++ flexbox engine
TextView / UILabel Android: the Maven JNI artifact
Button, EditText / UITextField iOS: source-built, reached through
ScrollView / UIScrollView Objective-C++ behind a plain-C
containers = layout-suppressed surface (Yoga's C++ headers can
frame containers never be visible to Swift)
↓
Yoga computes → every child is placed at its COMPUTED FRAME

The four ideas​

1. Your app is a native library, not a bundle​

The Blazor UI and your business logic compile ahead-of-time into a platform-native shared library. There is no IL interpreter, no JIT, and nothing to download at boot. On Android that is a .so cross-compiled against the NDK; on iOS it is a static archive linked directly into the app binary.

2. The renderer is headless, and it speaks structs​

A NativeRenderer drives the real Blazor render tree — nested components, keyed lists, real disposal — and turns diffs into typed struct patches: create node, set style, replace text. They cross into the shell through a C-ABI frame callback. Nothing is serialized. See The wire.

3. The shell builds two trees, not one​

This is the design's load-bearing move. Every shell keeps a tree of real platform widgets and, beside it, a Yoga node tree. Style names are partitioned by an allow-list — layout names go to the Yoga node, visual names go to the view — and the containers are layout-suppressed: they never place a child themselves. Yoga computes; the shell assigns computed frames. See Layout and Yoga.

4. The two shells are held to the same numbers by a test​

Two hand-written shells in two languages will drift. The answer is not discipline, it is a gate: the frames are asserted equal across platforms in CI. See The parity contract.

The packages​

PackageWhat it is
BlazorNative.CoreThe IMobileBridge contract, the capability result types, and DevHostBridge, the in-process mock bridge. A pure library. The on-device bridge lives in BlazorNative.Runtime.
BlazorNative.RendererThe headless NativeRenderer and the RenderPatch model. A pure library.
BlazorNative.HttpBridgeHttpHandler + DI — plain HttpClient over the shell's fetch.
BlazorNative.ComponentsThe Bn* component library. Reference.
BlazorNative.DeviceThe [Inject]-able device façades — geolocation, notifications, biometrics, secure storage, camera — over IMobileBridge.
BlazorNative.AnalyzersCompile-time guards for the native runtime. Rules.
BlazorNative.RuntimeThe publishable composition root: DI wiring, the on-device bridge, and the [UnmanagedCallersOnly] export surface.
BlazorNative.TestingA test-project-only harness: mount a page in a unit test and assert the widget tree it rendered. See Testing a page.

Where the drift is caught​

The style routing table is needed in every language — the renderer's C#, the Android shell's Kotlin, and the iOS shell's Objective-C++ and Swift. A name present in one copy and missing from another is silently dropped, not a build error, so no copy is written by hand: the names live once in src/wire-vocabulary.json, a generator emits every copy, and a test in the required CI lane regenerates them and fails if a committed copy differs.

That pattern — if it can drift silently, a gate reads it — is the one this project reaches for repeatedly, and it is why the architecture pages here point at the code and the workflows instead of restating what they say.

Extending the bridge​

Adding a capability to the host contract (navigate, storage, fetch, …) touches the C-ABI in several places at once. That procedure lives in the repository, next to the code it changes: docs/bridge-extension.md.