feat(poc/bridge): add flutter_rust_bridge hello spike

Proof-of-concept proving the Flutter/Rust bridge exit criterion from
docs/architecture/proof-of-concept-plan.md §2:
  "Flutter can call Rust and receive event stream data."

The spike exposes one synchronous fallible command (greet) returning
a typed GreetResult / GreetError DTO, and one async event stream
(counter_stream) emitting typed CounterTick events. The Flutter app
demonstrates both flows on a Material 3 surface; the headless test
suite in test/poc_verification_test.dart exercises the same API
directly through dart:ffi.

Verified on 2026-05-13 (Linux desktop, Flutter 3.41.9 / Dart 3.11.5,
flutter_rust_bridge 2.12.0, Rust 1.95). All three tests pass:
  - greet() returns typed result for valid input
  - greet() surfaces typed error for empty input
  - counterStream() delivers the expected event sequence

Authority: PoC plan §2, DEC-014 (typed Flutter/Rust bridge),
SAD-068, SDD-079, SysDes-049.
Naming note: the PoC plan lists this as flutter-rust-bridge-hello,
but Dart pubspec.yaml package names require underscores; the
directory uses underscores accordingly.
Not product code; not promoted into chanora_bridge.

Layout note: includes the full Flutter platform scaffold (android,
ios, macos, windows, web, linux). Only the Linux desktop target has
been built and verified.
This commit is contained in:
EdisonJwa
2026-05-14 12:26:09 +08:00
parent 02c11ead7e
commit 2bbad5feb9
186 changed files with 11579 additions and 0 deletions
@@ -0,0 +1 @@
pub mod simple;
@@ -0,0 +1,97 @@
//! Chanora PoC API — `flutter_rust_bridge_hello`.
//!
//! Two flows are proven here:
//! 1. **Command:** `greet` — typed input → typed `Result<T, E>` DTO,
//! including typed error mapping at the boundary.
//! 2. **Event stream:** `counterStream` — Rust pushes typed events to
//! Dart via a `StreamSink`, observed in Dart as a `Stream<CounterTick>`.
//!
//! Together they satisfy the PoC exit criterion from
//! `docs/architecture/proof-of-concept-plan.md` §2:
//! "Flutter can call Rust and receive event stream data."
//!
//! This is PoC code only. The Chanora DTO catalogue belongs to the
//! `chanora_bridge` crate; this PoC does not stand in for it.
use std::time::Duration;
use flutter_rust_bridge::frb;
use crate::frb_generated::StreamSink;
// -------- Command DTOs --------
#[derive(Debug, Clone)]
pub struct GreetResult {
pub message: String,
pub elapsed_micros: u64,
}
#[derive(Debug, Clone)]
pub enum GreetErrorKind {
EmptyName,
}
/// Typed error DTO surfaced as a Dart exception by FRB.
#[derive(Debug, Clone, thiserror::Error)]
#[error("greet error: {kind:?}: {detail}")]
pub struct GreetError {
pub kind: GreetErrorKind,
pub detail: String,
}
// -------- Lifecycle --------
#[frb(init)]
pub fn init_app() {
flutter_rust_bridge::setup_default_user_utils();
}
// -------- Command --------
/// Synchronous fallible command. Proves Dart → Rust call with a typed
/// `Result` DTO crossing the boundary.
#[frb(sync)]
pub fn greet(name: String) -> Result<GreetResult, GreetError> {
let start = std::time::Instant::now();
let trimmed = name.trim();
if trimmed.is_empty() {
return Err(GreetError {
kind: GreetErrorKind::EmptyName,
detail: "name must not be empty".into(),
});
}
Ok(GreetResult {
message: format!("Hello, {trimmed}! — greetings from Rust."),
elapsed_micros: start.elapsed().as_micros() as u64,
})
}
// -------- Event stream --------
#[derive(Debug, Clone)]
pub struct CounterTick {
pub seq: u64,
pub note: String,
}
/// Rust → Dart event stream.
/// `StreamSink` is provided in scope by `flutter_rust_bridge` glue;
/// the codegen wires it to a Dart `Stream<CounterTick>`.
pub async fn counter_stream(
sink: StreamSink<CounterTick>,
count: u32,
interval_ms: u32,
) {
let interval = Duration::from_millis(interval_ms.max(1) as u64);
for i in 0..count {
let tick = CounterTick {
seq: i as u64,
note: format!("tick {i}/{count}"),
};
if sink.add(tick).is_err() {
break;
}
tokio::time::sleep(interval).await;
}
}
@@ -0,0 +1,558 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
#![allow(
non_camel_case_types,
unused,
non_snake_case,
clippy::needless_return,
clippy::redundant_closure_call,
clippy::redundant_closure,
clippy::useless_conversion,
clippy::unit_arg,
clippy::unused_unit,
clippy::double_parens,
clippy::let_and_return,
clippy::too_many_arguments,
clippy::match_single_binding,
clippy::clone_on_copy,
clippy::let_unit_value,
clippy::deref_addrof,
clippy::explicit_auto_deref,
clippy::borrow_deref_ref,
clippy::uninlined_format_args,
clippy::needless_borrow
)]
// Section: imports
use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
use flutter_rust_bridge::{Handler, IntoIntoDart};
// Section: boilerplate
flutter_rust_bridge::frb_generated_boilerplate!(
default_stream_sink_codec = SseCodec,
default_rust_opaque = RustOpaqueMoi,
default_rust_auto_opaque = RustAutoOpaqueMoi,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1685941124;
// Section: executor
flutter_rust_bridge::frb_generated_default_handler!();
// Section: wire_funcs
fn wire__crate__api__simple__counter_stream_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "counter_stream",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_sink = <StreamSink<
crate::api::simple::CounterTick,
flutter_rust_bridge::for_generated::SseCodec,
>>::sse_decode(&mut deserializer);
let api_count = <u32>::sse_decode(&mut deserializer);
let api_interval_ms = <u32>::sse_decode(&mut deserializer);
deserializer.end();
move |context| async move {
transform_result_sse::<_, ()>(
(move || async move {
let output_ok = Result::<_, ()>::Ok({
crate::api::simple::counter_stream(
api_sink,
api_count,
api_interval_ms,
)
.await;
})?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__api__simple__greet_impl(
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "greet",
port: None,
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_name = <String>::sse_decode(&mut deserializer);
deserializer.end();
transform_result_sse::<_, crate::api::simple::GreetError>((move || {
let output_ok = crate::api::simple::greet(api_name)?;
Ok(output_ok)
})())
},
)
}
fn wire__crate__api__simple__init_app_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "init_app",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let message = unsafe {
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
ptr_,
rust_vec_len_,
data_len_,
)
};
let mut deserializer =
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
deserializer.end();
move |context| {
transform_result_sse::<_, ()>((move || {
let output_ok = Result::<_, ()>::Ok({
crate::api::simple::init_app();
})?;
Ok(output_ok)
})())
}
},
)
}
// Section: dart2rust
impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <String>::sse_decode(deserializer);
return flutter_rust_bridge::for_generated::anyhow::anyhow!("{}", inner);
}
}
impl SseDecode
for StreamSink<crate::api::simple::CounterTick, flutter_rust_bridge::for_generated::SseCodec>
{
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <String>::sse_decode(deserializer);
return StreamSink::deserialize(inner);
}
}
impl SseDecode for String {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <Vec<u8>>::sse_decode(deserializer);
return String::from_utf8(inner).unwrap();
}
}
impl SseDecode for crate::api::simple::CounterTick {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_seq = <u64>::sse_decode(deserializer);
let mut var_note = <String>::sse_decode(deserializer);
return crate::api::simple::CounterTick {
seq: var_seq,
note: var_note,
};
}
}
impl SseDecode for crate::api::simple::GreetError {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_kind = <crate::api::simple::GreetErrorKind>::sse_decode(deserializer);
let mut var_detail = <String>::sse_decode(deserializer);
return crate::api::simple::GreetError {
kind: var_kind,
detail: var_detail,
};
}
}
impl SseDecode for crate::api::simple::GreetErrorKind {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut inner = <i32>::sse_decode(deserializer);
return match inner {
0 => crate::api::simple::GreetErrorKind::EmptyName,
_ => unreachable!("Invalid variant for GreetErrorKind: {}", inner),
};
}
}
impl SseDecode for crate::api::simple::GreetResult {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_message = <String>::sse_decode(deserializer);
let mut var_elapsedMicros = <u64>::sse_decode(deserializer);
return crate::api::simple::GreetResult {
message: var_message,
elapsed_micros: var_elapsedMicros,
};
}
}
impl SseDecode for i32 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
deserializer.cursor.read_i32::<NativeEndian>().unwrap()
}
}
impl SseDecode for Vec<u8> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut len_ = <i32>::sse_decode(deserializer);
let mut ans_ = Vec::with_capacity(len_ as usize);
for idx_ in 0..len_ {
ans_.push(<u8>::sse_decode(deserializer));
}
return ans_;
}
}
impl SseDecode for u32 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
deserializer.cursor.read_u32::<NativeEndian>().unwrap()
}
}
impl SseDecode for u64 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
deserializer.cursor.read_u64::<NativeEndian>().unwrap()
}
}
impl SseDecode for u8 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
deserializer.cursor.read_u8().unwrap()
}
}
impl SseDecode for () {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {}
}
impl SseDecode for bool {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
deserializer.cursor.read_u8().unwrap() != 0
}
}
fn pde_ffi_dispatcher_primary_impl(
func_id: i32,
port: flutter_rust_bridge::for_generated::MessagePort,
ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len: i32,
data_len: i32,
) {
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
match func_id {
1 => wire__crate__api__simple__counter_stream_impl(port, ptr, rust_vec_len, data_len),
3 => wire__crate__api__simple__init_app_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
fn pde_ffi_dispatcher_sync_impl(
func_id: i32,
ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len: i32,
data_len: i32,
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
match func_id {
2 => wire__crate__api__simple__greet_impl(ptr, rust_vec_len, data_len),
_ => unreachable!(),
}
}
// Section: rust2dart
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::api::simple::CounterTick {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.seq.into_into_dart().into_dart(),
self.note.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::api::simple::CounterTick
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::api::simple::CounterTick>
for crate::api::simple::CounterTick
{
fn into_into_dart(self) -> crate::api::simple::CounterTick {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::api::simple::GreetError {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.kind.into_into_dart().into_dart(),
self.detail.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::api::simple::GreetError
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::api::simple::GreetError>
for crate::api::simple::GreetError
{
fn into_into_dart(self) -> crate::api::simple::GreetError {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::api::simple::GreetErrorKind {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
match self {
Self::EmptyName => 0.into_dart(),
_ => unreachable!(),
}
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::api::simple::GreetErrorKind
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::api::simple::GreetErrorKind>
for crate::api::simple::GreetErrorKind
{
fn into_into_dart(self) -> crate::api::simple::GreetErrorKind {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::api::simple::GreetResult {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.message.into_into_dart().into_dart(),
self.elapsed_micros.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::api::simple::GreetResult
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::api::simple::GreetResult>
for crate::api::simple::GreetResult
{
fn into_into_dart(self) -> crate::api::simple::GreetResult {
self
}
}
impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<String>::sse_encode(format!("{:?}", self), serializer);
}
}
impl SseEncode
for StreamSink<crate::api::simple::CounterTick, flutter_rust_bridge::for_generated::SseCodec>
{
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
unimplemented!("")
}
}
impl SseEncode for String {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<Vec<u8>>::sse_encode(self.into_bytes(), serializer);
}
}
impl SseEncode for crate::api::simple::CounterTick {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<u64>::sse_encode(self.seq, serializer);
<String>::sse_encode(self.note, serializer);
}
}
impl SseEncode for crate::api::simple::GreetError {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<crate::api::simple::GreetErrorKind>::sse_encode(self.kind, serializer);
<String>::sse_encode(self.detail, serializer);
}
}
impl SseEncode for crate::api::simple::GreetErrorKind {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<i32>::sse_encode(
match self {
crate::api::simple::GreetErrorKind::EmptyName => 0,
_ => {
unimplemented!("");
}
},
serializer,
);
}
}
impl SseEncode for crate::api::simple::GreetResult {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<String>::sse_encode(self.message, serializer);
<u64>::sse_encode(self.elapsed_micros, serializer);
}
}
impl SseEncode for i32 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
serializer.cursor.write_i32::<NativeEndian>(self).unwrap();
}
}
impl SseEncode for Vec<u8> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<i32>::sse_encode(self.len() as _, serializer);
for item in self {
<u8>::sse_encode(item, serializer);
}
}
}
impl SseEncode for u32 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
serializer.cursor.write_u32::<NativeEndian>(self).unwrap();
}
}
impl SseEncode for u64 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
serializer.cursor.write_u64::<NativeEndian>(self).unwrap();
}
}
impl SseEncode for u8 {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
serializer.cursor.write_u8(self).unwrap();
}
}
impl SseEncode for () {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {}
}
impl SseEncode for bool {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
serializer.cursor.write_u8(self as _).unwrap();
}
}
#[cfg(not(target_family = "wasm"))]
mod io {
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// Section: imports
use super::*;
use flutter_rust_bridge::for_generated::byteorder::{
NativeEndian, ReadBytesExt, WriteBytesExt,
};
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
use flutter_rust_bridge::{Handler, IntoIntoDart};
// Section: boilerplate
flutter_rust_bridge::frb_generated_boilerplate_io!();
}
#[cfg(not(target_family = "wasm"))]
pub use io::*;
/// cbindgen:ignore
#[cfg(target_family = "wasm")]
mod web {
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.12.0.
// Section: imports
use super::*;
use flutter_rust_bridge::for_generated::byteorder::{
NativeEndian, ReadBytesExt, WriteBytesExt,
};
use flutter_rust_bridge::for_generated::wasm_bindgen;
use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*;
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
use flutter_rust_bridge::{Handler, IntoIntoDart};
// Section: boilerplate
flutter_rust_bridge::frb_generated_boilerplate_web!();
}
#[cfg(target_family = "wasm")]
pub use web::*;
@@ -0,0 +1,2 @@
pub mod api;
mod frb_generated;