Calling Rust from Flutter: a practical guide to FFI
Dart will take you a long way, until you need raw performance, real cryptography, or a library that only exists in Rust. Here's how I bridge the two with flutter_rust_bridge, end to end, without losing my mind over manual FFI.
Every few months I hit the same wall: a Flutter feature that Dart just isn't the right tool for. Audio processing, a parser that needs to be fast, a crypto primitive I don't want to reimplement. Rust already has a crate for it, and rewriting that crate in Dart would be both slower and a waste of a weekend.
So instead of fighting it, I reach across the FFI boundary. This is the workflow I've settled on after shipping it in production a few times.
When Rust actually earns its place
FFI is not free. You're adding a build toolchain, a binary per platform, and a layer that can crash in ways Dart never will. Reach for it when the upside is real:
- Compute-bound work, image and signal processing, compression, anything where you'd feel a 5–10× difference.
- Existing Rust crates you'd rather reuse than port, transport stacks, parsers, ML runtimes.
- Shared logic you want identical across mobile, desktop and the server.
If it's a bit of glue code, keep it in Dart. The boundary is the cost.
Setting up the bridge
I use flutter_rust_bridge, which generates the binding code in both directions so you write plain Rust and call plain Dart. Add the Rust crate inside your Flutter project and wire up the dependencies:
rust/Cargo.toml
[lib]
crate-type = ["staticlib", "cdylib"]
[dependencies]
flutter_rust_bridge = "2"
pubspec.yaml
dependencies:
flutter_rust_bridge: ^2.0.0
ffi: ^2.1.0
Writing the Rust side
Annotate the functions you want exposed. The codegen turns these into a typed Dart API, including String, Vec<T> and your own structs.
rust/src/api.rs
// A function we want to call from Dart.
pub fn word_count(input: String) -> usize {
input.split_whitespace().count()
}
Run the generator and it writes the Dart bindings for you:
$ flutter_rust_bridge_codegen generate
Calling it from Dart
From Flutter it looks like any other async Dart call, the heavy work runs off the UI thread on a Rust-managed isolate, so your frames stay smooth.
lib/main.dart
final count = await api.wordCount(input: 'hello from flutter');
print(count); // 3
How the pieces fit
replace with your own image or screenshot
Shipping on Android & iOS
This is where most guides hand-wave, so: on Android the crate compiles to .so files per ABI and rides along in the APK; on iOS it's a static library linked into the app. flutter_rust_bridge's build hooks handle the cross-compilation in CI, I run it on Codemagic and never think about it again.
The goal isn't to write Rust everywhere. It's to make the boundary so cheap that "should this be in Rust?" becomes a normal design question, not a heroic effort.
Wrapping up
Once the bridge is in place, reaching for Rust stops feeling exotic. I keep a small rust/ crate in most of my serious Flutter apps now, empty until I need it, ready the moment I do.
If you want a real-world example, my flutter_quic package wraps a Rust QUIC stack exactly this way.
References
- flutter_rust_bridge — codegen and runtime for Dart ↔ Rust.
- flutter_quic — a production package built on this approach.
- dart:ffi — the lower-level interop the bridge sits on.