Knowledge
tutorial/Jun 2, 2026·12 min read

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
note Calls are async by default and run on a worker thread. For tiny, hot functions you can opt into synchronous calls, but measure first, the FFI hop has its own cost.

How the pieces fit

[ diagram: Dart UI → generated bindings → Rust core → platform binary ]
replace with your own image or screenshot
The generated layer is the only thing that touches FFI directly. You never write it by hand.

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

  1. flutter_rust_bridge — codegen and runtime for Dart ↔ Rust.
  2. flutter_quic — a production package built on this approach.
  3. dart:ffi — the lower-level interop the bridge sits on.
Written by Shankar KakumaniKnowledge