|
Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
|
Start with AriaTools to see Aria in a real application. It is Aria's single flagship cross-platform example, driving Qt, iOS, Android, and Web from one C++ ViewModel. This repository now stays focused on the framework, acceptance tests, and minimal documentation snippets.
Aria splits a screen in two. The ViewModel is plain C++ and knows nothing about any UI library; the View is native widgets. BindingEngine joins them, and it only ever talks to the IViewAdapter interface — so porting means swapping the adapter, nothing else.
Upper half — the ViewModel (plain C++, unit-testable, shared by every platform)
There is no UI in that code and no UI header included — it runs under a console test.
Lower half — wiring up the View (a dozen lines per platform; the UI itself stays native)
First, the thing most likely to be misread: you do not write the UI in C++. Buttons, layout and animation are still authored the usual way — Qt Designer, Storyboard, Compose, HTML. The code below only hands widgets that already exist over to the engine, and the three steps never change: ① construct the platform adapter ② build a BindingEngine from it ③ bind a widget to a Property.
.mm; the UI is still Storyboard / SwiftUI)UIKitView retains the UIView* under ARC and, on destruction, tells BindingEngine to drop its subscriptions while the native view is still valid — so no callback ever reaches a released widget.
.mm + NSView)Kotlin-side listeners forward native events back in (adapter->notify_text_changed(...) / notify_click(...)), so listener ownership stays on Android while the C++ side stays strongly typed. Compose has no addressable view object; use the side-channel shape from the adapter guide.
That is the point: five wiring snippets that look nearly identical, and the BillViewModel above is byte-for-byte unchanged across all of them. Porting costs you those dozen lines, not your business logic.
After wiring — mutate data, the UI follows
Once bound, the rest is platform-independent. The code below behaves identically on all five platforms, and not one line of refresh code is written by hand:
Change either bill or people and per_person recomputes and pushes to the label — because its dependencies were recorded automatically on the Computed's first evaluation. You never wrote anything resembling "when people changes, update the label".
One detail worth knowing when you change several values in a row:
And a convenient default: if the final result equals the current value, nothing is notified at all. Following on from above, a batch setting bill=600, people=4 (still 150) leaves the label untouched.
The whole architecture in one picture — upper half is the pure C++ ViewModel, the middle is BindingEngine (which only knows the IViewAdapter interface), and the bottom row is the five native adapters:

Continue with the binding guide, the per-platform adapter guides, the cookbook, or the full four-platform AriaTools application.
Aria does one thing: it extracts the reactive engine and binding layer out of the UI framework, as a plain C++ library supporting C++23, independent of any UI toolkit.
A ViewModel is an ordinary C++ class — no framework base class, no macros, no code generator. UI layers plug in through IViewAdapter; five adapters ship in-tree today (Qt6 / AppKit / UIKit / JNI / HTTP). Swapping the UI toolkit does not touch the ViewModel.
Know the costs before you pick it:
| Trade-off | What it means |
|---|---|
| C++20 minimum; C++23 supported | Full coroutine and concepts support (GCC 12+ / Clang 15+ / MSVC v143). C++17 projects cannot use it. |
| No widgets | Aria draws nothing. Widgets, layout and animation stay with your UI toolkit; Aria only owns the data flow between state and view. |
| Template layer is source-compatible only | aria-abi / aria-runtime / aria-binding are ABI-stable within a major version; Property<T> and friends need a recompile across versions. |
| Adapters are on you | Only the five adapters above work out of the box. A new toolkit means implementing an IViewAdapter (see the adapter guides). |
| Young project | Ecosystem, tutorials and third-party components are nowhere near a mature framework's. AriaTools is currently the only real application using it. |
Good fit: you already have a C++ business core, want to reuse one copy of that logic across platforms, and want each platform to keep its native UI.
Poor fit: you want "one codebase including the UI". That is what full UI frameworks like Flutter and Qt Quick are for — they have mature reactive binding of their own, and Aria does not try to replace them.
| Module | Type | Depends on | Notes |
|---|---|---|---|
| aria-abi | SHARED by default | Threads | Compiled foundation: signals, shared reactive graph, diagnostics storage, scheduler base, and version metadata. Static builds are supported. |
| aria-core | header-only | abi | All the templates: Property, Computed, Command, ObservableList, Validator. Source-compatible only (not ABI-stable). |
| aria-async | header-only | core | Coroutine Task<T>, executors. Source-compatible only. |
| aria-runtime | SHARED | core, abi | EventBus / Container / Dispatcher / Logger — singletons live in one dylib. ABI-stable (non-template exports). |
| aria-binding | SHARED | core, runtime | BindingEngine, IViewAdapter. ABI-stable (non-template exports). |
| Adapters | SHARED/STATIC | binding | Qt6 / AppKit / UIKit / JNI / HTTP (each opt-in). WASM is conditional roadmap work. |
Windows is supported on two toolchains: MSYS2 UCRT64 (GCC) and MSVC / Visual Studio 2022. Pick whichever fits your team's existing stack — both build the full framework + tests + adapters from a single tree, no source forks. See "Windows toolchains" below.
build/ is a container for build trees — never configure straight into it. The unified build layout is documented at the top of scripts/build.sh; the per-flavor script scripts/build.sh [release|debug|asan|tsan] picks the right directory for you.
Normal builds use the bundled doctest header. CMake fetches the fallback test dependency only if that vendored header is absent.
Aria ships with two parallel build scripts for Windows. They live side-by-side in scripts/, write to separate build directories, and neither one needs to know about the other.
| Toolchain | Script | Build dir | Notes |
|---|---|---|---|
| MSYS2 UCRT64 (GCC 14+ / Clang 18+) | scripts\build.ps1 | build/ | Lightweight (~300 MB). Pre-installed on most CI images. Auto-detected from C:\msys64\ucrt64\bin and a few other common paths. |
| MSVC v143 (VS 2022) | scripts\build-msvc.ps1 | build/flavors/msvc/ | Auto-detects the VS install via vswhere, scrubs MSYS2 env vars (INCLUDE / LIB / CPATH / ...) before running CMake, and uses the Visual Studio 17 2022 generator. |
You can switch back and forth without clean — the two trees are isolated. CI runs both nightly to make sure neither regresses.
Rationale for shipping both: Aria uses C++ coroutines extensively that libstdc++, libc++, and the MSVC STL all handle cleanly. Pinning a single Windows toolchain artificially excluded a large chunk of users in the .NET / Visual Studio ecosystem — we now validate against MSVC v143 on the same release gate as macOS, Ubuntu, and MSYS2.
Option A — find_package after install (recommended for production):
Installed Linux shared libraries resolve other Aria libraries from their own directory. Keep these libraries together; after moving the SDK, configure consumers against its new installation path.
Option B — vendored (no install):
AriaTools is the single flagship cross-platform example, driving Qt, iOS, Android, and Web from one C++ ViewModel, with all four shells gated in CI. It is also the reference for both Android integration shapes: the Compose side-channel and the typed JniAdapter. The Aria repository no longer carries application examples. Framework behavior is pinned by tests/acceptance/ and module tests, while these docs keep only focused, minimal snippets.
| Option | Default | Description |
|---|---|---|
| ARIA_BUILD_TESTS | ON | Build unit tests + ctest registration. |
| ARIA_BUILD_BENCHMARK | ON | Build the micro-benchmark suite. |
| ARIA_BUILD_SHARED | ON | Runtime/binding as .dylib/.so/.dll instead of .a. |
| ARIA_BUILD_QT6 | OFF | Build the Qt6 adapter (requires Qt6Widgets). |
| ARIA_BUILD_APPKIT | OFF | (production-grade) Build the macOS AppKit adapter as a first-class STATIC CMake module using Objective-C++; ships aria::adapters::appkit and passes the shared adapter_conformance battery. Requires APPLE. |
| ARIA_BUILD_UIKIT | OFF | (production-grade) Build the iOS UIKit adapter as a first-class STATIC CMake module using Objective-C++; ships aria::adapters::uikit and passes the shared conformance battery. Requires APPLE. |
| ARIA_BUILD_JNI | OFF | Build Android JNI adapter as a first-class CMake module — built as STATIC, ships aria::adapters::jni, implementing the same IViewAdapter contract as Qt/AppKit/UIKit via reflective JNI dispatch (text / bool / int / double / visibility / click). Requires an Android NDK toolchain (NDK r26+ — the C++20-concepts core does not build under NDK r25's libc++). |
| ARIA_ENABLE_ASAN | OFF | AddressSanitizer. |
| ARIA_ENABLE_UBSAN | OFF | UndefinedBehaviorSanitizer. |
| ARIA_ENABLE_TSAN | OFF | ThreadSanitizer. |
The section above needs a UI adapter. To see the reactive core on its own, you need no UI at all:
So sub exists to express the subscription's lifetime as a scope: while the variable lives the subscription lives, and when it goes the subscription is torn down. In a real UI this Subscription is usually a member of the View, so destroying the View detaches the binding and no callback ever reaches a destroyed widget.
| Platform | UI host | Adapter |
|---|---|---|
| Windows | Qt6 | aria-qt6 ✅ ready (MSYS2 UCRT64 + MSVC 2022) |
| macOS | AppKit / Qt6 | aria-qt6 ✅ ready; AppKit ✅ ready |
| Linux | Qt6 | aria-qt6 ✅ ready |
| iOS | UIKit | aria-uikit ✅ ready |
| Android | Compose / View | aria-jni ✅ ready (NDK r26+) |
| Web (server-driven) | HTML/JS in browser | aria-http ✅ ready (REST + SSE) |
| Web (in-browser C++) | DOM via WASM | Not implemented; conditional roadmap work |
The HTTP adapter ships a small server (HttpAdapter) that exposes any ViewModel over a JSON REST + Server-Sent-Events protocol, plus a vanilla-JS browser SDK (aria_client.js). The server is built on the vendored single-header cpp-httplib (HTTP/1.1 + SSE) and nlohmann::json (encode/decode) — both committed under third_party/, so the adapter adds no new external build dependency; aria itself owns the wire protocol, view registry, subscription dispatch and SSE fan-out. It is the right shape for desktop apps that want a web UI on the side, headless services, and local debug dashboards. The WASM adapter — which compiles C++ business logic into the browser sandbox — solves a different, more constrained problem and remains conditional on a concrete consumer. See RFC 0001 for the design.
The current release ships the platform-agnostic core, runtime, async, and binding layers — fully unit-tested. Qt6, AppKit, UIKit, JNI, and HTTP are first-class opt-in adapters in the CMake tree (subject to their platform requirements). WASM and SwiftUI remain conditional roadmap work.
The big picture first — three real applications grew out of one framework:

Below is what Aria looks like in real applications — one C++ ViewModel, native shells per platform. AriaTools (17-module cross-platform workbench on Qt / iOS / Android / Web), AriaAgent (provider-agnostic LLM Agent GUI), and OpenRead (cross-platform book-source engine, HTTP/SSE web shell) all run Aria 1.x in production shape. Every screenshot comes from a stable release: one build, one shared C++ business core across platforms.
Aria's flagship example: one ViewModel, four platforms. The cart / theme switching / Framework Lab / Echo modules in the side navigation are all driven by ObservableList, Computed, and reactive::batch.
| Platform | Screenshot | Adapter |
|---|---|---|
| macOS (Qt6) | | aria-qt6 |
| iOS / UIKit | | aria-uikit |
| Android (Compose side-channel) | | aria-jni |
| Web (HTTP/REST/SSE) | | aria-http |
A provider-agnostic Agent GUI built on Aria + Qt6: true token-level streaming SSE, tool-call chain visualization, permission approval (fail-closed), Markdown rendering.
| View | Screenshot |
|---|---|
| Main chat | |
| Settings (General / Model / Plugins / Agent Presets) | |
A book-source manager powered by the Aria HTTP adapter: source list on the left, book cards on the right — search, subscribe, and debug in one place. The same C++ core drives two web shapes: a REST+SSE thin client and an SSR variant.
| View | Screenshot |
|---|---|
| Source manager (Web, REST + SSE) | |
| Source manager (Web, SSR) | |
These screenshots show the macOS example applications. Other platforms reuse the ViewModel; native appearance depends on the platform, Qt style, and host application. Consult CI for framework build and test results on Windows/Linux.
Tests exercise reactive state, collection events, async cancellation, binding lifetimes, ABI, and adapter contracts. Enabled suites depend on platform and build options. See CI results, lifecycle contracts, and the error model.
Measured on 2026-09-13: Apple M3 Pro / Apple Clang 21 / C++20 Release (-O3 -DNDEBUG). Each entry is the median of five paired runs' mean operation times, comparing original revision eeb613f with 2.0 snapshot c33850d.
| Operation | Original | Current |
|---|---|---|
| Property set, no observers | 20.5 ns | 13.2 ns |
| Property set, one observer | 98.9 ns | 35.0 ns |
| Computed chain ×5 | 570.8 ns | 229.0 ns |
| Ten sets in one batch | 250.6 ns | 102.5 ns |
| FilteredList tail append, 10k initial rows | 11.18 μs | 0.23 μs |
| SortedList random-key append, 10k initial rows | 8.38 μs | 17.28 μs |
Performance is mixed: owning event data and maintaining correct ordering after batched changes also have costs. See the performance reference for all scenarios, complexity, remaining regressions, and reproduction details.
Every non-trivial behaviour Aria promises is pinned in a numbered contract document. Each contract item carries an ID (e.g. L-13, E-22, LD-7, D-4, S-31) so a failing assertion or PR review comment can point straight at the canonical description.
| Document | Prefix | Scope |
|---|---|---|
| docs/reference/api-style.md | S-N | Naming, namespace, error and async-entry style |
| docs/reference/lifecycle.md | L-N | Threading, subscription, reactive flush, view-destroy, async cancel/dtor invariants |
| docs/reference/error-model.md | E-N | aria::Error / ErrorKind taxonomy and per-subsystem error contracts |
| docs/reference/list-diff-contract.md | LD-N | Insert / Remove / Replace / Move / Reset / ItemChanged semantics |
| docs/reference/diagnostics.md | D-N | aria::TraceEvent + aria::TraceSink protocol |
| docs/reference/performance.md | PERF-N | Complexity bounds and per-operation baselines for every public API |
The P0 hard-bedrock pass (see CHANGELOG → Latest framework-grade hardening) closed every open contract above; the seven framework-level fuzzers in modules/core/fuzz/ stress-verify the lifecycle invariants (default 50k iterations / fuzzer; nightly runs set ARIA_FUZZ_ITERS=1000000).
| Capability | Type | Where |
|---|---|---|
| Reactive state | Property<T> / Computed<T> / Effect | aria/reactive/reactive.hpp |
| Commands | Command<Args...> (reactive can_execute) | aria/command.hpp |
| Collections | ObservableList<T> + derived Filtered/Sorted/Mapped/Distinct/Grouped/Paged | aria/observable_list.hpp, aria/derived/* |
| Selection | Selection<T> / MultiSelection<T> (SE-1..SE-5) | aria/selection.hpp |
| Validation | Validator<T> / FormValidator / ValidationState + async rules | aria/validator.hpp, aria/binding/form.hpp, aria/async/async_validator.hpp |
| Async | Task<T> / AsyncCommand / with_timeout / when_any / when_all / CancellationToken | aria/async/* |
| Data fetching | AsyncResource<T> (SWR + dedupe) / Loadable<T> (5-state) | aria/async/async_resource.hpp, aria/loadable.hpp |
| Navigation | Navigator (push/pop/push_for_result<R>, route patterns) | aria/binding/navigation.hpp |
| Binding | BindingEngine / IViewAdapter / IView / Converter / bind_view_lifetime | aria/binding/* |
| Diagnostics | TraceEvent / TraceSink / GraphInspector (atomic check when disabled) | aria/diagnostics.hpp |
Learn it: the documentation index links the guides, the Cookbook (task-oriented recipes), and the contract references. The public API reference is generated from main. See the 2.0 migration guide when upgrading from 1.2.x. Build the reference locally with cmake -B build/flavors/docs -DARIA_BUILD_DOCS=ON && cmake --build build/flavors/docs --target aria_docs.
Aria is open source (MIT License), hosted on GitHub. The single source of truth for what is not yet done (and what has been deliberately deferred) lives in docs/ROADMAP.md. For the current capability snapshot, see CHANGELOG.md.
Contributions are welcome! Please open an issue first to discuss design changes.
MIT © 2026 aria contributors