dovetail_rust_core 0.1.1
dovetail_rust_core: ^0.1.1 copied to clipboard
Compatibility layer between Dart and Rust: a shared tokio runtime, a lag-tolerant broadcast pump, platform probing and an idempotent bridge bootstrap. No application domain inside.
English · Português
dovetail_rust_core #
The compatibility layer between Dart and Rust. It does not know what a VPN is, what a reseller is, or what your app is.
Every project that puts Flutter in front of a Rust core rewrites the same three pieces, and gets the same three wrong. This package exists so they are written once and tested once.
What it solves #
The tokio bridge. flutter_rust_bridge's executor is not a tokio reactor.
A crate using tokio::net or tokio::time — a named pipe, a unix socket, a
timer — breaks if it runs directly on it. dovetail_rust_core::run_on_runtime
keeps a multi-thread runtime in a OnceLock and spawns on it, returning a
JoinHandle any executor can await.
The slow broadcast consumer. receiver.recv() returns
Err(RecvError::Lagged) when a subscriber falls behind, and the channel
stays alive. Writing while let Ok(event) = receiver.recv().await treats
that as end of stream and freezes the screen on the last state received.
dovetail_rust_core::pump tolerates Lagged and stops only on Closed.
The platform with no binary. Calling the bridge on Android, iOS or the web
has to produce an error you can read, not a MissingPluginException from three
layers down. RustBridge.isSupported answers before any native call, and
ensureInitialized is idempotent — and allows a retry if the first attempt
fails.
What it does not do, as a rule #
No application business rule goes in here. No domain DTO, no endpoint, no validation, no dependency on a product crate. If a type in this package knows the name of an entity in your app, somebody got it wrong.
The typed facade over your Rust core lives in your bridge package, which depends on this one.
Installation #
dependencies:
dovetail_rust_core: ^0.1.0
And in your project's FFI crate Cargo.toml:
dovetail_rust_core = { path = "path/to/dovetail_rust_core/rust" }
Usage #
On the Dart side, wrapping the init the generator produced for your
crate:
import 'package:dovetail_rust_core/dovetail_rust_core.dart';
final bridge = RustBridge(initializer: RustLib.init);
if (!bridge.isSupported) return;
await bridge.ensureInitialized();
A bridge that also holds on mobile declares that:
final bridge = RustBridge(
initializer: RustLib.init,
supportedPlatforms: <TargetPlatform>{...desktopPlatforms, ...mobilePlatforms},
);
On the Rust side, in your facade:
use dovetail_rust_core::{pump, run_on_runtime, runtime};
async fn run<T, E, F>(future: F) -> Result<T, MyFailure>
where
T: Send + 'static,
E: Into<MyFailure> + Send + 'static,
F: std::future::Future<Output = Result<T, E>> + Send + 'static,
{
match run_on_runtime(future).await {
Ok(Ok(value)) => Ok(value),
Ok(Err(error)) => Err(error.into()),
Err(bridge) => Err(MyFailure::from(bridge)),
}
}
And for a stream of events:
runtime()?.spawn(async move {
pump(source.subscribe(), |event| sink.add(MyEvent::from(event)).is_ok()).await;
});
Surface #
| Side | Item | What it is |
|---|---|---|
| Dart | RustBridge |
platform probe + idempotent init |
| Dart | UnsupportedPlatformException |
an error that names the platform and the guard to use |
| Dart | desktopPlatforms · mobilePlatforms |
ready-made sets |
| Rust | runtime() |
the shared tokio runtime |
| Rust | run_on_runtime |
runs a future on it and returns the value |
| Rust | pump |
a broadcast loop that tolerates lag |
| Rust | BridgeError |
runtime unavailable, or a task that died |
Development #
flutter test
cd rust && cargo test
This package builds no native code: it is not a plugin, it has no cargokit
and no platform folder. What builds Rust is your project's bridge package.