The documentation of flutter_rust_bridge v1.

Quickstart

Write down Rust functions and types normally.

// A normal Rust function ...
pub fn draw_tree(root: TreeNode, mode: DrawMode) -> Result<Vec<u8>> { /* ... */ }

// ... with rich types
pub struct TreeNode { pub value: String, pub children: Vec<MyTreeNode> }
pub enum DrawMode { Colorful {palette: String}, Grayscale }

Install the code generator flutter_rust_bridge_codegen:

cargo install flutter_rust_bridge_codegen
# or with cargo-binstall
cargo binstall flutter_rust_bridge_codegen
# or with scoop (Windows)
scoop bucket add frb https://github.com/Desdaemon/scoop-repo
scoop install flutter_rust_bridge_codegen
# or with Homebrew
brew install desdaemon/repo/flutter_rust_bridge_codegen

(Remark: Thanks @Desdaemon for scripts to publish to brew/scoop)

Then run the code generator.

Remark: It needs some installation steps. You may refer to the tutorial, create new projects from a template or integrating with existing projects for details.

flutter_rust_bridge_codegen --rust-input path/to/api.rs \
                            --dart-output path/to/bridge_generated.dart

With bindings automatically generated, use it seamlessly in Flutter/Dart:

api.drawTree(TreeNode(value: "root", ...), Colorful(palette: "viridis"));

Tutorial: A Flutter+Rust app

In this tutorial, let us draw a Mandelbrot set (a well-known infinite-resolution "image" generated by a simple math formula). The image is plotted in Flutter UI, generated by Rust algorithm, and communicated via this library.

(Click to see: What is a Mandelbrot set)

The Mandelbrot set is the set of complex numbers c for which the function f_c(z)=z^{2}+c does not diverge to infinity when iterated from z=0. Images of the Mandelbrot set exhibit an elaborate and infinitely complicated boundary that reveals progressively ever-finer recursive detail at increasing magnifications.

Image credit: Simpsons contributor

Get code

Please install Flutter (optionally with desktop support if you want to run app on desktop instead of cellphones), install Rust, and have some familiarity with them. Then get the example codebase:

git clone https://github.com/fzyzcjy/flutter_rust_bridge && cd flutter_rust_bridge/frb_example/with_flutter

Optional: Run generator

This step is optional, since I have generated the source code already (in quickstart). Even if you do it, you should not see anything changed.

As soon as you make any modification to api.rs, you need to run codegen again. More information about requirements for code generation can be seen in the Installing dependencies section.

At this step you may need to setup dependencies.

Run app

Prelogue: Command details

The CI workflow is useful if you want details of each command. The flutter_android_test, flutter_ios_test, flutter_windows_test, flutter_macos_test and flutter_linux_test demonstrates the exact commands needed to run this tutorial codebase from a brand new machine.

Android app

See Android setup

iOS app

Modify Cargo.toml to change cdylib to staticlib, then run cargo lipo && cp target/universal/debug/libflutter_rust_bridge_example.a ../ios/Runner to build Rust and copy the static library. Then run the Flutter app normally such as flutter run.

Remark: This tutorial will help you automatically execute cargo builds when building Flutter app.

Windows app

Run it directly using flutter run assuming Flutter desktop support has been configured. More details can be seen in #66.

Linux app

Same as Windows. If you install Flutter through snap, please be wary of #53.

MacOS app

Same as Windows. (P.S. Under the hood, cargo-xcode is used to automate the process)

Web (as a webpage)

Install flutter_rust_bridge_serve to simplify the process of building and serving a WASM binary. See Web setup for more details.

Android setup

JDK 8

Android Studio depends on the javax library being present in the Java runtime, and the only reliable way to ensure this is to install an older version of Java. On Unix-like systems, you can use asdf or similar tools to manage your Java versions, and the template defines a known working version of Java in the .tool-versions file.

Android NDK

An issue regarding building Rust's core library against the latest NDK means that when using Rust 1.67 and lower, only NDK versions 22 and older can be used. The issue has been patched in Rust 1.68, which is not yet stable at the time of writing.

Rust < 1.68:

Android Studio > SDK Manager > SDK Tools > uncheck Hide Obsolete Packages > NDK (version 22)

Rust >= 1.68:

Android Studio > SDK Manager > SDK Tools > NDK (side by side)

The Android NDK, or Native Development Kit, enables code written in other languages to be run on the JVM via the Java Native Interface, or JNI for short. In this case, we would like to pass the dynamic libraries created by Cargo to be included in the bundle when we run or build the project.

After following the instructions above, the NDK should be installed in your $ANDROID_HOME/ndk folder, where ANDROID_HOME usually is:

  • on Windows: %APPDATA%\Local\Android\sdk
  • on MacOS: ~/Library/Android/sdk
  • on Linux: set via the environment variable ANDROID_HOME, or ~/Android/sdk

ANDROID_NDK Gradle property

echo "ANDROID_NDK=(path to NDK)" >> ~/.gradle/gradle.properties

Next, you need to make this NDK visible to Gradle. The way to do this depends on your current system and is unlikely to be portable, but generally you can add a gradle.properties in your ~/.gradle folder like this:

ANDROID_NDK=(path to NDK)

or edit one of the gradle.properties that resides within the android folder.

cargo-ndk

cargo-ndk is a Cargo plugin for compiling code suitable for plugging into the JNI without additional configuration. Version 2.7.0 of cargo-ndk introduced changes that broke support for NDK version 22, so 2.6.0 must be used if you are on a Rust version below 1.68. If you still want to use cargo-ndk 2.7.0 or above on Rust versions below 1.68 with a workaround, see this article.

Rust < 1.68:

cargo install cargo-ndk --version 2.6.0

Rust >= 1.68:

cargo install cargo-ndk

Then run (all Rust versions)

cargo ndk -o ../android/app/src/main/jniLibs build

Then run the Flutter app normally with flutter run.

Remark: This tutorial will help you automatically execute cargo builds when building Flutter app.

Alternative NDK setup

This is only needed if you wish to use a version of the Android NDK higher than version 22 with versions of Rust that are lower than version 1.68. This guide details how to prevent the unable to find library -lgcc error.

Android NDK

Install the latest NDK:

Android Studio > SDK Manager > SDK Tools > NDK (Side by side)

Click on OK at the bottom right corner to start the installation.

cargo-ndk

You should install cargo-ndk version 2.7.0 or above which works for Android NDK versions greater than 22.

cargo install cargo-ndk --version ^2.7.0

A workaround may be under development in the cargo-ndk project. Until it is finished, you need to manually create four text files to redirect calls from libgcc to libunwind (reference):

  1. Find out all the 4 folders containing file libunwind.a.

    • On Windows, it is similar to:

      C:\Users\Administrator\AppData\Local\Android\Sdk\ndk\24.0.8215888\toolchains\llvm\prebuilt\windows-x86_64\lib64\clang\14.0.1\lib\linux\x86_64\
      
    • On macOS Monterey, it is similar to:

      ~/Library/Android/sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/14.0.1/lib/linux/x86_64/
      

    The three other folders end with aarch64, arm, i386 instead of x86_64.

  2. Create 4 text files named libgcc.a in the four folders mentioned above with these contents

    INPUT(-lunwind)
    

More details on NDK with flutter_rust_bridge

For more details on how NDK works with flutter_rust_bridge, have a look at this article please.

Features

In this chapter, we are going to demonstrate the features. Please use the menu bar at the left / left-top of the page to navigate.

Prelogue

What this library is and is not

This library is nothing but a code generator that helps your Flutter/Dart functions call Rust functions. It only generates some boilerplate code that you will manually write down otherwise. Moreover, we have provided detailed tutorials for you to play with examples, set up brand new apps, and integrate with existing apps.

Of course, you may still need to have some basic familiarity with Flutter/Dart, Rust, and its ffi. (Link for Android, iOS and macOS)

Full examples

If you want to look at a lot of examples - I have to warn you, really too many - have a look at pure_dart's api.rs. It contains all tests for this library.

In addition, when you are quite familiar with the basic example, you can then take a look at pure_dart_multi. This example contains multiple blocks of APIs instead of one, which is quite useful for complex projects.

Language translations

In this section, we will show how various language features are translated between Rust and Dart.

Simple correspondence

Here is a brief glance showing what the code generator can generate (non-exhaustive). Some rows have hyper-links pointing to more detailed explanations.

RustDart
Vec<u8>..Vec<u64>Uint8List..Uint64List
Vec<i8>..Vec<i64>Int8List..Int64List
Vec<f32>, Vec<f64>Float32List, Float64List
Vec<T>List<T>
[T; N]List<T>
struct { .. }, struct( .. )class
enum { A, B }enum
enum { A(..) }@freezed sealed class
use ...act normally
Option<T>T?
Arbitrary Rust types (opaque)RustOpaque
DartOpaqueArbitrary Dart types (opaque)
Result::Err, panicthrow Exception
Box<T>T
commentssame
i8, u8, .., usizeint
f32, f64double
boolbool
StringString
()void
type A = Btype alias
(T, U, ..)(T, U, ..)

Types from chrono crate are supported as a feature, see here. Types from uuid crate are supported as a feature, see here.

Raw strings are supported for struct field names. For example, you can have struct S { r#type: i32 }. In dart, the r# prefix will be correctly removed. They are not yet supported for function arguments.

Vec and array

Vec<u8>, Vec<i8>, ...

In Dart, when you want to express a long byte array such as a big image or some binary blob, people normally use Uint8List instead of List<int> since the former is much performant. flutter_rust_bridge takes this into consideration for you. When you have Vec<u8> (or Vec<i8>, or Vec<i32>, etc), it will be translated into Uint8List or its friends.

This section provides more details about Vec<u8> and its friends.

Vec<T>

When you have normal Vec<T> for T types other than u8, i8 etc, it will be converted to normal List<T>.

Remark: Vec<Box<T>> is not supported yet though fixable (#1072), but according to clippy lints, it is usually better to use Vec<T> directly.

[T; N]

Since Dart does not have special treatment for static-sized arrays, it is converted to List<T> as well.

Example

pub fn draw_tree(tree: Vec<TreeNode>) -> Vec<u8> { ... }

Becomes:

Future<Uint8List> drawTree({required List<TreeNode> tree});

Remark: If you are curious about Future, have a look at this.

structs

Normal Rust structs are supported. You can even use recursive fields, such as pub struct TreeNode { pub value: String, pub children: Vec<MyTreeNode>, pub parent: Box<MyTreeNode> }.

For versions older than v1.66.0 (no need for latest version), if a struct field has type being a struct or an enum, please add a Box on it, or it will lead to compile-time error. For example, struct A {b: B} should be struct A {b: Box<B>} instead.

Tuple structs

Tuple structs struct Foo(A, B) are translated as class Foo { A field0; B field1; }, since Dart does not have anonymous fields.

Non-final fields

By adding #[frb(non_final)] to a field of struct, the corresponding field in Dart will be non-final. By default, we make all generated fields final because of Rust's philosophy - immutable by default.

Unless a field has been annotated with #[frb(non_final)], generated classes will also be const-constructible.

Dart metadata annotations

You can add dart metadata annotations using dart_metadata parameter in frb macro.

  • For annotations that are prelude by dart (e.g. @deprecated), just put annotation as a Rust literal.
  • If importing is needed, then add importing part behind the annotation string. Currently two forms of importing supported:
    • import 'somepackage'
    • import 'somepackage' as somename, where somename will be the prefix of the annotation
  • Multiple annotations are separated by comma ,.

See below for an example.

freezed Dart classes

If you want the generated Dart class to be freezed (which is like data-classes in other languages like Kotlin), simply put #[frb(dart_metadata=("freezed"))] and it will generate everything needed for you.

Example

Example 1: Recursive fields

pub struct MyTreeNode {
    pub value: Vec<u8>,
    pub children: Vec<MyTreeNode>,
}

Becomes:

class MyTreeNode {
  final Uint8List value;
  final List<MyTreeNode> children;
  MyTreeNode({required this.value, required this.children});
}

Remark: If you are curious about Future, have a look at this.

Example 2: Metadata

#[frb(dart_metadata=("freezed", "immutable" import "package:meta/meta.dart" as meta))]
pub struct UserId {
    pub value: u32,
}

Becomes:

import 'package:meta/meta.dart' as meta;

@freezed
@meta.immutable
class UserId with _$UserId {
  const factory UserId({
    required int value,
  }) = _UserId;
}

enums

Rust's enum are known to be very expressive and powerful - it allows each enum variant to have different associated data. Dart does not have such things in built-in enums, but no worries - we will automatically translate it into the equivalent using the freezed Dart library. The syntax for freezed may look a bit strange at the first glance, but please look at its doc and see its powerfulness.

Example

pub enum KitchenSink {
    Empty,
    Primitives {
        /// Dart field comment
        int32: i32,
        float64: f64,
        boolean: bool,
    },
    Nested(Box<KitchenSink>),
    Optional(
        /// Comment on anonymous field
        Option<i32>,
        Option<i32>,
    ),
    Buffer(ZeroCopyBuffer<Vec<u8>>),
    Enums(Weekdays),
}

Becomes:

@freezed
class KitchenSink with _$KitchenSink {
  /// Comment on variant
  const factory KitchenSink.empty() = Empty;
  const factory KitchenSink.primitives({
    /// Dart field comment
    required int int32,
    required double float64,
    required bool boolean,
  }) = Primitives;
  const factory KitchenSink.nested(
    KitchenSink field0,
  ) = Nested;
  const factory KitchenSink.optional([
    /// Comment on anonymous field
    int? field0,
    int? field1,
  ]) = Optional;
  const factory KitchenSink.buffer(
    Uint8List field0,
  ) = Buffer;
  const factory KitchenSink.enums(
    Weekdays field0,
  ) = Enums;
}

And they are powered with all functionalities of freezed.

Remark: If you are curious about Future, have a look at this.

Pattern matching in Dart

Introduced in Dart 3, sealed classes can be used to pattern match values, enabling exhaustive variant checks and refuable patterns among other capabilities. Refer to the documentation for more details.

This feature supersedes Freezed's map and when families of methods. You can opt out of generating sealed classes by passing --no-dart3 when running codegen.

pub enum Maybe {
    None,
    Some { value: i32 },
}
Maybe maybe;
final value = switch (maybe) {
  Maybe_None() => 'got nothing',
  Maybe_Some(:final value) => 'got value: $value',
};
// single case à la if-let
if (maybe case Maybe_Some(:final value)) {
  ..
}

Tuples

Introduced in Dart 3, records provide the equivalent of Rust's tuples. Tuples of up to 10 elements are supported, and more can be added by nesting tuples. Tuples can be returned, received as parameters, and stored inside structs.

pub fn my_coordinate() -> (f64, f64);
(double, double) myCoordinate();
final (lat, long) = myCoordinate();

External types

Types in other files within the same crate

Imported symbols can be used normally. For example, with use crate::data::{MyEnum, MyStruct};, you can use MyEnum or MyStruct in your code normally.

Example

use crate::data::{MyEnum, MyStruct};

pub fn use_imported_things(my_struct: MyStruct, my_enum: MyEnum) { ... }

Becomes:

// Well it just behaves normally as you expect
Future<void> useImportedThings({required MyStruct myStruct, required MyEnum myEnum});

Remark: If you are curious about Future, have a look at this.

Types in other crates

The feature is called "mirroring". In short, you need to define the type again mirroring the external type that you want to use. That definition is only used at code-generation time to tell flutter_rust_bridge type information. To see exact grammar, have a look at the example below.

No need to worry whether this breaks the DRY principle, or what happens when you accidentally write down a wrong field. This is because compile errors will happen if your mirrored type is not exactly same as the original type.

More information: #352

When multiple structs have the same fields, you can mirror them once using grammar like #[frb(mirror(FirstStruct, SecondStruct, ThirdStruct))]. (#619)

Example

// Mirroring example:
// The goal of mirroring is to use external objects without needing to convert them with an intermediate type
// In this case, the struct ApplicationSettings is defined in another crate (called external-lib)

// To use an external type with mirroring, it MUST be imported publicly (aka. re-export)
pub use external_lib::{ApplicationEnv, ApplicationMode, ApplicationSettings};

// To mirror an external struct, you need to define a placeholder type with the same definition
#[frb(mirror(ApplicationSettings))]
pub struct _ApplicationSettings {
    pub name: String,
    pub version: String,
    pub mode: ApplicationMode,
    pub env: Box<ApplicationEnv>,
}

// It works with basic enums too
// Enums with struct variants are not yet supported
#[frb(mirror(ApplicationMode))]
pub enum _ApplicationMode {
    Standalone,
    Embedded,
}

#[frb(mirror(ApplicationEnv))]
pub struct _ApplicationEnv {
    pub vars: Vec<String>,
}

// This function can directly return an object of the external type ApplicationSettings because it has a mirror
pub fn get_app_settings() -> ApplicationSettings {
    external_lib::get_app_settings()
}

// Similarly, receiving an object from Dart works. Please note that the mirror definition must match entirely and the original struct must have all its fields public.
pub fn is_app_embedded(app_settings: ApplicationSettings) -> bool {
    // println!("env: {}", app_settings.env.vars[0]);
    match app_settings.mode {
        ApplicationMode::Standalone => false,
        ApplicationMode::Embedded => true,
    }
}

Another example using one struct to mirror multiple structs:

// *no* need to do these
#[frb(mirror(MessageId))]
pub struct MId(pub [u8; 32]);
#[frb(mirror(BlobId))]
pub struct BId(pub [u8; 32]);
#[frb(mirror(FeedId))]
pub struct FId(pub [u8; 32]);

// simply do this is sufficient
#[frb(mirror(MessageId, BlobId, FeedId))]
pub struct Id(pub [u8; 32]);

Options

Dart has special syntaxs for nullable variables - the ? symbol, and we translate Option into ? automatically. You may refer to the official doc for more information.

In addition, flutter_rust_bridge also understands the required keyword in Dart: If an argument is not-null, it is marked as required since you have to provide a value. On the other hand, if it is nullable, no required is needed since by Dart's convention a null is there in absence of manually providing a value.

Example

pub struct Element {
    pub tag: Option<String>,
    pub text: Option<String>,
    pub attributes: Option<Vec<Attribute>>,
    pub children: Option<Vec<Element>>,
}

pub fn parse(mode: String, document: Option<String>) -> Option<Element> { ... }

Becomes:

Future<Element?> handleOptionalStruct({required String mode, String? document});

class Element {
  final String? tag;
  final String? text;
  final List<Attribute>? attributes;
  final List<Element>? children;
  Element({this.tag, this.text, this.attributes, this.children});
}

Remark: If you are curious about Future, have a look at this.

Methods

There is support for structs with methods. Both static methods, and non-static methods are supported.

Related configuration: --no-use-bridge-in-method (see below for an example).

Example

pub struct SumWith { pub x: u32 }

impl SumWith {
    pub fn sum(&self, y: u32) -> u32 { self.x + y }
    pub fn sum_static(x: u32, y: u32) -> u32 { x + y }
}

Becomes:

class SumWith {
  final FlutterRustBridgeExampleSingleBlockTest bridge;
  final int x;

  SumWith({
    required this.bridge,
    required this.x,
  });

  Future<int> sum({required int y, dynamic hint}) => ..
  static Future<int> sum({required int x, required int y, dynamic hint}) => ..
}

Or show as follow if you use flag --no-use-bridge-in-method:

class SumWith {
  final int x;

  const SumWith({
    required this.x,
  });

  Future<int> sum({required int y, dynamic hint}) => api.sumMethodSumWith(
        that: this,
        y: y,
      );

  static Future<int> sumStatic({required int x, required int y, dynamic hint}) =>
      api.sumStaticStaticMethodSumWith(x: x, y: y, hint: hint);
}

Remark: If you are curious about Future, have a look at this.

Return Types

The return type can be either anyhow::Result<YourType>, or YourType directly. For exceptions (errors), please refer to exceptions section as well.

Example

pub fn f(a: i32, b: i32) -> i32 { a + b }

pub fn g(a: i32, b: i32) -> anyhow::Result<i32> { Ok(a + b) }

Dynamic Types

Dart's dynamic is a special type that can hold any type of value. Although it is possible to return a dynamic to Dart in the form of DartAbi, it is preferable to return an enum instead that has a variant for each type you want to support.

Example

Let's say you have a struct that can hold either a u32 or a String and some other fields (in a significantly worse design):

struct MyStruct {
    a: Optional<u32>,
    b: Optional<String>,
}

struct DataStruct {
    msg:  String,
    data: MyStruct,
}

You can define an enum in Rust to represent this:

enum MyEnum {
    U32(u32),
    String(String),
}

And then you can define a struct that holds this enum:

struct MyStruct {
    msg:  String,
    data: MyEnum,
}

Returning dynamics

Aside from DartOpaque, you may also return a dynamic type to Dart by specifing the return type as DartAbi. DartAbi is the umbrella type for all C-representable Dart values, which can be obtained from Rust types that implement IntoDart.

pub fn return_dynamic() -> DartAbi {
    vec![
        ().into_dart(),
        0i32.into_dart(),
        format!("Hello there!").into_dart()
    ].into_dart()
}
final dynamic values = await api.returnDynamic();
assert(values is List<dynamic>);
assert(values[0] == null);
assert(values[1] == 0);
assert(values[2] == "Hello there!");

DartAbi is not supported as parameters, and structs that transitively include them may not be used in parameter positions either. If you only care about accepting or returning an opaque Dart object without interacting with it, consider DartOpaque.

This type is meant to be used only as an esacpe hatch, if your data cannot be expressed as either a fixed struct or enum.

Arbitrary Rust types (opaque)

On one hand, any Rust type, even if it is not supported using features of this library, can be used in Dart. This is done by wrapping it with RustOpaque.

The Rust opaque objects in Dart should be disposed manually, though it will also be disposed when it is GCed, that is discouraged, due to suggestions by Dart team. Think of it just like a lot of Flutter objects that we are familiar with, such as ui.Image - we have to manually dispose them as well.

Different from non-opaque types, opaque types are not copied/moved/reconstructed at all. For example, if you pass around RwLock<Mutex<ArbitraryData> in arguments and return values, you will get the exact same RwLock<ArbitraryData> object.

Example RustOpaque

Rust:

struct ArbitraryData { ... }
pub fn use_opaque(a: RustOpaque<ArbitraryData>) { ... }
pub fn even_use_locks(b: RustOpaque<Mutex<ArbitraryData>) -> RustOpaque<RwLock<ArbitraryData>> { ... }
enum AnEnumContainingOpaque { Hello(RustOpaque<ArbitraryData>), World(i32) }
...

And use it in Dart:

var opaque = await api.functionThatCreatesSomeOpaqueData();
await api.functionThatUsesSomeOpaqueData(opaque);
opaque.dispose();

Implementation details

As for how it is implemented as well as the design towards safety, please refer to this doc

Arbitrary Dart types (opaque)

Any Dart type can be passed to Rust. This is done by wrapping it with DartOpaque.

This library ensures that any Dart objects are always removed on the parent Dart thread.

Different from non-opaque types, opaque types are not copied/moved/reconstructed at all. For example, if you pass around a Dart object MyObject in arguments and return values, you will get the exact same object.

Example DartOpaque

Rust:

pub fn get_dart_opaque(a: DartOpaque) { ... }
pub fn return_dart_opaque() -> DartOpaque { ... }
...

And use it in Dart:

var opaque = await api.getDartOpaque(() => '42');
Object answer_obj = await api.returnDartOpaque();
var fn = answer_obj as String Function();
print(fn());

Implementation details

As for how it is implemented as well as the design towards safety, please refer to this doc).

Type alias

Type alias is also supported. For example:

enum MyEnum {...}
struct MyStruct {...}

// type aliases
pub type Id = u64;
pub type EnumAlias = MyEnum;
pub type StructAlias = MyStruct;

// can also use them in fields, etc
pub struct TestModel { pub id: Id, pub e: EnumAlias, pub s: StructAlias}

pub fn f(input: Id) -> TestModel {...}

Limitation

The ItemType inside Generic is not supported yet, such as SyncReturn<Id>. The nested ItemType may also not be supported.

Result / Exceptions

  1. For Result/Error, theanyhow::Result/anyhow::Error is supported. It will be automatically converted to a Dart Exception.
  2. For panics, it will also be automatically captured and converted to Dart exceptions.
  3. For error hierarchy, or arbitrary error types, it is also supported. For example, you can create your own CustomError (such as using thiserror), and it will automatically be converted to a new Dart class.

If you want to see stack traces (backtraces), this doc page discusses how to configure it.

Example

Example 1: Anyhow Result

For example, the following code, when called by Dart code, will throw Dart exceptions.

pub fn f() -> anyhow::Result<i32> { bail!("oops I failed") }

Example 2: Panic

All functions below, when called, will throw Dart exceptions at the Dart side due to the panic.

pub fn g1() -> i32 { panic!("oops I failed") }
pub fn g2() -> anyhow::Result<String> { panic!("oops I failed") }
pub fn g3() -> Result<Vec<u8>, CustomError> { panic!("oops I failed") }

Example 3: Custom Error Without backtrace

pub enum CustomError {
    Error0(String),
    Error1(u32),
}

pub fn return_err_custom_error() -> Result<u32, CustomError> {
    Err(CustomError::Error1(3))
}

Becomes something that can be used like this:

try {
    final r = await api.returnErrCustomError();
    print("received $r");
} catch (e) {
    print('dart catch e: $e');
    expect(e, isA<CustomError>());
}

Example 4: Custom Error With backtrace

Errors with custom fields are also supported, and you can even pass a backtrace:

pub enum CustomStructError {
    Error0 { e: String, backtrace: Backtrace },
    Error1 { e: u32, backtrace: Backtrace },
}

As for how to fill it in or use it, you can refer to thiserror crate for some hints.

Parameter defaults

Dart allows default values for function and constructor parameters, and you can achieve the same effect using #[frb(default)]. The syntax is as follows:

  • If the parameter is a String or any other primitive, #[frb(default = ".." | 0 | true | ..)] annotates its default value.
  • If the parameter is a class or an enum, #[frb(default = "..")] annotates the Dart code to initialize the parameter. Note that this is run in the constant context, so classes can only be constructed if they are preceded with const.

This will be translated to either a default value annotation, or Freezed's @Default in the case of enum constructor parameters.

pub enum Answer { Yes, No }
pub struct Point(pub f64, pub f64);

#[frb]
pub fn defaults(
    #[frb(default = "Answer.Yes")]
    answer: Answer,
    #[frb(default = "const Point(field0: 2, field1: 3)")]
    point: Point,
);

Zero copy

ZeroCopyBuffer<Vec<u8>> (and its friends like ZeroCopyBuffer<Vec<i8>>) sends the data from Rust to Dart without making copies1. Thus, you save the time of copying data, which can be large if your data is big (such as a high-resolution image).

Make it the default

If you don't want to wrap Vec<u8> and its friends with ZeroCopyBuffer over and over again to avoid copying memory, you can alternatively provide a cargo feature called zero-copy. With this feature enabled, Vec<u8> and its friends will be zero-copied by default.

# Cargo.toml
[dependencies]
flutter_rust_bridge = { version = "...", features = ["zero-copy"] }

Example

pub fn draw_tree(tree: Vec<TreeNode>) -> ZeroCopyBuffer<Vec<u8>> { ... }

Becomes:

Future<Uint8List> drawTree({required List<TreeNode> tree});

The generated Dart code looks exactly the same as the case without ZeroCopyBuffer. However, the internal implementation changes and now there is no memory copy at all!

If you are curious about what those Vec<u8> and its friends actually are, take a look at this.

1

Not currently supported on Web, and will fallback to copying the buffer.

Stream / Iterator

What is Stream? In short: call once, return multiple times; like Iterators.

Flutter's Stream is a powerful abstraction. When using it as the return value of Rust function, we can allow the scenario that we call function once, and then return multiple times.

For example, your Rust function may run computationally heavy algorithms, and for every hundreds of milliseconds, it finds out a new piece of the full solution. In this case, it can immediately give that piece to Flutter, then Flutter can render it to UI immediately. Therefore, users do not need to wait for the full algorithm to finish before he can see some partial results on the user interface.

As for the details, a Rust function with signature like fn f(sink: StreamSink<T>, ..) -> Result<()> is translated to a Dart function Stream<T> f(..).

Notice that, you can hold that StreamSink forever, and use it freely even after the Rust function itself returns. The logger example below also demonstrates this (the create_log_stream returns almost immediately, while you can use the StreamSink after, say, an hour).

The StreamSink can be placed at any location. For example, fn f(a: i32, b: StreamSink<String>) and fn f(a: StreamSink<String>, b: i32) are both valid.

Examples

See logging examples which uses streams extensively.

What about streaming from Dart/Flutter to Rust?

This is not currently supported. As a workaround, consider iterating through your Dart stream and calling a normal Rust function for each item.

Asynchronous in Dart

This library generates functions that are asynchronous in Dart by default. So you will see fn f(..) -> String becomes Future<String> f(..) with that interesting Future.

Why? Flutter UI is single-threaded. If you use the intuitive synchronous approach, just like what you will (have to) do with plain-old Flutter bindings, your UI will be stuck as long as your Rust code is executing. If your Rust code run for 100ms for a heavy computation, your UI will fully freeze for 100ms and the users will not be happy.

On the other hand, with the generated asynchronous bindings in Dart, you can simply call functions directly in main isolate (thread) of Dart/Flutter, and Rust code will not block the Flutter UI.

Indeed async and Futures is almost everywhere in Flutter/Dart, and it has very good built-in support. So no worries about it ;)

Remark: A common mistake is to call Rust code in another Dart isolate (i.e. "thread") instead of the main isolate. That is completely not needed, and will only make your life harder. As is described above, even if your Rust code computes for 100ms, the async call will only take, say, 0.1ms, and will not block your UI.

Synchronous in Dart

If you need to generate synchronous functions in Dart, you can use SyncReturn<T> as the return type. It supports whatever types that we have described in other places, i.e. whatever types that async mode supports.

We suggest only do this for very quick Rust functions, or the Dart UI will be blocked.

If you are using the default handler, the behavior about threading is different from normal Rust functions. Normal function calls are executed in the worker pool. However, Rust functions with return type of SyncReturn<T> are executed on the main thread. This means that if a SyncReturn<T> function takes time, Dart UI will not be able to respond.

Concurrency

Multiple Rust functions can be running at the same time, and they will be running concurrently. This is because by default we use a thread pool to execute the Rust functions. However, you can fully customize this behavior (and even throw away the thread pool).

Example

Consider the following Rust code:

pub fn compute() {
  thread::sleep(Duration::from_millis(1000));
}

And the following Dart code using it:

var a = compute();
var b = compute();
var c = compute();
await Future.wait([a, b, c]); // You may need to learn `Future` and `async` in Dart to understand this

Then it will take 1 second instead of 3 seconds to complete the code, because multiple compute can run concurrently.

Handler

By default, the DefaultHandler is used for handling function calls. You can implement your own Handler with other custom behaviors you want. In order to do this, create a module variable named FLUTTER_RUST_BRIDGE_HANDLER in api.rs(probably using lazy_static) of your project. You may not need to create a brand new struct implementing Handler, but instead, use the SimpleHandler and customize its generic arguments such as its Executor.

Examples

Example: Report errors to your backend in addition to telling Dart

pub struct MyErrorHandler(ReportDartErrorHandler);

impl ErrorHandler for MyErrorHandler {
    fn handle_error(&self, port: i64, error: handler::Error) {
        send_error_to_your_backend(&error);
        self.0.handle_error(port, error)
    }

    ...
}

Example: Log when execution starts and ends

pub struct MyExecutor(ThreadPoolExecutor<MyErrorHandler>);

impl Executor for MyExecutor {
    fn execute<TaskFn, TaskRet>(&self, wrap_info: WrapInfo, task: TaskFn) {
        let debug_name_string = wrap_info.debug_name.to_string();
        self.thread_pool_executor
            .execute(wrap_info, move |task_callback| {
                Self::log_around(&debug_name_string, move || task(task_callback))
            })
    }
}

impl MyExecutor {
    fn log_around<F, R>(debug_name: &str, f: F) -> R where F: FnOnce() -> R {
        let start = Instant::now();
        debug!("(Rust) execute [{}] start", debug_name);
        let ret = f();
        debug!("(Rust) execute [{}] end delta_time={}ms", debug_name, start.elapsed().as_millis());
        ret
    }
}

Example: Use a simple handler

// api.rs

use flutter_rust_bridge::handler::ReportDartErrorHandler;
use flutter_rust_bridge::handler::SimpleHandler;
use flutter_rust_bridge::handler::ThreadPoolExecutor;
use lazy_static::lazy_static;

lazy_static! {
    static ref FLUTTER_RUST_BRIDGE_HANDLER:
    SimpleHandler<ThreadPoolExecutor<ReportDartErrorHandler>, ReportDartErrorHandler> =
        SimpleHandler::new(
            ThreadPoolExecutor::new(ReportDartErrorHandler),
            ReportDartErrorHandler {}
        );
}

Initialization

If you want that feature, have a look at FlutterRustBridgeSetupMixin in the Dart side. (More documentaions to be added; you can create an issue if you have questions now.)

Async in Rust

Currently, this feature has not been supported yet. However, it is implementable and the flutter_rust_bridge barely has assumption that user Rust functions should be sync. Issue #966 has some discussions about how to implement it.

This older article also describes some workarounds, which may be useful before the function is implemented.

Multiple files

When having a large project, it is often insufficient to put everything in a single api.rs, but instead we may want to separate it into api_of_one_module.rs, api_of_another_module.rs, etc. That is why we have this feature.

Basically, just specify all input Rust files and all output locations and we are done. Here is an example:

flutter_rust_bridge_codegen \
  --rust-input "$REPO_DIR/native/src/api_1.rs" "$REPO_DIR/native/src/api_2.rs" \
  --dart-output "$REPO_DIR/lib/bridge_generated_api_1.dart" "$REPO_DIR/lib/bridge_generated_api_2.dart" \
  --class-name ApiClass1 ApiClass2 \
  --rust-output generated_api_1 generated_api_2

For more details, have a look at this article.

Run in build.rs

There are basically two approaches to execute the code generator. The first and most evident approach is to directly execute the flutter_rust_bridge in command line.

The second approach is to integrate it into build.rs of your project. With this approach, the code generator is automatically triggered whenever you build your Rust project. For example configuration, have a look at this build.rs file.

If the build.rs in the example projects is making it difficult to modify and test flutter_rust_bridge_codegen, you can rename it to disable it, and instead use the pure_dart and pure_dart_multi tests to debug any issues. Please refer to frb_codegen/src/main.rs's tests for more details.

Cancellable tasks

When the Rust code is computationally heavy, you may want to cancel it at the middle when, for example, the user does not need it anymore. Then the precious computation power can be saved.

Installation: Currently, the feature is complete, and I have used it in my own app for a long time. (I have not merge this PR to the main repo just because I need to figure out how to put those code as if in api.rs.) Thus, visit #333 and copy the code directly to your project, and use it as normal.

Object pools

When there are some big objects in the Rust side, you may not want to copy them between Rust and Dart over and over again. That is when object pools become useful: You only pass around a "object handle" (indeed just a few integers) between Rust and Dart, and the Rust side will convert that handle from and to the real object.

Installation: Same as cancelable tasks, please see doc there.

WASM

flutter_rust_bridge_codegen can also generate code to run in browsers using wasm_bindgen. To generate a WASM-specific file, pass this option to your invocation:

flutter_rust_bridge_codegen .. --wasm

By default this will create several new files:

├── lib
│   ├── ...
│   ├── bridge_generated.io.dart
│   └── bridge_generated.web.dart
└── native/src
    ├── bridge_generated.io.rs
    └── bridge_generated.web.rs

The .io and .web modules implement platform-specific helpers. This split is mandatory for Dart due to its module system, however if you prefer to keep the Rust bridge in a single file pass the --inline-rust flag as well.

Check out Integrating with Web for instructions on how to consume the web bridge.

have a look at issue 860

Miscellaneous

Separate generated definitions from implementations

The generated bridge_generated.dart by default contains definitions of the APIs as well as the implementations. With the flag --dart-decl-output, the two can be separated, and the definitions will not contain anything like dart:ffi.

A command example as follow:

flutter_rust_bridge_codegen .. --dart-decl-output <DECL>

where DECL is the path to the common class/function declarations file. For example, if you emit your Dart bridge to lib/bridge_generated.dart, you can put the declarations file at lib/bridge_definitions.dart

By default this will create new file:

├── lib
│   ├── bridge_definitions.dart

More information: #298.

Injecting WASM initialization code

By default, this library injects its own initialization code to facilitate panic information recovery using console_error_panic_hook. If you would like to run some initialization code for WASM, e.g. to set up logging libraries, specify default-features = false in Cargo.toml:

flutter_rust_bridge = { version = "..", default-features = false, features = [..] }

The wasm-start feature governs this behavior and is enabled by default.

Logging for developers

For developers who want to contribute to this project, here is the feature logging that needs to mention.

When the code in frb_codegen is modified, usually developers want to build and run it locally for testing. Now with the init_logger in logs.rs from frb_codegen, it is easy to do so. Take frb_example/pure_dart as an example, in ./rust/build.rs, with:

use lib_flutter_rust_bridge_codegen::init_logger;
fn main() {
    init_logger("./logs/").unwrap();
...
}

Then, all information from standard panic, log::info!(), log::debug()!... of frb_codegen would be recorded to ./logs/ with a file name of date, like 2023-02-01.log in frb_example/pure_dart/rust as long as the example is built through build.rs. Note, the data from the same day would be appended to the same file.

Moreover, if rust-analyzer is used, then whenever frb_codegen is modified, all examples with build.rs would be automatically triggered to rebuild. Then the log would be updated automatically to disk, which makes the whole developing routine easier.

Logging

Since I have seen some questions asking how logging can be implemented with a Flutter + Rust application, here are some examples.

Logger in production

In my own app in production, I use the following strategy for Rust logging: Use normal Rust logging methods, such as info! and debug! macros. The logs are consumed in two places: They are printed via platform-specific methods (like android Logcat and iOS NSLog), and also use a Stream to send them to the Dart side such that my Dart code and further process are using the same pipeline as normal Dart logs (e.g. save to a file, send to server, etc).

The full code related to logging in my app can be seen here: #486.

Simple logger

Let us implement a simple logging system (adapted from the logging system I use with flutter_rust_bridge in my app in production), where Rust code can send logs to Dart code.

The Rust api.rs:

pub struct LogEntry {
    pub time_millis: i64,
    pub level: i32,
    pub tag: String,
    pub msg: String,
}

// Simplified just for demonstration.
// To compile, you need a OnceCell, or Mutex, or RwLock
// Also see https://github.com/fzyzcjy/flutter_rust_bridge/issues/398
lazy_static! { static ref log_stream_sink: StreamSink<LogEntry>; }

pub fn create_log_stream(s: StreamSink<LogEntry>) {
    stream_sink = s;
}

Now Rust will probably complain at you because IntoDart is not implemented for LogEntry. This is expected, because flutter_rust_bridge will generate this trait implementation for you. To fix this error you should just rerun flutter_rust_bridge_codegen.

Generated Dart code:

Stream<LogEntry> createLogStream();

Now let us use it in Dart:

Future<void> setup() async {
    createLogStream().listen((event) {
      print('log from rust: ${event.level} ${event.tag} ${event.msg} ${event.timeMillis}');
    });
}

And now we can happily log anything in Rust:

log_stream_sink.add(LogEntry { msg: "hello I am a log from Rust", ... })

Of course, you can implement a logger following the Rust's log crate wrapping this raw stream sink, then you can use standard Rust logging mechanisms like info!. I did exactly that in my project.

Example: Simple timer

Credits: this and #347.

use anyhow::Result;
use std::{thread::sleep, time::Duration};

use flutter_rust_bridge::StreamSink;

const ONE_SECOND: Duration = Duration::from_secs(1);

// can't omit the return type yet, this is a bug
pub fn tick(sink: StreamSink<i32>) -> Result<()> {
    let mut ticks = 0;
    loop {
        sink.add(ticks);
        sleep(ONE_SECOND);
        if ticks == i32::MAX {
            break;
        }
        ticks += 1;
    }
    Ok(())
}

And use it in Dart:

import 'package:flutter/material.dart';
import 'ffi.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late Stream<int> ticks;

  @override
  void initState() {
    super.initState();
    ticks = api.tick();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text("Time since starting Rust stream"),
            StreamBuilder<int>(
              stream: ticks,
              builder: (context, snap) {
                final style = Theme.of(context).textTheme.headlineMedium;
                final error = snap.error;
                if (error != null)
                  return Tooltip(
                      message: error.toString(),
                      child: Text('Error', style: style));

                final data = snap.data;
                if (data != null) return Text('$data second(s)', style: style);

                return const CircularProgressIndicator();
              },
            )
          ],
        ),
      ),
    );
  }
}

Stack Traces

To pass Rust stack traces to flutter, you need to set RUST_BACKTRACE in the running application. For that simply add env::set_var("RUST_BACKTRACE", "1"); before initialising the bridge.

Note: The --dart-define will not work, you must use env::set_var, because the former does not set the "environment variable" in the common sense, but instead a special thing only visible to Dart.

Worker pool

Note: You can customize handlers and even completely get rid of the worker pool. The following doc only works for those who wants to use the default handler and thus pool.

When you call a Rust function with generated code from Dart side, it is executed inside a separate worker pool handled by flutter_rust_bridge. Thanks to the pool, type of the return values in Dart is async Future which means heavy calculation in Rust does not block the user interface from responding.

In non-WASM configuration, flutter_rust_bridge internally creates a pool of 4 threads. In WASM configuration, a pool with 4 web workers is used.

However, if you think that this number of 4 is inappropriate for your project, you can choose a different option. By specifying features in Cargo.toml, you can optimize the number of threads or workers in the pool. Just include features key when adding flutter_rust_bridge as a dependency.

// Cargo.toml

[dependencies]
flutter_rust_bridge = { workspace = true, features = ["worker-max"] }

Currently available options related to worker pool are:

  • worker-single: Uses 1 worker in the pool.
  • worker-max: Uses all available logical cores.

Note that both non-WASM and WASM configurations provide true multithreaded parallelism. They both utilize actual threads in logical cores. However, you do need to be aware of the limitations of WASM, such as inability to use shared memory, etc.

The options are experimental, and may change later. However, it should be trivial to migrate even if it changes.

Expanding macros

This library automatically handles macros inside your code. For example, support you calls a macro that will generate a struct, then even if that struct is not in the code directly, this library can understand it.

The implementation is as follows: To produce code for types or functions that are generated through macros, it is necessary to first expand the code before it is parsed. This is done by invoking cargo-expand, a tool that expands all macros, resulting in code that can then be parsed.

Caution: This expansion process cannot be utilized when the code-generator is invoked within a build.rs script. The issue here is that cargo-expand triggers a project build, and invoking it within build.rs would lead to a deadlock, as cargo-expand would wait for the calling cargo build to complete. In such cases, code is read from files without macro expansion. If your API definition does not rely on macros for code generation, this works fine. Otherwise, you have to call the flutter_rust_bridge_codegen binary seperately.

Create new projects from a template

In this chapter, we are going to use create your own project from a code template. It seems a bit long, but it is just because we have tried to describe every detail that you may encounter.

Remark: Most complexity does not come from this library, flutter_rust_bridge - it is as same complex as using raw Dart/Flutter FFI with Rust. In other words, it is the Dart/Flutter + Rust toolchain that takes time to set up.

Creating a new project

Start by creating a repository using the template from flutter_rust_bridge_template and cloning it. This template is set up to be able to flutter run for most platforms that Flutter supports.

(Remark: The template is created by @Desdaemon who contributed many features into flutter_rust_bridge, instead of the creator of flutter_rust_bridge, @fzyzcjy. Thus, it is not a typo to see Desdaemon in the URL instead of fzyzcjy for that template repo.)

Android setup

Before trying this, ensure you can run the example project

Rust targets

If you have not already done so, cross-compiling to Android requires some additional targets which can easily be added:

rustup target add \
    aarch64-linux-android \
    armv7-linux-androideabi \
    x86_64-linux-android \
    i686-linux-android

iOS setup

iOS requires some additional Rust targets for cross-compilation:

# 64 bit targets (real device & simulator):
rustup target add aarch64-apple-ios x86_64-apple-ios
# New simulator target for Xcode 12 and later
rustup target add aarch64-apple-ios-sim
# 32 bit targets (you probably don't need these):
rustup target add armv7-apple-ios i386-apple-ios

Web setup

Building on web requires nightly Rust, the wasm32-unknown-unknown target and wasm-pack, which can be installed using these commands:

rustup toolchain install nightly
rustup +nightly component add rust-src
rustup +nightly target add wasm32-unknown-unknown
# either of these
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
cargo install wasm-pack

Optionally (but highly recommended), install flutter_rust_bridge_serve to expedite the process of building the WASM binary and setting up HTTP headers:

# in your Flutter/Dart package
flutter pub add flutter_rust_bridge
# then run this instead of "flutter web -d chrome"
dart run flutter_rust_bridge:serve
# or install globally
dart pub global activate flutter_rust_bridge
flutter_rust_bridge_serve

Limitations of WASM

Running code on the Web entails several restrictions on the kinds of code that can be executed. Please refer to Limitations of WASM to see if your code is compatible with WASM.

Windows and Linux

Windows and Linux share the same build system (CMake), making setup for these two platforms the easiest even from scratch. The template uses Corrosion to expedite the process, which has to clone and initialize the builder first. If you are running builds continuously, it might be a good idea to follow this guide to learn how to install Corrosion permanently onto your system. Once that's done, go ahead and modify rust.cmake in windows and linux:

-# find_package(Corrosion REQUIRED)
+find_package(Corrosion REQUIRED)

-include(FetchContent)
-
-FetchContent_Declare(
-    Corrosion
-    GIT_REPOSITORY https://github.com/AndrewGaspar/corrosion.git
-    GIT_TAG v0.4.4 # Optionally specify a version tag or branch here
-)
-
-FetchContent_MakeAvailable(Corrosion)

Troubleshooting: CMake on Linux

Unless you have certain use-cases that you require from the latest versions of Corrosion, it is recommended to use v0.4.x in your build scripts since CMake versions supplied by the system and/or Flutter snap installations trail behind the master branch. This will relax the CMake requirement to v15, which should be generally available from most package maintainers and Flutter snaps.

A workaround is to ignore rust.cmake and manually configure CMake to build and bundle the Rust library, as suggested by this comment in the case of Flutter on ARM Linux.

Other platforms

For all remaining platforms, there are no required setup steps to take, apart from those listed in Desktop support for Flutter. If you need to check your progress, run flutter doctor -v and it will display the status of your toolchain and any actionable steps. The rest of this page documents additional hints for each of the platforms that might be useful for newcomers to Flutter and/or Rust.

Template tour

success-screen

Congratulations! 🎉 You should have a working Flutter app equipped with a Rust runtime component. This section is meant to be a gentle introduction to the details of Rust integration with the existing Flutter toolchain. Feel free to skip forward to Generating code to learn how to write new code, or visit Integrating with existing projects to add Rust to your preexisting Flutter project.

native/src/api.rs

This is the default entry point for your library. Only functions defined here will be eligible for codegen. Functions may use types not defined in this file as parameter or return types, but those types must have been imported through pub use so that they are visible from native/src/bridge_generated.rs.

Only types defined within the current crate are eligible for codegen. Furthermore, structs and enums may only comprise of types that are themselves eligible.

To review the subset of currently eligible functions and types, see the example file here.

android/app/build.gradle

This file is part of the default Flutter build process for Android apps. The template injects additional hooks to run cargo-ndk upon invoking flutter run. This method is explained more in detail in Hooking onto tasks.

native/native.xcodeproj

This is the Xcode project folder for the Rust native library generated by cargo-xcode. The iOS and MacOS root projects import this folder as a subproject and depends on it during build-time.

It is important that the suitable crate-types are configured for your target devices. Make sure these lines exist in your Cargo.toml:

[lib]
crate-type = ["lib", "cdylib", "staticlib"]

where

  • lib is required for non-library targets, such as tests and benchmarks
  • staticlib is required for iOS
  • cdylib for all other platforms

justfile

This file defines the recipes for the just command runner, in a similar vein to make and Makefile. just is built using Rust and improves upon the traditional Makefile syntax with better support for conditionals, arguments, cross-platform compatibility and more.

One non-trivial feature of just utilized by this template is the conditional LLVM flag for MacOS. On certain setups, a brew install llvm does not make the LLVM libraries visible to other executables, which causes problems for ffigen, a C-to-Dart codegen that flutter_rust_bridge_codegen uses under the hood.

Running just by default runs the gen and lint tasks.

just gen

Generates the Rust bindings and puts them into the correct folders. The Generating new code section goes into detail how to modify this task to perform side jobs as well.

just lint

Runs the default linters for Dart and Rust.

just clean

Runs the default clean commands for Flutter and Rust. Useful when you want to debug build-related issues.

rust.cmake

In windows and linux are two identical files named rust.cmake. These files are included in the existing CMakeLists.txt that Flutter uses to compile its applications.

Generating code

This section assumes you followed the instructions in Creating a new project, and has successfully flutter run on your target device.

Up until now, all the code necessary for executing the program has been supplied for you, so there was no need to install anything. We will now look at how to create new Rust code, generate the necessary glue code and use them in Dart.

Installing codegen

More information in the Installing dependencies section.

Adding new code

Let's say we need to change Platform such that we don't really care about whether it is running on Intel or Apple Silicon, but we would like to keep this information so downlevel code can act on it. We would like to merge MacApple and MacIntel into a single MacOs(String) that contains the current CPU architecture. Go ahead and update native/src/api.rs:

 pub enum Platform {
     ..
-    MacIntel,
-    MacApple,
+    MacOs(String),
     ..
 }

Now run just and see that your binding code now has changed.

Troubleshooting: "Please supply one or more path/to/llvm..."

A common issue with ffigen is that its detection of the LLVM installation is not reliable across platforms. Especially for MacOS and the split between x86-64 and arm64 binaries, you might have to modify justfile to explicitly point to its location:

llvm_path := if os() == "macos" {
    "--llvm-path /opt/homebrew/opt/llvm"
} else {
    ""
}

Using build_runner

Inspect your lib/bridge_generated.dart and you will see that the definition of Platform has changed:

@freezed
sealed class Platform with _$Platform {
    const factory Platform.unknown() = Platform_Unknown;
    const factory Platform.android() = Platform_Android;
    const factory Platforn.ios() = Platform_Ios;
    const factory Platform.windows() = Platform_Windows;
    const factory Platform.unix() = Platform_Unix;
    const factory Platform.macOs(
        String field0,
    ) = Platform_MacOs;
    const factory Platform.wasm() = Platform_Wasm;
}

It is no longer a plain enum, but a full-blown enum class with variants! As it is right now, this code cannot compile yet since it is missing some components, namely the freezed library. freezed is a codegen library similar to those we've encountered thus far, but generates more Dart code instead. All such libraries perform their code generation upon invoking build_runner, i.e. when flutter pub run build_runner build is executed.

Regardless, to make this code compile again, we need to make a few changes:

  • Run the following commands to add the latest version of freezed:
flutter pub add -d build_runner
flutter pub add -d freezed
flutter pub add freezed_annotation
  • Update justfile to run build_runner after Rust codegen:
 gen:
     ..
     # Uncomment this line to invoke build_runner as well
-    # flutter pub run build_runner build
+    flutter pub run build_runner build

Now calling just will generate both the Rust bindings and the Dart library code.

Wrapping up

With our new definition of Platform in place, we can rewrite the previous code to make use of it! Here is an example of what you can do with freezed enums.

In lib/main.dart:

- final text = const {
-   Platform.Android: 'Android',
-   Platform.Ios: 'iOS',
-   Platform.MacApple: 'MacOS with Apple Silicon',
-   Platform.MacIntel: 'MacOS',
-   Platform.Windows: 'Windows',
-   Platform.Unix: 'Unix',
-   Platform.Wasm: 'the Web',
- }[platform] ??
- 'Unknown OS';
+ final text = platform.when(
+   android: () => 'Android',
+   ios: () => 'iOS',
+   macOs: (arch) => 'MacOS on $arch',
+   windows: () => 'Windows',
+   unix: () => 'Unix',
+   wasm: () => 'the Web',
+ );

In native/src/api.rs:

     } else if cfg!(target_os = "ios") {
         Platform::Ios
     } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
-        Platform::MacApple
+        Platform::MacOs("Apple Silicon".into())
     } else if cfg!(target_os = "macos") {
-        Platform::MacIntel
+        Platform::MacOs("Intel".into())
     } else if cfg!(target_family = "wasm") {
         Platform::Wasm
     } else if cfg!(unix) {

When you flutter run, you should get something like this: macos-intel

Tip: Using switch expressions

Introduced in Dart 3, switch expressions provide the equivalent of Rust's match expressions, complete with exhaustive checks. Instead of using when() in the above example, you could also use this syntax:

final text = switch (platform) {
  Platform_Android() => 'Android',
  Platform_Ios() => 'iOS',
  Platform_MacOs(:final arch) => 'MacOS on $arch',
  Platform_Windows() => 'Windows',
  Platform_Unix() => 'Unix',
  Platform_Wasm() => 'the Web',
  // we have covered all cases, so this compiles.
};

Integrating with existing projects

This guide is an intermediate-level introduction to integrating Rust with an existing Flutter project. If you are new to Rust or configuring build processes in general, we suggest looking at the template tour to learn about the moving parts behind a flutter run.

Before following this guide, upgrade your Flutter SDK, and if possible refresh your native build folders (android, ios, etc.) to make the process as straightforward as possible.

Remark: Most complexity does not come from this library, flutter_rust_bridge - it is as same complex as using raw Dart/Flutter FFI with Rust. In other words, it is the Dart/Flutter + Rust toolchain that takes time to set up.

Using the flutter_rust_bridge brick

The following sections cover how to set up Rust support from scratch for completeness' sake, however for your convenience you can also use the fluttter_rust_bridge brick to scaffold most of1 the code written here.

1

Some setup steps are still required even with the brick, which we will go into more detail in the later sections. The brick is a work-in-progress.

Creating a new crate

First, if you haven't done so already, create a new crate within your project directory using cargo new --lib. It is recommended that the crate root is a sibling of the other native build folders for ease of config, e.g.:

├── android
├── ios
├── lib
├── linux
├── macos
├── $crate
│   ├── Cargo.toml
│   └── src
├── test
├── web
└── windows

Throughout this section we will refer to your crate name as $crate. Unless otherwise noted, the crate folder and the crate name will be used interchangeably.

Next, add these two lines to your Cargo.toml:

+[lib]
+crate-type = ["staticlib", "cdylib"]

This configures your crate to be output as a static library for MacOS and iOS, and a dynamic library on other platforms. Configure this to your needs. If you would like to write tests or benchmarks, append "rlib" to the list as well.

Installing dependencies

Next, we need to install a few build-time and runtime dependencies.

Build-time dependencies

These dependencies are required only in build-time:

An easy way to install most of these dependencies is to run:

  • dart project

    cargo install flutter_rust_bridge_codegen
    dart pub add --dev ffigen && dart pub add ffi
    # if building for iOS or MacOS
    cargo install cargo-xcode
    
  • flutter project

    cargo install flutter_rust_bridge_codegen
    flutter pub add --dev ffigen && flutter pub add ffi
    # if building for iOS or MacOS
    cargo install cargo-xcode
    

Alternatively, each of these dependencies may provide prebuilt binaries. Check with your package manager and review them individually.

Dart dependencies

On the Dart side, flutter_rust_bridge is the required runtime component of flutter_rust_bridge_codegen. If you plan to use enum structs in Rust, the following dependencies are also needed:

  • build_runner (dev)
  • freezed (dev)
  • freezed_annotation

Their usage is explained in Using build_runner.

flutter pub add flutter_rust_bridge
# if using Dart codegen
flutter pub add -d build_runner
flutter pub add -d freezed
flutter pub add freezed_annotation

Rust dependencies

Similar to Dart, Rust requires the flutter_rust_bridge runtime component for support.

Add these lines to Cargo.toml:

+[dependencies]
+flutter_rust_bridge = "1"

System dependencies

Non-Debian Linux

For non-debian based Linux distributions, there are a few prerequisites:

Firstly, ensure that packages are up to date (or install by demand).

  • clang
  • llvm-libs
  • glibc

Restarting system may be required.

Secondly, set the environment variable in your shell profile (.bashrc, .zshrc, etc):

export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include"

Integrating with Android

The setup process is identical to Android setup, so go ahead and follow the steps described there. Once you're done, we will discuss how to modify the existing toolchain to accommodate Rust.

There is more than one way to set up Cargo to run alongside Gradle, so this guide will cover the two main ones: hooking onto tasks, and integrating with CMake.

Hooking onto tasks

This is the same method used by the app template and also the easier one. Go ahead and install cargo-ndk if you have not already done so:

cargo install cargo-ndk

Next, add these lines1 near the bottom of android/app/build.gradle:

[
    new Tuple2('Debug', ''),
    new Tuple2('Profile', '--release'),
    new Tuple2('Release', '--release')
].each {
    def taskPostfix = it.first
    def profileMode = it.second
    tasks.whenTaskAdded { task ->
        if (task.name == "javaPreCompile$taskPostfix") {
            task.dependsOn "cargoBuild$taskPostfix"
        }
    }
    tasks.register("cargoBuild$taskPostfix", Exec) {
        // Until https://github.com/bbqsrc/cargo-ndk/pull/13 is merged,
        // this workaround is necessary.

        def ndk_command = """cargo ndk \
            -t armeabi-v7a -t arm64-v8a -t x86_64 -t x86 \
            -o ../android/app/src/main/jniLibs build $profileMode"""

        workingDir "../../$crate"
        environment "ANDROID_NDK_HOME", "$ANDROID_NDK"
        if (org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem.isWindows()) {
            commandLine 'cmd', '/C', ndk_command
        } else {
            commandLine 'sh', '-c', ndk_command
        }
    }
}

Note the ANDROID_NDK variable, this is a Gradle property that points to your installation of the Android NDK. If you don't rely on portability, you can hardcode this value, but note that it can be supplied by one of the many gradle.properties scattered throughout your filesystem. The most reliable way is to create a file at ~/.gradle/gradle.properties and fill it with this:

ANDROID_NDK=(path to NDK)

Note the ABIs x86_64 and x86 in ndk_command are usually used for Android simulators. Feel free to remove them as needed.

1

This excerpt might be outdated, please check out the source file at the template repository.

CMake with Gradle

If you have taken a look at either the windows or linux folder, you will see a file named CMakeLists.txt. This is the definition file for the CMake toolchain that Flutter uses to build Windows and Linux apps. You can also use this strategy on Gradle, but this setup is beyond the scope of this guide and reserved for advanced tinkers.

Refer to the Add C and C++ code to your project page on the official Android docs, modify around C-specific parts and use a tool like Corrosion to integrate with Cargo. The advantage of this setup is that you can reuse your C tools and benefit from various techniques such as caching builds.

Integrating with iOS/MacOS

Credit to brotskydotcom/rust-on-ios for the inspiration of this method.

Setting up flutter run for iOS and MacOS is slightly more complicated than other platforms, due to its reliance on the Xcode user interface. This guide assumes you are running a relatively recent version of Xcode, which at the time of writing is Xcode 13. Other versions might have minor variances but the overall process should be the same.

Creating the Rust project

First, follow the instructions on the Usage section of cargo-xcode. The instructions that follow are quoted from there, but keep in mind that it might have become outdated.


Ensure that these lines are present in your $crate/Cargo.toml:

[lib]
crate-type = ["lib", "staticlib", "cdylib"]

where

  • lib is required for non-library targets, such as tests and benchmarks
  • staticlib is required for iOS
  • cdylib for all other platforms

Configure this to suit your needs. Then run this command in $crate:

cargo xcode

This will generate a $crate/$crate.xcodeproj that can be imported into other Xcode projects. You only have to do this once per crate.

Now, open up that $crate/$crate.xcodeproj file with Xcode and select the root item at the left pane. The item's name will be identical to your crate's name. In the Build Settings tab, search for Dynamic Library Install Name Base and change the value into @executable_path/../Frameworks/. This is required by cargo-xcode to enable macOS executable to properly locate .dylib library files in the package.

Linking the project

Open ios/Runner.xcodeproj in Xcode, then add $crate/$crate.xcodeproj as a subproject of the Runner project. It should look like this:

proj-tree

Click on the Runner root project, then go to the Build Phases tab. First, expand the Dependencies phase, and add $crate-staticlib for iOS, or $crate-cdylib for MacOS.

dep-phase

Then, expand the Link Binary With Libraries phase, and add lib$crate_static.a for iOS, or $crate.dylib for MacOS.

link-phase

Generating bindings

Now that we've got most of the plumbing out of the way, let's compile our Rust application. If you just created your crate a few moments ago, go ahead and add a new file at $crate/src/api.rs and replace its contents with this snippet or whatever suits your fancy:

pub fn greet() -> String {
    "Hello from Rust! 🦀".into()
}

then in $crate/src/lib.rs:

+mod api;

Running the codegen

Before we can compile the library, we need to generate the bindings first. From the root of the app, run these commands:

flutter_rust_bridge_codegen \
    -r $crate/src/api.rs \
    -d lib/bridge_generated.dart \
    -c ios/Runner/bridge_generated.h \
    -e macos/Runner/   # if building for MacOS, extra path is essential

Note: These will be the same commands to use whenever you modify your Rust library code.

Running this command yields the C header of the functions and types exported by the Rust library, which we will need to keep the symbols from being stripped.

Using dummy headers

flutter_rust_bridge_codegen created a C header which lists all the exported symbols from our library, then uses it so that Xcode won't strip the symbols.

Add ios/Runner/bridge_generated.h (or macos/Runner/bridge_generated.h) to the project, either by dragging it onto the project tree or via the Add Files to "Runner"... menu option.

Switch to the Build Phases tab and drag the bridge_generated.h file over to the Copy Bundle Resources section, if it isn't already present.

iOS

Next, add this line to ios/Runner/Runner-Bridging-Header.h:

+#import "bridge_generated.h"

and in ios/Runner/AppDelegate.swift:

 override func application(
     _ application: UIApplication,
     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
 ) -> Bool {
+    let dummy = dummy_method_to_enforce_bundling()
+    print(dummy)
     ..
 }

It is important that you use the result of dummy_method_to_enforce_bundling() (like in the example above), otherwise the symbols might still get stripped.

Stripping iOS symbols

If you release your app through App Store, the steps above might not be sufficient. In that case you need to modify how Xcode strips the symbols:

  1. In Xcode, go to Target Runner > Build Settings > Strip Style.
  2. Change from All Symbols to Non-Global Symbols.

Ref: https://docs.flutter.dev/development/platform-integration/ios/c-interop#stripping-ios-symbols

MacOS

Flutter on MacOS does not use headers by default, so let's go ahead and add one ourselves. In the Build Settings tab, set the Objective-C Bridging Header to be Runner/bridge_generated.h.

Also, head over to the Build Phases tab, Bundle Framework section and add your $crate.dylib by clicking the plus button. This includes your dynamic library file in your app package.

Finally, use dummy_method_to_enforce_bundling somewhere within macos/Runner/AppDelegate.swift, as long as Xcode does not consider it dead code.

For multi-blocks

If there are multi-blocks:

  • For iOS, just add the 1st generated block-header files in Runner-Bridging-Header.h.
  • For MacOS, just add the 1st generated block-header files as Objective-C Bridging Header.

For all cases, the AppDelegate.swift should be the same as that in the single-block case. related issue

Integrating with Windows and Linux

This guide groups together instructions for Windows and Linux desktop apps, as they use the same build system.

The idea is the same as other platforms: we hook onto the existing projects using scripts, and we will also be borrowing from the template. Go ahead and download rust.cmake into your windows and linux folders. Keep in mind that CMake will refuse to use files that lie outside of its working directory, so there will be duplications between the two build folders.

Next, add this line to your CMakeLists.txt files:

 # Generated plugin build rules, which manage building the plugins and adding
 # them to the application.
 include(flutter/generated_plugins.cmake)

+include(./rust.cmake)

 # === Installation ===
 # Support files are copied into place next to the executable, so that it can

Linux

On Linux, you will need to bump the minimum CMake version to 3.12 to make use of Corrosion, which is used by rust.cmake. Change this line in linux/CMakeLists.txt:

-cmake_minimum_required(VERSION 3.10)
+cmake_minimum_required(VERSION 3.12)

Alternatively, you can install Corrosion permanently on your system. Refer to the Linux troubleshooting notes here.

Integrating with Web

Refer to the Web setup page for required installables.

Once you have installed the required dependencies, you will need to create a wrapper to consume the bridge files. In the case of DynamicLibrary you only needed to supply the path to the binary, but to import a WASM module you need to:

  • Create a script tag to the JS file generated by wasm_bindgen and insert it into the document;
  • Invoke the wasmModule initializer defined in the web bridge;
  • And finally, create the implementation class.

Create a Dart file and copy these lines to it:

import 'bridge_generated.web.dart';
export 'bridge_definitions.dart';

import 'dart:html';

// Path to the wasm_bindgen generated files
const root = 'pkg/native';
final api = NativeImpl.wasm(WasmModule.initialize(
    kind: const Modules.noModules(root: root),
));

Using the dynamic library

If everything went well, running flutter run will now build your Rust library, the Flutter binary and link the two together. Now the only thing left to do is to actually use it!

Download this file to lib/ffi.dart, then modify its contents:

 // Re-export the bridge so it is only necessary to import this file.
 export 'bridge_generated.dart';
 import 'dart:io' as io;

-const _base = 'native';
+const _base = '$crate';

 // On MacOS, the dynamic library is not bundled with the binary,
 // but rather directly **linked** against the binary.
 final _dylib = io.Platform.isWindows ? '$_base.dll' : 'lib$_base.so';

Wrapping up

Congratulations! You have successfully added a Rust component to your Flutter app using flutter_rust_bridge and configured flutter run (more on web later) to build your Rust library and link it to the app.

As a reminder, you need to run these commands every time your Rust code changes and before you run flutter run:

flutter_rust_bridge_codegen \
    -r $crate/src/api.rs \
    -d lib/bridge_generated.dart \
    -c ios/Runner/bridge_generated.h \
    -e macos/Runner/   # if building for MacOS, extra path is essential

Renaming the Rust bridge module

If you would like to use the --rust-output flag of flutter_rust_bridge_codegen, keep in mind that you will have to update $crate/src/lib.rs to point to the correct file, for example if you use this command instead:

flutter_rust_bridge_codegen \
    ..
    --rust-output $crate/src/my_bridge.rs

then you need to modify this in lib.rs:

- mod bridge_generated;
+ mod my_bridge;

flutter_rust_bridge_serve-less workflows

If you don't need to run Flutter Web in development mode and would rather build in release mode once in a while, read here for instructions on how to build your WASM binary without flutter_rust_bridge_serve.

Creating a Dart/Flutter library

In this chapter, we discuss how to add flutter_rust_bridge (FRB) to an already existing application or create a new application from scratch; this section covers creating a Dart-only library with a Flutter wrapper library on top.

In many cases, following this guide is actually easier long-term than creating an application around FRB directly, but it does have a bit of overhead to set up. This is also true when only using a library internally, because this guide will also help you get set up with Melos, a monorepo tool specifically built for Dart/Flutter.

Overview

This section of the user guide will walk you through the entire process of making a Dart-only library base with a Flutter library built on top of it.

In the end, if you choose to publish to pub.dev, users can simply run:

  • flutter pub add flutter_library_name for Flutter
  • dart pub add library_name for Dart-only (say for a CLI or server app)

But you also don't need to publish your library either! You can just use your library internally in a monorepo, as described later on.

High Level Design

We will build out our library from scratch, piece by piece, allowing you to include only what you want in your library. It is intended that whenever a code snippet/file is shown, you read and understand the content of the snippet so that you can work with your library more easily in the future. We will create the following components:

  • Dart-only library
  • Flutter library wrapping the Dart-only library
  • CI/CD with GitHub Actions
    • Dart/Flutter unit & integration testing
      • Test your code on all supported Flutter platforms automatically!
    • Automate release creation
      • Release library binaries through GitHub releases
      • Create pub.dev releases automatically on GitHub release
  • Custom build system to cross-compile to all supported Flutter platforms
    • You need a Mac to compile to macOS/iOS (at least locally)
      • But CI can handle your compilation/releases if you don't have a Mac!
    • Can cross-compile to all other Flutter platforms, no matter your dev device

For a full working example (that this guide was created based on!), take a look at mimir (which is created by the author of this chapter of document). It incorporates all functionality present in this guide and some more.

Warning!

Please note, this entire section will be outdated & need to be overhauled once "Native Assets" are added to Dart. A lot of the techniques described here are merely workarounds until Dart supports Native Assets.

Also, this guide does not currently cover web support, but provides the necessary ground work to support web in the future. It may be worth waiting for Native Assets before trying to come up with a custom solution for WASM. Feel free to PR to add web support to this guide!

Setup

Dependencies

To start developing your Dart/Flutter library, you will need to download some dependencies locally.

Required

The rest of this guide assumes you have the following tools installed on any development machines:

  • Flutter
  • rustup
  • Melos (needed for our monorepo, see here)
    • dart pub global activate melos to install once Dart/Flutter are installed

Optional

If you would like to build your binaries (for Flutter devices) locally in addition to CI (say, to test on a real device or emulator), you will additionally need the following:

  • To compile to macOS/iOS targets
    • macOS
  • To cross-compile to Android targets
    • Android NDK
      • Most NDK versions should work nowadays due to fixes in cargo-ndk
        • Previously, NDK version 21 (r21e) was the only one that could be used easily
          • You might see reference to this elsewhere, but that is largely out of date
        • NDK version 25 (r25b) was working at the time of writing this documentation
  • To cross-compile to Windows/Linux targets
    • Zig
    • llvm (with clang-cl!)
      • Need to run brew install llvm on macOS since Apple's llvm doesn't have it

Repository Structure

We will be using the following structure for our repository, assuming our library name is library_name:

  • .github/ for CI/CD (with GitHub Actions) & dependabot
  • packages/ where our Flutter/Dart packages will live
    • library_name/ the Dart-only (library) package using flutter_rust_bridge (FRB)
      • native/ the Rust library used by Dart
      • test/ unit tests for our Dart-only library
      • example/ an example project showing how to use library_name from Dart-only
        • test/ (optional) tests for the example; can be used to ensure example continues to work in CI
    • flutter_library_name/ the Flutter (library) package wrapping around library_name for ease of use
      • android/, ios/, linux/, macos/, & windows/ for platform-specific wrappers in order to bundle our library binaries with Flutter applications
      • test/ unit tests for our Flutter library (note: there might not be any if your Flutter library does not add any Flutter-specific functionality; in that case, add a dummy test in so CI is happy)
      • example/ an example project showing how to use flutter_library_name from within a Flutter application
        • integration_test/ integration tests to ensure your Flutter library, example, and platform-specific configuration are all working together correctly
  • scripts/ build Flutter binaries and handle release creation
  • platform-build/ the output (build) folder for all created Flutter binaries
  • analysis_options.yaml to enable consistent Dart analysis in our Dart/Flutter libraries
  • Cargo.toml so IDEs can find our Rust project under packages/library_name/native
  • melos.yaml to configure the monorepo, see more here

Monorepo with Melos

This page covers the basics of how to setup and use Melos, a Dart/Flutter focused monorepo tool, so you can use it to manage your own monorepo.

While you can, in theory, go about this guide without using Melos, using Melos will save you a lot of time and headache. There is some slight upfront cost of configuring and learning how Melos works, but it pays off substantially in the long-term.

The rest of this guide assumes you are using Melos, so here are the steps to setup Melos, along with some common commands.

It is also highly recommended that you read the Melos documentation linked above, as this page only covers the bare minimum and it is likely you will want to do more than listed here.

/melos.yaml

Here is a sample of Melos' configuration file to get you started:

name: library_name

repository: https://github.com/YourGitHubAccount/library_name

packages:
  - packages/**

scripts:
  analyze:
    exec: flutter analyze .
    description: Analyze a specific package in this project.

  check-format:
    exec: dart format --set-exit-if-changed .
    description: Check the format of a specific package in this project.

  format:
    exec: dart format .
    description: Format a specific package in this project.

  version:
    description: Updates version numbers in all build files
    run: bash scripts/version.sh

  build:
    run: melos run build:apple && melos run build:android && melos run build:other
    description: Build all native libraries for the project.

  build:apple:
    run: bash scripts/build-apple.sh
    description: Build the XCFramework for iOS and macOS.

  build:android:
    run: bash scripts/build-android.sh
    description: Build the .tar.gz for Android.

  build:other:
    run: bash scripts/build-other.sh
    description: Build the .tar.gz for all other platforms.

  test:
    run: melos run test:dart --no-select && melos run test:flutter --no-select
    description: Run all Dart & Flutter tests in this project.

  test:dart:
    run: melos exec -c 1 --fail-fast -- "dart test test"
    description: Run Dart tests for a specific package in this project.
    select-package:
      flutter: false
      dir-exists: test

  test:flutter:
    run: melos exec -c 1 --fail-fast -- "flutter test test"
    description: Run Flutter tests for a specific package in this project.
    select-package:
      flutter: true
      dir-exists: test

You can run the melos "scripts" defined in this file with melos run ..., e.g. melos run build:android to build a .tar.gz for Android devices. Also, when you first setup your Melos repo, you will need to run melos bootstrap (or melos bs for short). To clean your repo in the future, you can run melos clean && melos bs.

/scripts/version.sh

Every time you need to make a new release of your library, Melos will take care of the heavy lifting for you. Melos creates new versions via the simple command, melos version. melos version creates and manages git tags, in addition to automatically incrementing the version numbers appropriately.

Since we are distributing our binaries separately from the Dart/Flutter packages on pub.dev, we take advantage of a special "melos script" defined in the configuration file, named "version". In this versioning script, we change the version numbers for our Flutter build process so that consumers of our library will always get the binaries associated with their version.

Replace all instances of library_name below with your library name.

#!/bin/bash

CURR_VERSION=library_name-v`awk '/^version: /{print $2}' packages/library_name/pubspec.yaml`

# iOS & macOS
APPLE_HEADER="release_tag_name = '$CURR_VERSION' # generated; do not edit"
sed -i.bak "1 s/.*/$APPLE_HEADER/" packages/flutter_library_name/ios/flutter_library_name.podspec
sed -i.bak "1 s/.*/$APPLE_HEADER/" packages/flutter_library_name/macos/flutter_library_name.podspec
rm packages/flutter_library_name/macos/*.bak packages/flutter_library_name/ios/*.bak

# CMake platforms (Linux, Windows, and Android)
CMAKE_HEADER="set(LibraryVersion \"$CURR_VERSION\") # generated; do not edit"
for CMAKE_PLATFORM in android linux windows
do
    sed -i.bak "1 s/.*/$CMAKE_HEADER/" packages/flutter_library_name/$CMAKE_PLATFORM/CMakeLists.txt
    rm packages/flutter_library_name/$CMAKE_PLATFORM/*.bak
done

git add packages/flutter_library_name/

Conventional Commits

For Melos versioning to work, which our monorepo relies on to distribute binaries properly, you need to use "conventional commits." If you are not familiar with conventional commits, that is ok. Simply read up on conventional commits in the Melos guide.

Creating the libraries

In this section, we will create our Dart-only base library and then a Flutter wrapper library built on top of the Dart-only base.

The Flutter library can add additional Flutter-specific functionality to your Dart-only base; however, it does not need to. The main purpose of the Flutter wrapper is to bundle the Rust binaries alongside your Dart library and to re-export the Dart library.

Dart-only base

This page details how to set up the initial structure of our monorepo, including the crucial Dart-only base package.

Initialization script

This script creates a new monorepo named $LIBNAME in the current working directory and initializes it with some needed files. The following script assumes a bash shell, which you should make sure to use to run it. Also, the script generates some ffi convenience files in your Dart src/ which you should check out after the script completes.

After the script runs, change the flutter_rust_bridge dependency in /packages/library_name/pubspec.yaml to the following:

  flutter_rust_bridge: "1.62.1"

Note: If you so choose, you can update the flutter_rust_bridge versions in /packages/library_name/native/Cargo.toml and /packages/library_name/pubspec.yaml to the latest version available, but newer versions are not guaranteed to work with this section of the guide due to a lack of CI testing. Version 1.62.1 is known to work with this guide as-is. CI testing is planned once the Native Assets feature is released.

Finally, change the variables at the top of the script to fit your needs.

LIBNAME=library_name # snake_case
DART_CLASS_NAME=LibraryName # probably is PascalCase version of $LIBNAME

# Monorepo setup
mkdir -p $LIBNAME/packages
cd $LIBNAME
git init

cat << EOF >> Cargo.toml
[workspace]
members = ["packages/$LIBNAME/native"]
EOF

cat << EOF >> analysis_options.yaml
# TODO change the below options/lints as you see fit
analyzer:
  exclude:
    - '**.freezed.dart'
    - '**.g.dart'
  language:
    strict-inference: true
    strict-raw-types: true
  errors:
    invalid_annotation_target: ignore

linter:
  rules:
    # Custom lints
    - prefer_single_quotes

    # Core Dart lints
    - avoid_empty_else
    - avoid_relative_lib_imports
    - avoid_shadowing_type_parameters
    - avoid_types_as_parameter_names
    - await_only_futures
    - camel_case_extensions
    - camel_case_types
    - curly_braces_in_flow_control_structures
    - depend_on_referenced_packages
    - empty_catches
    - file_names
    - hash_and_equals
    - iterable_contains_unrelated_type
    - list_remove_unrelated_type
    - no_duplicate_case_values
    - non_constant_identifier_names
    - null_check_on_nullable_type_parameter
    - package_prefixed_library_names
    - prefer_generic_function_type_aliases
    - prefer_is_empty
    - prefer_is_not_empty
    - prefer_iterable_whereType
    - prefer_typing_uninitialized_variables
    - provide_deprecation_message
    - unnecessary_overrides
    - unrelated_type_equality_checks
    - valid_regexps
    - void_checks

    # Recommended Dart lints
    - always_require_non_null_named_parameters
    - annotate_overrides
    - avoid_function_literals_in_foreach_calls
    - avoid_init_to_null
    - avoid_null_checks_in_equality_operators
    - avoid_renaming_method_parameters
    - avoid_return_types_on_setters
    - avoid_returning_null_for_void
    - avoid_single_cascade_in_expression_statements
    - constant_identifier_names
    - control_flow_in_finally
    - empty_constructor_bodies
    - empty_statements
    - exhaustive_cases
    - implementation_imports
    - library_names
    - library_prefixes
    - library_private_types_in_public_api
    - no_leading_underscores_for_library_prefixes
    - no_leading_underscores_for_local_identifiers
    - null_closures
    - overridden_fields
    - package_names
    - prefer_adjacent_string_concatenation
    - prefer_collection_literals
    - prefer_conditional_assignment
    - prefer_contains
    - prefer_equal_for_default_values
    - prefer_final_fields
    - prefer_for_elements_to_map_fromIterable
    - prefer_function_declarations_over_variables
    - prefer_if_null_operators
    - prefer_initializing_formals
    - prefer_inlined_adds
    - prefer_interpolation_to_compose_strings
    - prefer_is_not_operator
    - prefer_null_aware_operators
    - prefer_spread_collections
    - prefer_void_to_null
    - recursive_getters
    - slash_for_doc_comments
    - type_init_formals
    - unnecessary_brace_in_string_interps
    - unnecessary_const
    - unnecessary_constructor_name
    - unnecessary_getters_setters
    - unnecessary_late
    - unnecessary_new
    - unnecessary_null_aware_assignments
    - unnecessary_null_in_if_null_operators
    - unnecessary_nullable_for_final_variable_declarations
    - unnecessary_string_escapes
    - unnecessary_string_interpolations
    - unnecessary_this
    - use_function_type_syntax_for_parameters
    - use_rethrow_when_possible

    # Flutter lints
    - avoid_print
    - avoid_unnecessary_containers
    - avoid_web_libraries_in_flutter
    - no_logic_in_create_state
    - prefer_const_constructors
    - prefer_const_constructors_in_immutables
    - prefer_const_declarations
    - prefer_const_literals_to_create_immutables
    - sized_box_for_whitespace
    - sort_child_properties_last
    - use_build_context_synchronously
    - use_full_hex_values_for_flutter_colors
    - use_key_in_widget_constructors
EOF

cat << EOF >> .gitignore
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
pubspec.lock
pubspec_overrides.yaml
**/doc/api/
.dart_tool/
.packages
build/
.pub-cache/
.pub/
.flutter-plugins
.flutter-plugins-dependencies

# Rust related
/target/
/Cargo.lock
/platform-build
EOF

# Dart setup
DART_BASE=packages/$LIBNAME
dart create --template=package $DART_BASE
(cd $DART_BASE && dart pub add flutter_rust_bridge ffi && dart pub add ffigen --dev)
rm $DART_BASE/analysis_options.yaml # we provide our own in repo root
( # ffi setup
cd $DART_BASE
mkdir -p lib/src/ffi

cat << EOF >> lib/src/ffi/stub.dart
import 'package:$LIBNAME/src/bridge_generated.dart';

/// Represents the external library for $LIBNAME
///
/// Will be a DynamicLibrary for dart:io or WasmModule for dart:html
typedef ExternalLibrary = Object;

$DART_CLASS_NAME createWrapperImpl(ExternalLibrary lib) =>
    throw UnimplementedError();
EOF

cat << EOF >> lib/src/ffi/io.dart
import 'dart:ffi';

import 'package:$LIBNAME/src/bridge_generated.dart';

typedef ExternalLibrary = DynamicLibrary;

$DART_CLASS_NAME createWrapperImpl(ExternalLibrary dylib) =>
    ${DART_CLASS_NAME}Impl(dylib);
EOF

cat << EOF >> lib/src/ffi/web.dart
import 'package:$LIBNAME/src/bridge_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';

typedef ExternalLibrary = WasmModule;

$DART_CLASS_NAME createWrapperImpl(ExternalLibrary module) =>
    ${DART_CLASS_NAME}Impl.wasm(module);
EOF

cat << EOF >> lib/src/ffi.dart
import 'bridge_generated.dart';
import 'ffi/stub.dart'
    if (dart.library.io) 'ffi/io.dart'
    if (dart.library.js_interop) 'ffi/web.dart';

$DART_CLASS_NAME? _wrapper;

$DART_CLASS_NAME createWrapper(ExternalLibrary lib) {
  _wrapper ??= createWrapperImpl(lib);
  return _wrapper!;
}
EOF

echo "export 'src/ffi.dart';" >> lib/$LIBNAME.dart
)

# Rust setup
RUST_BASE=$DART_BASE/native
mkdir -p $RUST_BASE/src

cat << EOF >> $RUST_BASE/build.rs
use lib_flutter_rust_bridge_codegen::{
    config_parse, frb_codegen, get_symbols_if_no_duplicates, RawOpts,
};

const RUST_INPUT: &str = "src/api.rs";
const DART_OUTPUT: &str = "../lib/src/bridge_generated.dart";

const IOS_C_OUTPUT: &str = "../../flutter_$LIBNAME/ios/Classes/frb.h";
const MACOS_C_OUTPUT_DIR: &str = "../../flutter_$LIBNAME/macos/Classes/";

fn main() {
    // Tell Cargo that if the input Rust code changes, rerun this build script
    println!("cargo:rerun-if-changed={}", RUST_INPUT);

    // Options for frb_codegen
    let raw_opts = RawOpts {
        rust_input: vec![RUST_INPUT.to_string()],
        dart_output: vec![DART_OUTPUT.to_string()],
        c_output: Some(vec![IOS_C_OUTPUT.to_string()]),
        extra_c_output_path: Some(vec![MACOS_C_OUTPUT_DIR.to_string()]),
        inline_rust: true,
        wasm: true,
        ..Default::default()
    };

    // Generate Rust & Dart ffi bridges
    let configs = config_parse(raw_opts);
    let all_symbols = get_symbols_if_no_duplicates(&configs).unwrap();
    for config in configs.iter() {
        frb_codegen(config, &all_symbols).unwrap();
    }

    // Format the generated Dart code
    _ = std::process::Command::new("flutter")
        .arg("format")
        .arg("..")
        .spawn();
}
EOF

cat << EOF >> $RUST_BASE/.gitignore
# Rust library related
Cargo.lock
target
EOF

cat << EOF >> $RUST_BASE/Cargo.toml
[package]
name = "$LIBNAME"
version = "0.0.0"
edition = "2018"

[lib]
crate-type = ["staticlib", "cdylib"]

[build-dependencies]
flutter_rust_bridge_codegen = "1.62.*"

[dependencies]
flutter_rust_bridge = "1.62.*"
EOF

touch $RUST_BASE/src/api.rs

cat << EOF >> $RUST_BASE/src/lib.rs
mod api;
EOF

cargo build

Flutter wrapper

On this page, we will start creating the Flutter wrapper around our Dart-only library package. We start with the plugin_ffi Flutter template since it is somewhat similar to what we need, but we will need to modify it significantly in the coming steps. Configuring the build processes for each supported platform is also a bit involved, so those are covered individually in the coming pages.

Run flutter create --help to see all the available options; you may want to set some (like --org).

Finally, in the packages folder, run the following, adding any other options you choose and replacing library_name with your library name:

flutter create --template=plugin_ffi --platforms=android,ios,macos,linux,windows --org=com.example flutter_library_name

Additional setup steps

  1. Add your Dart-only base as a dependency in your new Flutter package's pubspec.yaml. Use the version syntax, e.g. ^1.0.0. Melos will take care of the dependency resolution for us.
  2. If you choose to have integration testing in CI (recommended), add an integration_test folder to your Flutter package's and/or Flutter example package's root directory, then add the following to the pubspec.yaml of the applicable package(s):
dev_dependencies:
  flutter_test:
    sdk: flutter
  integration_test:
    sdk: flutter
  1. In /packages/flutter_library_name/lib/flutter_library_name.dart, add the following near the top of the file, replacing library_name with your Dart-only package's name:
export 'package:library_name/library_name.dart';

This re-exports your Dart-only package to users of your Flutter package, so they only need to do one flutter pub add.

  1. Finally, we will need to write some code to be able to handle FFI in Flutter. Modify the following as needed (replacing library_name and LibraryName with your library name).
// lib/src/ffi/stub.dart
Object createLibraryImpl() => throw UnimplementedError();
// lib/src/ffi/io.dart
import 'dart:ffi';
import 'dart:io';

DynamicLibrary createLibraryImpl() {
  const base = 'library_name';

  if (Platform.isIOS || Platform.isMacOS) {
    return DynamicLibrary.open('$base.framework/$base');
  } else if (Platform.isWindows) {
    return DynamicLibrary.open('$base.dll');
  } else {
    return DynamicLibrary.open('lib$base.so');
  }
}
// lib/src/ffi/web.dart
import 'package:library_name/library_name.dart';

WasmModule createLibraryImpl() {
  // TODO add web support. See:
  // https://github.com/fzyzcjy/flutter_rust_bridge/blob/master/frb_example/with_flutter/lib/ffi.web.dart
  throw UnsupportedError('Web support is not provided yet.');
}
// lib/src/ffi.dart
import 'package:library_name/library_name.dart';
import 'ffi/stub.dart'
    if (dart.library.io) 'ffi/io.dart'
    if (dart.library.js_interop) 'ffi/web.dart';

LibraryName createLib() =>
    createWrapper(createLibraryImpl());
  1. Run melos bs

Now, inside your Flutter library, you can call createLib() to get an instance of the FRB-generated Dart class! However, it won't work just yet; we will wire up our Flutter package to use our Rust binaries in the next subsection.

Platform setup

In this subsection, we will be exploring how to set up our Flutter wrapper package to bundle the platform-specific Rust binaries so users of the library will be able to actually use the library. After creating the Flutter wrapper on the previous page, you may have noticed that you get a runtime error when trying to use it as-is because of DynamicLibrary; this is because the binaries are not yet distributed along with the package!

Binary distribution

Unfortunately, at the time of writing, pub.dev has a hard 100 MB upload limit and discourages distributing platform-specific binaries through pub.dev directly. In the future, hopefully with Native Assets, there will be a way to distribute our Rust binaries through pub.dev, or something similar, which will make distribution as a library author much more convenient.

In the meantime, however, we will need to work around these limitations. There are many ways to distribute the binaries ourselves, outside of pub.dev, but in this subsection, we will cover using GitHub releases because it easily integrates with our CI/CD solution, GitHub Actions (more on this later).

How it works

If you look in your Flutter wrapper's pubspec (/packages/flutter_library_name/pubspec.yaml), you should notice the following section near the bottom (if you don't see this, or it is incomplete, add it now!):

flutter:
  plugin:
    platforms:
      ios:
        ffiPlugin: true
      macos:
        ffiPlugin: true
      android:
        ffiPlugin: true
      linux:
        ffiPlugin: true
      windows:
        ffiPlugin: true

This section of the pubspec tells Flutter that our package is using the newer ffi plugins format instead of the older platform channels Flutter has. This makes the work on our end much simpler, because instead of having to specify platform channels for each platform supported, we now only have to bundle the binaries with our package.

But, the key here is that we still must bundle our binaries along with our package. To do so, we have to follow a certain procedure (read this; it is important):

  1. We have a series of build scripts (/scripts/build-*.sh) that build all of our platform specific binaries into /platform-build and package them up appropriately, based on the target platform. Example: on iOS/macOS, this bundle is an XCFramework, on Windows/Linux, it is a .tar.gz.
  2. These binaries are uploaded to somewhere online; as mentioned previously, we will use GitHub releases in this guide (which is automated in ci).
  3. When the Dart tooling builds our library (such as when an application consuming our library is built), it invokes the platform specific build process. We hijack this build process by downloading a copy of the binaries for the needed platform, if not already present on the filesystem. This last part is the key; it allows us to run integration tests locally and in CI by providing our own copy of the binaries instead of forcing our build process to always fetch the binaries from GitHub releases.
  4. After the binaries are stored locally (either by being copied to the proper folder(s) or by fetching them from online), we extract them and place them in the needed locations.

Here are the relevant directories per platform. This is helpful for if you want to test your library on a real device or emulator locally. Also note: replace library_name below with your library name, and replace library_tag below with library_name-vVERSION where VERSION is the current version in your Dart-only pubspec.yaml. This setup is a bit of a pain to test locally with but I am not sure there is a better way at the moment (other than creating a melos script to copy over all the binaries for you). The idea here is that you will do most of your integration tests in CI.

  • Binary archive locations (copy created archives from /platform-build to these locations to test locally)
    • iOS (/plaform-build/LibraryName.xcframework.zip): /packages/flutter_library_name/ios/Frameworks/library_tag.zip
    • macOS (/platform-build/LibraryName.xcframework.zip): /packages/flutter_library_name/macos/Frameworks/library_tag.zip
    • Android (/platform-build/android.tar.gz): /packages/flutter_library_name/android/library_tag.tar.gz
    • Windows (/platform-build/other.tar.gz): /packages/flutter_library_name/windows/library_tag.tar.gz
    • Linux (/platform-build/other.tar.gz): /packages/flutter_library_name/linux/library_tag.tar.gz
  • Extracted binary locations (not as important to know, but helps understand the build process)
    • iOS: /packages/flutter_library_name/ios/Frameworks/
    • macOS: /packages/flutter_library_name/macos/Frameworks/
    • Android: /packages/flutter_library_name/android/src/main/jniLibs/
      • If you know what an aar is, Flutter does something similar for android plug-ins under the hood
    • Windows: /packages/flutter_library_name/windows/
    • Linux: /packages/flutter_library_name/linux/

Always use melos to build the latest version(s) of the binaries (e.g. melos run build:android) before copying the binary archives from /platform-build/ and testing locally! Also, do not check the /platform-build/ or /target/ directories into source control!

Windows & Linux

Windows and Linux both use CMake for their build system, so this page will walk you through setting up CMake for Windows/Linux.

Also, this page will introduce the windows & linux build script to compile your Rust library to these platforms.

CMake

CMake happens to be by far the easiest build process to set up of of all the Flutter supported platforms.

Replace all instances of library_name below with your library name. Also, replace other variables (i.e. YourGitHubAccount and repo_name) as needed.

Linux CMakeLists.txt (/packages/flutter_library_name/linux/CMakeLists.txt)

set(LibraryVersion "library_name-v0.0.0") # generated; do not edit

# The Flutter tooling requires that developers have CMake 3.10 or later
# installed. You should not increase this version, as doing so will cause
# the plugin to fail to compile for some customers of the plugin.
cmake_minimum_required(VERSION 3.10)

# Project-level configuration.
set(PROJECT_NAME "flutter_library_name")
project(${PROJECT_NAME} LANGUAGES CXX)

# Download the binaries if they are not already present.
set(LibRoot "${CMAKE_CURRENT_SOURCE_DIR}/${LibraryVersion}")
set(ArchivePath "${LibRoot}.tar.gz")
if(NOT EXISTS ${ArchivePath})
  file(DOWNLOAD
    "https://github.com/YourGitHubAccount/repo_name/releases/download/${LibraryVersion}/other.tar.gz"
    ${ArchivePath}
    TLS_VERIFY ON
  )
endif()

# Extract the binaries, overriding any already present.
file(REMOVE_RECURSE ${LibRoot})
file(MAKE_DIRECTORY ${LibRoot})
execute_process(
  COMMAND ${CMAKE_COMMAND} -E tar xzf ${ArchivePath}
  WORKING_DIRECTORY ${LibRoot}
)

# List of absolute paths to libraries that should be bundled with the plugin.
# This list could contain prebuilt libraries, or libraries created by an
# external build triggered from this build file.
set(flutter_library_name_bundled_libraries
  "${LibRoot}/${FLUTTER_TARGET_PLATFORM}/liblibrary_name.so"
  PARENT_SCOPE
)

Windows CMakeLists.txt (/packages/flutter_library_name/windows/CMakeLists.txt)

set(LibraryVersion "library_name-v0.0.0") # generated; do not edit

# TODO Remove this workaround once Flutter supports Windows ARM.
# https://github.com/flutter/flutter/issues/116196
set(FLUTTER_TARGET_PLATFORM windows-x64)

# The Flutter tooling requires that developers have a version of Visual Studio
# installed that includes CMake 3.14 or later. You should not increase this
# version, as doing so will cause the plugin to fail to compile for some
# customers of the plugin.
cmake_minimum_required(VERSION 3.14)

# Project-level configuration.
set(PROJECT_NAME "flutter_library_name")
project(${PROJECT_NAME} LANGUAGES CXX)

# Download the binaries if they are not already present.
set(LibRoot "${CMAKE_CURRENT_SOURCE_DIR}/${LibraryVersion}")
set(ArchivePath "${LibRoot}.tar.gz")
if(NOT EXISTS ${ArchivePath})
  file(DOWNLOAD
    "https://github.com/YourGitHubAccount/repo_name/releases/download/${LibraryVersion}/other.tar.gz"
    ${ArchivePath}
    TLS_VERIFY ON
  )
endif()

# Extract the binaries, overriding any already present.
file(REMOVE_RECURSE ${LibRoot})
file(MAKE_DIRECTORY ${LibRoot})
execute_process(
  COMMAND ${CMAKE_COMMAND} -E tar xzf ${ArchivePath}
  WORKING_DIRECTORY ${LibRoot}
)

# List of absolute paths to libraries that should be bundled with the plugin.
# This list could contain prebuilt libraries, or libraries created by an
# external build triggered from this build file.
set(flutter_library_name_bundled_libraries
  "${LibRoot}/${FLUTTER_TARGET_PLATFORM}/library_name.dll"
  PARENT_SCOPE
)

Platform-Specific Peculiarities

There exists a few differences between the Linux and Windows CMakeLists.txts:

  1. The minimum CMake version supported
  2. At the time of writing, Windows CMake does not yet have a builtin FLUTTER_TARGET_PLATFORM variable; thus, we need to define a dummy version of the variable. See here for updates on this issue
  3. On linux, dynamic library names follow the form of liblibrary_name.so and on Windows, dynamic library names follow the form of library_name.dll

.gitignore

If you choose to have a .gitignore in your linux/ and windows/ directories, here is what the author of this page uses:

# Set up as allowlist
*

# Allowed files
!.gitignore
!CMakeLists.txt

Build Script (/scripts/build-other.sh)

Replace library_name below as needed.

#!/bin/bash

# Setup
BUILD_DIR=platform-build
mkdir $BUILD_DIR
cd $BUILD_DIR

# Install build dependencies
cargo install cargo-zigbuild
cargo install cargo-xwin

zig_build () {
    local TARGET="$1"
    local PLATFORM_NAME="$2"
    local LIBNAME="$3"
    rustup target add "$TARGET"
    cargo zigbuild --target "$TARGET" -r
    mkdir "$PLATFORM_NAME"
    cp "../target/$TARGET/release/$LIBNAME" "$PLATFORM_NAME/"
}

win_build () {
    local TARGET="$1"
    local PLATFORM_NAME="$2"
    local LIBNAME="$3"
    rustup target add "$TARGET"
    cargo xwin build --target "$TARGET" -r
    mkdir "$PLATFORM_NAME"
    cp "../target/$TARGET/release/$LIBNAME" "$PLATFORM_NAME/"
}

# Build all the dynamic libraries
LINUX_LIBNAME=liblibrary_name.so
zig_build aarch64-unknown-linux-gnu linux-arm64 $LINUX_LIBNAME
zig_build x86_64-unknown-linux-gnu linux-x64 $LINUX_LIBNAME
WINDOWS_LIBNAME=library_name.dll
win_build aarch64-pc-windows-msvc windows-arm64 $WINDOWS_LIBNAME
win_build x86_64-pc-windows-msvc windows-x64 $WINDOWS_LIBNAME

# Archive the dynamic libs
tar -czvf other.tar.gz linux-* windows-*

# Cleanup
rm -rf linux-* windows-*

iOS & macOS

Flutter libraries targeting iOS and macOS use cocoapods for dependencies, so this page will walk you through setting that up with FRB.

The simplist way the author of this page found to integrate with cocoapods for all Apple platforms (iOS, iOS Simulator, and macOS) is to create an XCFramework. While you don't need to know what an XCFramework is to follow this guide, if you want to understand how this all works behind the scenes, I'd recommend doing a quick Google search on "What is an XCFramework?".

Also, this page will introduce the iOS & macOS build script (build-apple.sh) to compile your Rust library for all Apple platforms. Note: unlike all of the other build scripts presented in this guide (which we can run on any host OS), build-apple.sh must be run on macOS.

Directory Tree

We will need to create several files for both iOS and macOS to:

  • Prevent our Rust symbols from being accidentally stripped
  • Bundle our "XCFramework" with our Flutter library

ios/Classes/EnforceBundling.swift and macos/Classes/EnforceBundling.swift

public func dummyMethodToEnforceBundling() -> Int64 {
  return dummy_method_to_enforce_bundling()
}
let dummyVar = dummyMethodToEnforceBundling();

ios/Frameworks/.gitkeep and macos/Frameworks/.gitkeep

No file contents here; simply add a blank file (i.e., touch .gitkeep in bash).

ios/.gitignore

Flutter/
Runner/
Frameworks/*
!Frameworks/.gitkeep

macos/.gitignore

Flutter/
Frameworks/*
!Frameworks/.gitkeep

ios/flutter_library_name.podspec and macos/flutter_library_name.podspec (for Cocoapods)

We cannot use the CMake approach taken on other platforms with Cocoapods, so we do something a bit different here. .podspec files are actually just ruby files; due to this observation, we can access the system shell to make arbitrary changes. While we could download and extract our Rust binaries for iOS/macOS in ruby directly, it is much more straightforward to simply use bash/zsh.

Replace all instances of library_name and LibraryName below with your library name. Also, replace other variables (i.e. YourGitHubAccount and repo_name) as needed.

Note: the same exact flutter_library_name.podspec is used for both iOS and macOS; you can thank the XCFramework for this simplicity.

release_tag_name = 'library_name-v0.0.0' # generated; do not edit

# We cannot distribute the XCFramework alongside the library directly,
# so we have to fetch the correct version here.
framework_name = 'LibraryName.xcframework'
remote_zip_name = "#{framework_name}.zip"
url = "https://github.com/YourGitHubAccount/repo_name/releases/download/#{release_tag_name}/#{remote_zip_name}"
local_zip_name = "#{release_tag_name}.zip"
`
cd Frameworks
rm -rf #{framework_name}

if [ ! -f #{local_zip_name} ]
then
  curl -L #{url} -o #{local_zip_name}
fi

unzip #{local_zip_name}
cd -
`

Pod::Spec.new do |spec|
  spec.name          = 'library_name'
  spec.version       = '0.0.1'
  spec.license       = { :file => '../LICENSE' }
  spec.homepage      = 'https://github.com/YourGitHubAccount/repo_name'
  spec.authors       = { 'Your Name' => 'your-email@example.com' }
  spec.summary       = 'iOS/macOS Flutter bindings for library_name'

  spec.source              = { :path => '.' }
  spec.source_files        = 'Classes/**/*'
  spec.public_header_files = 'Classes/**/*.h'
  spec.vendored_frameworks = "Frameworks/#{framework_name}"

  spec.ios.deployment_target = '11.0'
  spec.osx.deployment_target = '10.11'
end

Build Script (/scripts/build-apple.sh)

Replace library_name and LibraryName below as needed.

#!/bin/bash

# Setup
BUILD_DIR=platform-build
mkdir $BUILD_DIR
cd $BUILD_DIR

# Build static libs
for TARGET in \
        aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim \
        x86_64-apple-darwin aarch64-apple-darwin
do
    rustup target add $TARGET
    cargo build -r --target=$TARGET
done

# Create XCFramework zip
FRAMEWORK="LibraryName.xcframework"
LIBNAME=liblibrary_name.a
mkdir mac-lipo ios-sim-lipo
IOS_SIM_LIPO=ios-sim-lipo/$LIBNAME
MAC_LIPO=mac-lipo/$LIBNAME
lipo -create -output $IOS_SIM_LIPO \
        ../target/aarch64-apple-ios-sim/release/$LIBNAME \
        ../target/x86_64-apple-ios/release/$LIBNAME
lipo -create -output $MAC_LIPO \
        ../target/aarch64-apple-darwin/release/$LIBNAME \
        ../target/x86_64-apple-darwin/release/$LIBNAME
xcodebuild -create-xcframework \
        -library $IOS_SIM_LIPO \
        -library $MAC_LIPO \
        -library ../target/aarch64-apple-ios/release/$LIBNAME \
        -output $FRAMEWORK
zip -r $FRAMEWORK.zip $FRAMEWORK

# Cleanup
rm -rf ios-sim-lipo mac-lipo $FRAMEWORK

Android

There are a few different ways to integrate with our Android binaries when building for Android. None are particularly outstanding:

  • An "Ivy Repository"
    • Works great, but impossible to test changes on an emulator locally or in CI :(
  • Raw Groovy & Gradle
    • Works in theory, but tedious to have to write all needed logic in Groovy/Gradle
  • Starting an OS Shell
    • Similar to iOS & macOS, see that section for more details on this
    • Wouldn't work on Windows development machines unfortunately; the started shell would not be bash
  • CMake
    • We call to CMake from Gradle to take care of fetching and processing our Android binaries
    • A somewhat odd approach, but works cross-platform and has code re-use from Windows/Linux!

Due to the above reasoning, we cover how to use CMake on this page. But do note, there are other possibilities out there.

CMake (/packages/flutter_library_name/android/CMakeLists.txt)

Unlike windows and linux CMakeLists.txt, the android equivalent does not actually build anything, which may come as a surprise. Instead, its sole purpose is to download & extract our Android binaries in a cross-platform friendly way. Here is our android CMakeLists.txt:

set(LibraryVersion "library_name-v0.0.0") # generated; do not edit
set(PROJECT_NAME "project_name")

# Unlike the Windows & Linux CMakeLists.txt, this Android equivalent is just here
# to download the Android binaries into src/main/jniLibs/ and does not build anything.
# The binary download/extraction is difficult to do concisely in Groovy/Gradle,
# at least across host platforms, so we are just reusing our Linux/Windows logic.

# The Flutter tooling requires that developers have CMake 3.10 or later
# installed. You should not increase this version, as doing so will cause
# the plugin to fail to compile for some customers of the plugin.
cmake_minimum_required(VERSION 3.10)

project(PROJECT_NAME)

# Download the binaries if they are not already present.
set(LibRoot "${CMAKE_CURRENT_SOURCE_DIR}/src/main/jniLibs")
set(ArchivePath "${CMAKE_CURRENT_SOURCE_DIR}/${LibraryVersion}.tar.gz")
if(NOT EXISTS ${ArchivePath})
  file(DOWNLOAD
    "https://github.com/YourGitHubAccount/repo_name/releases/download/${LibraryVersion}/android.tar.gz"
    ${ArchivePath}
    TLS_VERIFY ON
  )
endif()

# Extract the binaries, overriding any already present.
file(REMOVE_RECURSE ${LibRoot})
file(MAKE_DIRECTORY ${LibRoot})
execute_process(
  COMMAND ${CMAKE_COMMAND} -E tar xzf ${ArchivePath}
  WORKING_DIRECTORY ${LibRoot}
)

Replace all instances of library_name above with your library name. Also, replace other variables (i.e. YourGitHubAccount and repo_name) as needed.

build.gradle Changes

Replace the android {...} section at the bottom of build.gradle with the following:

android {
    compileSdkVersion 31

    defaultConfig {
        minSdkVersion 16
    }

    // Trigger the binary download/update over in CMakeLists.txt
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

.gitignore

Add the following to android/.gitignore

# Ignore Rust binaries
src/main/jniLibs/
*.tar.gz

Build Script (/scripts/build-android.sh)

#!/bin/bash

# Setup
BUILD_DIR=platform-build
mkdir $BUILD_DIR
cd $BUILD_DIR

# Create the jniLibs build directory
JNI_DIR=jniLibs
mkdir -p $JNI_DIR

# Set up cargo-ndk
cargo install cargo-ndk
rustup target add \
        aarch64-linux-android \
        armv7-linux-androideabi \
        x86_64-linux-android \
        i686-linux-android

# Build the android libraries in the jniLibs directory
cargo ndk -o $JNI_DIR \
        --manifest-path ../Cargo.toml \
        -t armeabi-v7a \
        -t arm64-v8a \
        -t x86 \
        -t x86_64 \
        build --release 

# Archive the dynamic libs
cd $JNI_DIR
tar -czvf ../android.tar.gz *
cd -

# Cleanup
rm -rf $JNI_DIR

Continuous Integration & Deployment (CI/CD)

The CI/CD detailed here, using GitHub Actions, automates a lot of the busy work that you would otherwise need to maintain your library. These workflows include:

  • Automatic dependency updates with dependabot
  • Continuous Integration (CI)
    • Unit tests and code checks on pushes/PRs to main
    • Integration tests on real & emulated devices on pushes/PRs to main
  • Continuous Deployment (CD)
    • Manual version/release creation with Melos through a workflow dispatch
      • You can set this up to be automated, but in most cases you don't want a new release on every commit to main
    • Automated publishing of new versions to GitHub releases and pub.dev

Dependabot (/.github/dependabot.yaml)

It is highly recommended that you set up dependabot to automatically submit PRs when your dependencies fall out of date.

Replace library_name below with your library name.

version: 2
enable-beta-ecosystems: true
updates:
  - package-ecosystem: pub
    directory: "/packages/library_name"
    schedule:
      interval: weekly
  - package-ecosystem: pub
    directory: "/packages/library_name/example"
    schedule:
      interval: weekly
  - package-ecosystem: pub
    directory: "/packages/flutter_library_name"
    schedule:
      interval: weekly
  - package-ecosystem: pub
    directory: "/packages/flutter_library_name/example"
    schedule:
      interval: weekly
  - package-ecosystem: cargo
    directory: "/packages/library_name/native"
    schedule:
      interval: weekly

Continuous Integration (/.github/workflows/build.yml)

Replace library_name and LibraryName below with your library name.

name: Build & Test

on:
  pull_request:
  push:
    branches:
      - main
  schedule:
    # runs the CI everyday at 10AM
    - cron: "0 10 * * *"

jobs:
  # General build, check, and test steps
  build_and_test:
    runs-on: ubuntu-latest

    steps:
      # Setup
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
      - uses: bluefireteam/melos-action@v2
      - uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: stable
          components: rustfmt, clippy

      # Rust
      - name: Check Rust format
        working-directory: ./packages/library_name/native/src
        run: rustfmt --check lib.rs
      - name: Rust code analysis
        run: cargo clippy -- -D warnings
      - name: Run Rust tests
        run: cargo test
      - name: Build Rust code for Dart tests
        run: cargo build -r

      # Dart/Flutter
      - name: Check Dart format
        run: melos run check-format --no-select
      - name: Dart code analysis
        run: melos run analyze --no-select
      - name: Run Dart tests
        run: melos run test

  macos_integration_test:
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
      - uses: bluefireteam/melos-action@v2
      - uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: stable

      - name: Build the XCFramework
        run: melos run build:apple
      - name: Copy the XCFramework to the needed location
        run: |
          CURR_VERSION=library_name-v`awk '/^version: /{print $2}' packages/library_name/pubspec.yaml`
          cp platform-build/LibraryName.xcframework.zip packages/flutter_library_name/macos/Frameworks/$CURR_VERSION.zip
          echo Copied file!

      - name: Run Flutter integration tests
        working-directory: packages/flutter_library_name/example
        run: flutter test -d macos integration_test

  windows_integration_test:
    runs-on: windows-latest

    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
      - uses: bluefireteam/melos-action@v2
      - uses: goto-bus-stop/setup-zig@v2
      - uses: KyleMayes/install-llvm-action@v1
        with:
          version: "15"
      - uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: stable

      - name: Build the binaries
        run: melos run build:other
      - name: Copy the binaries to the needed location
        shell: bash
        run: |
          CURR_VERSION=library_name-v`awk '/^version: /{print $2}' packages/library_name/pubspec.yaml`
          cp platform-build/other.tar.gz packages/flutter_library_name/windows/$CURR_VERSION.tar.gz
          echo Copied file!

      - name: Run Flutter integration tests
        working-directory: packages/flutter_library_name/example
        run: flutter test -d windows integration_test

  linux_integration_test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - name: Install dependencies for flutter integration test
        run: sudo apt update && sudo apt-get install -y libglu1-mesa ninja-build clang cmake pkg-config libgtk-3-dev liblzma-dev
      - uses: pyvista/setup-headless-display-action@v1
      - uses: subosito/flutter-action@v2
      - uses: bluefireteam/melos-action@v2
      - uses: goto-bus-stop/setup-zig@v2
      - uses: KyleMayes/install-llvm-action@v1
        with:
          version: "15"
      - uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: stable

      - name: Build the binaries
        run: melos run build:other
      - name: Copy the binaries to the needed location
        run: |
          CURR_VERSION=library_name-v`awk '/^version: /{print $2}' packages/library_name/pubspec.yaml`
          cp platform-build/other.tar.gz packages/flutter_library_name/linux/$CURR_VERSION.tar.gz
          echo Copied file!

      - name: Run Flutter integration tests
        working-directory: packages/flutter_library_name/example
        run: flutter test -d linux integration_test

  ios_integration_test:
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
      - uses: bluefireteam/melos-action@v2
      - uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: stable

      - name: Start iOS Simulator
        run: |
          DEVICE_ID=$(xcrun xctrace list devices | grep iPhone | head -1 | awk '{print $NF}' | tr -d '()')
          echo "DEVICE_ID=$DEVICE_ID" >> $GITHUB_ENV
          xcrun simctl boot $DEVICE_ID

      - name: Build the XCFramework
        run: melos run build:apple
      - name: Copy the XCFramework to the needed location
        run: |
          CURR_VERSION=library_name-v`awk '/^version: /{print $2}' packages/library_name/pubspec.yaml`
          cp platform-build/LibraryName.xcframework.zip packages/flutter_library_name/ios/Frameworks/$CURR_VERSION.zip
          echo Copied file!

      - name: Run Flutter integration tests
        working-directory: packages/flutter_library_name/example
        run: flutter test -d ${{ env.DEVICE_ID }} integration_test

  android_integration_test:
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
      - uses: bluefireteam/melos-action@v2
      - uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: stable
      - uses: nttld/setup-ndk@v1
        with:
          ndk-version: r25b
      - uses: actions/setup-java@v3
        with:
          distribution: zulu
          java-version: "11.x"

      - name: Build the binaries
        run: melos run build:android
      - name: Copy the binaries to the needed location
        run: |
          CURR_VERSION=library_name-v`awk '/^version: /{print $2}' packages/library_name/pubspec.yaml`
          cp platform-build/android.tar.gz packages/flutter_library_name/android/$CURR_VERSION.tar.gz
          echo Copied file!

      - name: Run Flutter integration tests
        uses: Wandalen/wretry.action@master # sometimes android tests are flaky
        with:
          attempt_limit: 5
          action: reactivecircus/android-emulator-runner@v2
          with: |
            api-level: 33
            target: google_apis
            arch: x86_64
            ram-size: 1024M
            disk-size: 2048M
            script: cd packages/flutter_library_name/example && flutter test -d `flutter devices | grep android | tr ' ' '\n' | grep emulator-` integration_test

Continuous Deployment

There are two files you need for CD:

  1. Create new versions/releases with Melos
  2. Publish new releases to GitHub releases and pub.dev

Create new versions with Melos (/.github/workflows/create-release.yml)

You can create new releases of your library with this workflow by going to the "Actions" tab in your GitHub repo and manually starting this workflow with an appropriate option. The options are:

  • -- -> call melos version with no additional parameters
  • --prerelease -> create a prerelease version instead of normal release (e.g., 1.0.0-dev.0)
  • --graduate -> graduate a prerelease version to a normal release (e.g., 1.0.0-dev.0 becomes 1.0.0)

You will need to set a repository secret of BOT_ACCESS_TOKEN to your GitHub personal access token (PAT) to allow for pushes to main from this Action.

Change YourName and your-email@example.com below as appropriate.

name: Create Release(s)

on:
  workflow_dispatch:
    inputs:
      version_parameters:
        description: 'Parameters to pass to "melos version"'
        required: true
        default: " "
        type: choice
        options:
          - "--"
          - "--prerelease"
          - "--graduate"

jobs:
  create_release:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
        with:
          token: ${{ secrets.BOT_ACCESS_TOKEN }}
          fetch-depth: 0
      - name: Setup git
        run: |
          git config user.name "YourName"
          git config user.email "your-email@example.com"
      - uses: subosito/flutter-action@v2
      - uses: bluefireteam/melos-action@v2

      - name: Create the new version(s)
        run: melos version --yes ${{ inputs.version_parameters }}

      - name: Push created version commit
        run: git push
      - name: Push modified tags
        run: git push --tags

Publish new releases to GitHub releases and pub.dev (/.github/workflows/publish-release.yml)

In order for this workflow to execute correctly and publish packages to pub.dev, you need to have the contents of your pub credentials JSON file in a GitHub repo secret.

First you need to sign-in into your pub account locally by running the following command: dart pub login.

After the authorization is completed, open the credentials file, which can be found:

  • On Linux, at ~/.config/dart/pub-credentials.json
  • On macOS, at ~/Library/Application Support/dart/pub-credentials.json
  • On Windows, at C:\Users\YourUsername\AppData\Roaming\dart\pub-credentials.json

And copy the contents of this pub-credentials.json file to a new GitHub repo secret named PUB_CRED_JSON.

This workflow is set to execute whenever new version tags are pushed up to GitHub.

name: Publish Release(s)

on:
  push:
    tags:
      - "*"

jobs:
  publish_github_release:
    # macOS because we can cross-compile to all other platforms from it
    runs-on: macos-latest

    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
      - uses: bluefireteam/melos-action@v2
      - uses: goto-bus-stop/setup-zig@v2
      - uses: KyleMayes/install-llvm-action@v1
        with:
          version: "15"
      - uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: stable
      - uses: nttld/setup-ndk@v1
        with:
          ndk-version: r25b

      - name: Build all library binaries
        run: melos run build

      - name: Create GitHub release
        uses: softprops/action-gh-release@v1
        with:
          files: platform-build/*

  publish_pub_release:
    needs: publish_github_release
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
      - uses: bluefireteam/melos-action@v2
      - name: Setup pub.dev credentials
        run: |
          mkdir -p $HOME/.config/dart
          cat << EOF > $HOME/.config/dart/pub-credentials.json
          ${{ secrets.PUB_CRED_JSON }}
          EOF
      - name: Dry-run publish to pub.dev
        run: melos publish -y --dry-run
      - name: Publish to pub.dev
        run: melos publish -y --no-dry-run

Overview

Prelude

Firstly, welcome, and thanks for your contributions!

If you want to contribute, feel free to create a Pull Request. If you need some ideas of what to contribute, have a look at the Issues section of this repository.

For a checklist, have a look at the PR template.

Overall design

To get a high-level idea how this library is implemented, here is the overall design: link.

Overall design

This doc is still WIP. Tracking issue: https://github.com/fzyzcjy/flutter_rust_bridge/issues/593

Folder structure

  • frb_codegen: Code generator. It inputs api.rs and outputs Rust and Dart code files.
  • frb_example: Examples.
    • pure_dart: Not only an example, but, more importantly, serves as end-to-end tests.
    • with_flutter: Example with integration into Flutter.
    • pure_dart_multi: Demonstrate multi-file usage.
  • frb_dart: Support library for Dart - to be imported by users.
  • frb_rust: Support library for Rust - to be imported by users.
  • frb_macros: Indeed part of frb_rust. It is a separate package simply because limitation of proc macros.
  • book: The documentation.
  • .github: GitHub-related.
    • workflows/ci.yaml: Definition of CI workflows.

Terminology

Rust IO Wire types refers to the C types the Dart VM uses to communicate with the Rust library.

Dart IO Wire types are the Dart counterpart of Rust IO wire types, but in the *.io.dart files. Both Rust and Dart wire types communicate using the vocabulary of C types, aka primitives, structs, unions and pointers.

Rust JS Wire types are the WASM equivalent of Rust IO wire types, many of which are distinct from their C siblings. In addition, these types may also take the form of the catch-all JsValue.

Dart JS Wire types are the WASM equivalent of Dart IO wire types, but unlike Rust JS wire types, most of these types remain identical to their real API counterparts. Similar to the the relationship between Rust IO and Dart IO wire types, Rust JS and Dart JS wire types use the vocabulary of JavaScript types, aka primitives, arrays, typed arrays and objects.

Code-generator structure

The pipeline is as follows:

flowchart LR
api.rs -- src/parser --> src/ir
src/ir -- src/generator --> rd[Rust & Dart]
  • The input, api.rs in the figure, is the user-provided handwritten Rust code.
  • The parser (src/parser) converts the input code (indeed syn tree) into IR.
  • IR (src/ir), or internal representation, is a data structure that represents the information of the code that we are interested in.
  • The generator (src/generator) converts the IR into final outputs. More specifcially, as you can probably guess, src/generator/dart generates Dart code, src/generator/rust is for Rust code, and src/generator/c is for (a bit of) C code.
  • The outputs (Rust & Dart in the figure) are written to corresponding files.

Data flow

Let us see what happens when a function is called.

Suppose a user calls a (generated) Dart function func({required String str}). Then, the following happens:

  1. The generated Dart function, func({required String str}), convert "Dart api data" (i.e. the data that user really provides) into "Dart wire data" (i.e. the data that will really pass between Dart and Rust). More specifically, it calls _api2wire_String(str) and get a ffi.Pointer<wire_uint_8_list> (because Strings use pub struct wire_uint_8_list { ptr: *mut u8, len: i32 } under the hood).
  2. Now we call the Dart version of wire_func, with low-level data like wire_uint_8_list. We have used our codegen to create a Rust wire_func function, and use cbindgen to generate the corresponding C function, and use ffigen to get the corresponding Dart function. Here, we call the Dart version of wire_func. Since Dart FFI and Rust FFI is C-compatible, it seamlessly calls the Rust version of wire_func. Notice that, since we are utilizing C-compatible functions (and it is the only feasible way), we can only pass around low-level things like pointers, instead of high-level and safe things.
  3. Surely, the Rust wire_func is called. The function uses .wire2api() to convert "Rust wire data" (wire_uint_8_list here) into "Rust api data" (String here, i.e. data that users really use).
  4. The FLUTTER_RUST_BRIDGE_HANDLER is called with "Rust api data". That handler is user-customizable, so users may provide their own implementation other than the default thread-pool, etc. By default, we use a thread pool, and we call the user-written func Rust function in api.rs.
  5. The user-written fn func(str: String) -> String { ... } is called, and we get a return value.
  6. The return value, a String, is posted to the Dart side. It is done by the Dart-provided API, Dart_PostCObject, which let us provide C structs and it will automatically become Dart data on the other side. We use the Rust-safe wrapper allo-isolate for it. We deliberately choose this, because this enables Dart code to be async instead of sync.
  7. On the Dart side, we now see some Dart objects (indeed "Dart wire data"). We use functions like _wire2api_SomeType to convert it to the final "Dart api data". Notice this "wire2api" is on Dart side, so it means "Dart wire data to Dart api data", and is different from the one above which is for Rust. For example, since Dart_PostCObject does not provide a way to construct arbitrary structs(classes), we have to pass Rust structs as lists, and use the wire2api to convert them to corresponding Dart classes.
  8. The final result value is provided as return value of the Dart function, func, that the user called just now. A function call finishes!

Type Mappings

Unless otherwise noted, T refers to a type from the same column or the generic type. Does not include delegated types.

RustRust IO WireDart IO WireRust JS WireDart JS WireDart
i{8..32}i{8..32}int1i{8..32}intint
u{8..32}u{8..32}int1u{8..32}intint
i64i64intBigIntBigIntint
u64u64intBigIntBigIntint
usizeusizeintusizeintint
boolboolboolboolboolbool
Vec<i{8..32}>wire_int_{8..32}_listwire_int_{8..32}_listBox<[i{8..32}]>Int{8..32}ArrayInt{8..32}List
Vec<u{8..32}>wire_uint_{8..32}_listwire_uint_{8..32}_listBox<[u{8..32}]>Uint{8..32}ArrayUint{8..32}List
Vec<i64>wire_int_64_listwire_int_64_listBox<[i64]>BigInt64ArrayInt64List2
Vec<u64>wire_uint_64_listwire_uint_64_listBox<[u64]>BigUint64ArrayUint64List2
Stringwire_uint_8_listwire_uint_8_listStringStringString
Vec<String>wire_StringListwire_StringListBox<[String]>ListList<String>
Vec<T>wire_list_twire_list_tBox<[JsValue]>ListList<T>
Box<T>*mut Tffi.Pointer<T>TTT
Option<T>*mut Tffi.Pointer<T>Option<T>T?T?
Option<Box<T>>*mut Tffi.Pointer<T>Option<T>T?T?
enum/struct T*mut wire_tffi.Pointer<T>ArrayListclass T
enum T3i324int1i324intenum T
DartAbiDartCObjectdynamicJsValuedynamicdynamic

Memory safety

How is memory safety implemented? This is a case-by-case problem. For example, suppose we want to see how a String is safely passed from Dart to Rust. Then, we need to examine the Dart _api2wire_String and the Rust .wire2api() for it.

Indeed String is implemented by delegating to Vec<u8>, so we need to see code related to String as well as Vec<u8>. By simply clicking a few times and jump around code, we will see that:

ffi.Pointer<wire_uint_8_list> _api2wire_String(String raw) {
  return _api2wire_uint_8_list(utf8.encoder.convert(raw));
}

ffi.Pointer<wire_uint_8_list> _api2wire_uint_8_list(Uint8List raw) {
  final ans = inner.new_uint_8_list_0(raw.length);
  ans.ref.ptr.asTypedList(raw.length).setAll(0, raw);
  return ans;
}

and

impl Wire2Api<Vec<u8>> for *mut wire_uint_8_list {
    fn wire2api(self) -> Vec<u8> {
        unsafe {
            let wrap = support::box_from_leak_ptr(self);
            support::vec_from_leak_ptr(wrap.ptr, wrap.len)
        }
    }
}

impl Wire2Api<String> for *mut wire_uint_8_list {
    fn wire2api(self) -> String {
        let vec: Vec<u8> = self.wire2api();
        String::from_utf8_lossy(&vec).into_owned()
    }
}

pub struct wire_uint_8_list {
    ptr: *mut u8,
    len: i32,
}

In other words, String (or Vec<u8>) is converted to a raw struct with pointer and length field. The memory is manipulated carefully so there is no leak or double free.

We use Valgrind to check as well, and I use it in production environment without problems, so no worries about memory problems :)

Dart bridge hierarchy

A bridge module consists of several classes:

  • One _Impl class implementing the wire functions and common helpers; and
  • One or more _Platform classes implementing the platform-specific helpers.

The implementor class takes a platform class as a private attribute, and the platform class exposes all of its members decorated with @protected. The specific platform class to be used is gated by conditional imports.

Cross-scope communication in the browser

On Web platforms, for lack of a proper SendPort there exists replacements from dart:html.

MessagePort replaces dart:ffi's SendPort and is created from MessageChannel. The Dart thread creates a channel, keeps the receive port and transfers the send port to the workers.

sequenceDiagram
Dart ->> Rust: port2
Rust ->> Rust Worker: port2
Rust Worker ->> Dart: port2.postMessage

BroadcastChannel replaces dart:ffi's SendPort for StreamSinks, due to the fact that wasm_bindgen keeps the ports in a JS-local scope that cannot be shared with other threads. A broadcast channel is created by Dart, then passed to the main Rust thread. Rust then transfers its name to the workers. When other workers refer to a StreamSink from another worker, e.g. if the sink was put in a static variable, a new BroadcastChannel will be created from its name.

BroadcastChannels are guaranteed to be unique for each invocation.5

sequenceDiagram
Dart ->> Rust: channel
Rust ->> Rust Worker 1: channel.name
Rust Worker 1 ->> Dart: channel.postMessage
Rust ->> Rust Worker 2: channel.name
Rust Worker 2 ->> Dart: channel.postMessage

It is theoretically possible to have a one-to-one implementation of Isolate using only web primitives, BroadcastChannels and Workers, but it remains to be seen how practical such an approach would be.

OptionalList

Per the implementation, most IRs are also accompanied by a List type (GeneralList, PrimitiveList, StringList etc.) each of which handles lists in different ways. When Optional was first implemented, it relied on GeneralList since the underlying assumption that Optional already boxed stack values should allow for seamless interaction. Howver, this became an issue later because other IRs would have to accommodate for Optionals instead of being perfectly encapsulated, leading to ugly hacks. #1388 introduced OptionalList to bring Optional in line with other IRs, and is implemented as a list of maybe-null pointers. It does highlight several drawbacks to this approach to IRs where specializations shine compared to GeneralList.

  1. GeneralList requires a fully-allocated list and asks the Dart side to fill in the blanks via api_fill functions, but these are not implemented by any delegates since they all have their own special lists (StringList, TimeList, Uuids). This renders types like List<String?> difficult to implement without hacks.
  2. OptionalList's inner pointer is a *mut *mut T, which without significant refactoring would be difficult to represent with GeneralList, and whose typical usage doesn't really require double indirection often enough to justify it.
  3. OptionalList enables future optimizations, for example the case when sizeof(T) <= sizeof(usize), which would certainly be difficult to accomplish with GeneralList.

Want to know more? Tell me

What do you want to know? Feel free to create an issue in GitHub, and I will tell more :)

1

When behind a ffi.Pointer, they are their respective types from dart:ffi: ffi.Int8, ffi.Int16, etc.

2

These types are unsupported on Web by dart:typed_list, so this library provides a barebores shim over the JS native types. If you wish to use these types, replace all dart:typed_list imports with this library.

3

Refers to C-style enums only (no fields).

5

This is currently implemented as a monotonically-increasing index.

4

Enums may also specify a #[repr], which is planned to be implemented.

Submodule implementations

In this chapter, we will present implementations and discussions for various submodules.

Rust opaque type safety

Restrictions

A RustOpaque type can be created from any Rust structure. The flutter_rust_bridge async dart api requires the Rust type to be Send and Sync, due to the possible sharing of RustOpaque type by multiple flutter_rust_bridge executor threads.

Ownership and GC

From the moment an opaque type is passed to Dart, it has full ownership of it. Dart implements a finalizer for opaque types, but the memory usage of opaque types is not monitored by Dart and can accumulate, so in order to prevent memory leaks, opaque pointers must be disposed.

Rust opaque type like function args

When calling a function with an opaque type argument, the Dart thread safely shares ownership of the opaque type with Rust. This is safe because RustOpaque<T> requires that T be Send and Sync, furthermore Rust's RustOpaque<T> hand out immutable references through Deref or get an internal property if only Rust owns the opaque type. If dispose is called on the Dart side before the function call completes, Rust takes full ownership.

Example

Case 1: Simple call.

Rust api.rs:

pub use crate::data::HideData; // `pub` for bridge_generated.rs

pub fn create_opaque() -> RustOpaque<HideData> {
    // [`HideData`] has private fields.
    RustOpaque::new(HideData::new())
}

pub fn run_opaque(opaque: RustOpaque<HideData>) -> String {
    // RustOpaque impl Deref trait.
    opaque.hide_data()
}

Dart: (test:'Simple call' frb_example/pure_dart/dart/lib/main.dart)

// (Arc counter = 1) Dart has full ownership.
var opaque = await api.createOpaque();

// (Arc counter = 2) for the duration of the function 
// and after (Arc counter = 1).
// 
// Dart and Rust share the opaque type.
String hideData = await api.runOpaque(opaque);

// (Arc counter = 0) opaque type is dropped (deallocated).
opaque.dispose();

Case 2: Call after dispose.

Rust api.rs:

pub use crate::data::HideData; // `pub` for bridge_generated.rs

pub fn create_opaque() -> RustOpaque<HideData> {
    // [`HideData`] has private fields.
    RustOpaque::new(HideData::new())
}

pub fn run_opaque(opaque: RustOpaque<HideData>) -> String {
    // RustOpaque impl Deref trait.
    opaque.hide_data()
}

Dart: (test:'Call after dispose' frb_example/pure_dart/dart/lib/main.dart)

// (Arc counter = 1) Dart has full ownership.
var opaque = await api.createOpaque();

// (Arc counter = 0) opaque type dropped (deallocated)
opaque.dispose();

// (Arc counter = 0) Dart throws StateError('Use after dispose.')
try {
    await api.runOpaque(opaque: opaque);
} on StateError catch (e) {
    expect(e.toString(), 'Bad state: Use after dispose.');
}

Case 3: Dispose before complete.

Rust api.rs:

pub use crate::data::HideData; // `pub` for bridge_generated.rs

pub fn create_opaque() -> RustOpaque<HideData> {
    // [`HideData`] has private fields.
    RustOpaque::new(HideData::new())
}

pub fn run_opaque(opaque: RustOpaque<HideData>) -> String {
    // RustOpaque impl Deref trait.
    opaque.hide_data()
}

pub fn run_opaque_with_delay(opaque: RustOpaque<HideData>) -> String {
    sleep(Duration::from_millis(1000));
    opaque.hide_data()
}

Dart:

// (Arc counter = 1) Dart has full ownership.
var opaque = await api.createOpaque();

// (Arc counter = 2) increases immediately. 
// Dart and Rust share the opaque type.
// Safely because opaque type has `Send` `Sync` Rust trait.
var unawait_task = api.runOpaqueWithDelay(opaque: opaque);

// (Arc counter = 1) Rust has full ownership.
// Dart stops owning the opaque type. 
// Trying to use an opaque type will throw StateError('Use after dispose.')
opaque.dispose();

// Successfully completed.
//
// Rust:
// `executes run_opaque_with_delay.`
// after complete (Arc counter = 0) 
// opaque type is dropped (deallocated)
await unawait_task;

Case 4: Multi call.

Rust api.rs:

pub use crate::data::HideData; // `pub` for bridge_generated.rs

pub fn create_opaque() -> RustOpaque<HideData> {
    // [`HideData`] has private fields.
    RustOpaque::new(HideData::new())
}

pub fn run_opaque(opaque: RustOpaque<HideData>) -> String {
    // RustOpaque impl Deref trait.
    opaque.hide_data()
}

Dart: (test:'Double Call' frb_example/pure_dart/dart/lib/main.dart)


// (Arc counter = 1) Dart has full ownership.
var opaque = await api.createOpaque();

// (Arc counter = 2) increases immediately.
// (Arc counter = 1) after complete
String hideData1 = await api.runOpaque(opaque: opaque);

// (Arc counter = 2) increases immediately.
// (Arc counter = 1) after complete
String hideData2 = await api.runOpaque(opaque: opaque);

// (Arc counter = 0) opaque type is dropped (deallocated)
opaque.dispose();

Case 5: Double call with dispose before complete.

Rust api.rs:

pub use crate::data::HideData; // `pub` for bridge_generated.rs

pub fn create_opaque() -> RustOpaque<HideData> {
    // [`HideData`] has private fields.
    RustOpaque::new(HideData::new())
}

pub fn run_opaque(opaque: RustOpaque<HideData>) -> String {
    // RustOpaque impl Deref trait.
    opaque.hide_data()
}

Dart:


// (Arc counter = 1) Dart has full ownership.
var opaque = await api.createOpaque();

// (Arc counter = 2) increases immediately. 
var unawait_task1 = api.runOpaque(opaque); *1

// (Arc counter = 3) increases immediately. 
var unawait_task2 = api.runOpaque(opaque); *2

// (Arc counter = 2) Rust has full ownership
opaque.dispose();

// (*1 is complete) (Arc counter = 1)
//
// Rust:
//
//`executes rust_call_example and counter decreases.`

// (*2 is complete) (Arc counter = 0) 
// opaque type is dropped (deallocated)
//
// Rust:
//
//`executes rust_call_example and drop opaque type.`

Case 6: Dispose was not called (native).

Rust api.rs:

pub use crate::data::HideData; // `pub` for bridge_generated.rs

pub fn create_opaque() -> RustOpaque<HideData> {
    // [`HideData`] has private fields.
    RustOpaque::new(HideData::new())
}

pub fn run_opaque(opaque: RustOpaque<HideData>) -> String {
    // RustOpaque impl Deref trait.
    opaque.hide_data()
}

Dart:


// (Arc counter = 1) Dart has full ownership.
var opaque = await api.createOpaque();

// (Arc counter = 2) increases immediately. 
String hideData = await api.runOpaque(opaque);

// (Arc counter = 1)
//
// Rust:
//
// `executes rust_call_example and counter decreases.`

// memory of opaque types is not monitoring by dart and can accumulate.
// (Arc counter = 0) 
// opaque type is dropped (deallocated)
// 
// Dart:
//
// `the finalizer is guaranteed to be called before the program terminates.`

Case 7: Dispose was not called (web).

Rust api.rs:

pub use crate::data::HideData; // `pub` for bridge_generated.rs

pub fn create_opaque() -> RustOpaque<HideData> {
    // [`HideData`] has private fields.
    RustOpaque::new(HideData::new())
}

pub fn run_opaque(opaque: RustOpaque<HideData>) -> String {
    // RustOpaque impl Deref trait.
    opaque.hide_data()
}

Dart:


// (Arc counter = 1) Dart has full ownership.
var opaque = await api.createOpaque(); 

// (Arc counter = 2) increases immediately. 
String hideData = await api.rustOpaque(opaque);

// (Arc counter = 1)
//
// Rust:
//
//`executes rust_call_example and counter decreases.`

// memory of opaque types is not monitoring by Dart and can accumulate.
// (Arc count can be 0 or 1) don't count on automatic clearing.
//
// Dart:
//
//`the finalizer is NOT guaranteed to be called before the program terminates.`

Case 8: Unwrap.

Rust api.rs:

pub use crate::data::HideData; // `pub` for bridge_generated.rs

pub fn unwrap_rust_opaque(opaque: Opaque<HideData>) -> Result<String> {
    let res: Result<HideData, Opaque<HideData>> = opaque.try_unwrap();
    let data: HideData = res.map_err(|_| anyhow::anyhow!("opaque type is shared"))?;
    Ok(data.hide_data())
}

Dart:


// (Arc counter = 1) Dart has full ownership.
var opaque = await api.createOpaque();

// When passed as an argument, dart will relinquish ownership.
opaque.move = true;

// (Arc counter = 1) Rust has full ownership.
// On the Rust side, the Arc unwrap safely 
// as the Rust has full ownership of the opaque type. 
// Memory is cleared in the usual way Rust.
await api.unwrapRustOpaque(opaque: data);

Dart opaque type safety

Ownership and GC

From the moment the opaque type is passed, Rust will own a persistent representation of the dart object (Dart_PersistentHandle or JsValue). This means that while Rust owns DartOpaque the object will not be cleared by GC. Also flutter_rust_bridge provides a thread-safe drop for DartOpaque: Rust delegates the drop to the Dart side using the Dart port.

Dispose flutter_rust_bridge Api before all DartOpaques are cleaned.

If there is an attempt to delegate the drop to the Dart side after the drop port (Api.dispose()) has been closed. flutter_rust_bridge will issue a warning in the logs, the memory behind the object will leak.

Example

Case 1: loopBack.

Rust api.rs:

pub fn loop_back(opaque: DartOpaque) -> DartOpaque {
    opaque
}

Dart:


String f() => 'Test_String';

var fn = await api.loopBack(opaque: f) as String Function();

expect(fn(), 'Test_String');

Case 2: drop.

Rust api.rs:

pub fn sync_accept_dart_opaque(opaque: DartOpaque) -> SyncReturn<String> {
    drop(opaque);
    SyncReturn("test".to_owned())
}

pub fn async_accept_dart_opaque(opaque: DartOpaque) {
    drop(opaque);
}

Dart:

// the closure is safely removed on the Rust side (on another thread)
await api.asyncAcceptDartOpaque(opaque: () => 'Test_String');
// the closure is safely removed on the Rust side (on current thread)
api.syncAcceptDartOpaque(opaque: () => 'Test_String');

Case 3: Unwrap.

Rust api.rs:

/// [DartWrapObject] can be safely retrieved on a dart thread.
pub fn unwrap_dart_opaque(opaque: DartOpaque) -> SyncReturn<String> {
    let handle = opaque.try_unwrap().unwrap();
    SyncReturn("Test".to_owned())
}

/// [DartWrapObject] cannot be obtained 
/// on a thread other than the thread it was created on.
pub fn panic_unwrap_dart_opaque(opaque: DartOpaque) {
    let handle = opaque.try_unwrap().unwrap();
}

Dart:


// Rust gets (drop safely) wrap Dart_PersistentHandler (or JsValue).
api.unwrapDartOpaque(opaque: () => 'Test_String');

// We get an error because DartOpaque was passed to another thread.
await expectLater(() => api.panicUnwrapDartOpaque(opaque: () => 'Test_String'), throwsA(isA<FfiException>()));

Appendix

Remark: Some docs here seem to be outdated. Refer to ci.yaml, main doc, justfile, etc to see an up-to-date version. This appendix will be overhauled.

Releasing a new version

Usually this is done by the owner (@fzyzcjy), so you do not need to do the following. If you need to release a new version, the following steps are needed. Bump several versions, change the version number in changelog, and use cargo check to automatically update the examples' dependency versions:

just release

Sample commands to run code generator

Just copied from CI codegen.yml.

(cd frb_codegen && cargo run --package flutter_rust_bridge_codegen --bin flutter_rust_bridge_codegen -- --rust-input ../frb_example/pure_dart/rust/src/api.rs --dart-output ../frb_example/pure_dart/dart/lib/bridge_generated.dart --dart-format-line-length 120 && cargo run --package flutter_rust_bridge_codegen --bin flutter_rust_bridge_codegen -- --rust-input ../frb_example/with_flutter/rust/src/api.rs --dart-output ../frb_example/with_flutter/lib/bridge_generated.dart --c-output ../frb_example/with_flutter/ios/Runner/bridge_generated.h --dart-format-line-length 120)

Format and lint everything

(cd frb_codegen && cargo fmt --all); (cd frb_rust && cargo fmt --all); (cd frb_macros && cargo fmt --all); (cd frb_example/pure_dart/rust && cargo fmt --all); (cd frb_example/with_flutter/rust && cargo fmt --all);
(cd frb_codegen && cargo clippy); (cd frb_rust && cargo clippy); (cd frb_macros && cargo clippy); (cd frb_example/pure_dart/rust && cargo clippy); (cd frb_example/with_flutter/rust && cargo clippy);                                                                                                                                          
(cd frb_dart && dart format . --line-length 80); (cd frb_example/pure_dart/dart && dart format . --line-length 120); (cd frb_example/with_flutter && dart format . --line-length 120);
(cd frb_dart && dart analyze --fatal-infos); (cd frb_example/pure_dart/dart && dart analyze --fatal-infos); (cd frb_example/with_flutter && dart analyze --fatal-infos);

Upgrade dependency in your dependent project

flutter pub upgrade flutter_rust_bridge
cargo update -p flutter_rust_bridge

Unit tests in dart

To run flutter or dart test with the bridge you need to load the library on your own development machine (Windows/MacOS/Linux/CI). For that use loadLibForFlutter or loadLibForDart, for example:

BridgeImpl initializeExternalLibrary(String path) => BridgeImpl(loadLibForDart(path));

Note however, that you need to build the library for your IDE's Operating System. cargo build should normally handle that.

Do not change the target to your OS only, as otherwise you will not be able to build for your target platform anymore.

Example setup (verified on MacOS)

project
|- lib
|- test
|-- ffi.test.dart
|-- bridge_test.dart
|- rust
|-- src
|--- api.rs
|-- target

Where ffi.test.dart has the following content:

import 'package:basis_hybrid/bridge_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge.dart';

BridgeImpl initializeExternalLibrary(String path) {
  return BridgeImpl(
    loadLibForFlutter(path)
  );
}

and then bridge_test.dart has the following content:

import 'package:basis_hybrid/bridge_definitions.dart';
import 'package:flutter_test/flutter_test.dart';

import 'ffi.test.dart';

Future<void> main() async {
  final api = initializeExternalLibrary('rust/target/debug/librustbridge.dylib');
  await api.init(sqlPath: 'test.db', kvPath: 'test.kv');

  test('User save/load', () async {
     await api.saveUser();
     var user = await api.readUser();
     expect(user, isNotNull);
   });
}

Ensure that you have your IDE's system target installed (rustup) according to Creating a new project, after running cargo build you should've a library in rust/target/debug/

Tutorial: Pure Dart

Remark: The valgrind_test section of the CI workflow can also be useful, if you want details of each command and want to see Valgrind configuration.

Unlike the previous tutorial, this one integrates Rust with pure Dart instead of Flutter.

Get example code

Please install Dart, install Rust, and have some familiarity with them. Then run git clone https://github.com/fzyzcjy/flutter_rust_bridge, and my example is in frb_example/pure_dart.

(Optional) Manually run code generator

Remark: Bridge is automatically generated upon running cargo build using build-script in build.rs file, so this step is optional. Even if you do it, you should not see anything changed.

Install it: cargo install flutter_rust_bridge_codegen.

Run it: flutter_rust_bridge_codegen --rust-input frb_example/pure_dart/rust/src/api.rs --dart-output frb_example/pure_dart/dart/lib/bridge_generated.dart (See CI workflow as a reference.) (For Windows, you may need \\ instead of / for paths.)

Run "Dart+Rust" app

You may run frb_example/pure_dart/dart/lib/main.dart as a normal Dart program, except that you should provide the dynamic linked library of the Rust code (for simplicity, here I only demonstrate the approach for dynamic linked library, but you can for sure use other methods). The detailed steps are as follows.

Run cargo build in frb_example/pure_dart/rust to build the Rust code into a .so file. Then run dart frb_example/pure_dart/dart/lib/main.dart frb_example/pure_dart/rust/target/debug/libflutter_rust_bridge_example_pure_dart.so to run the Dart program with Rust .so file. (If you have problems, see "Troubleshooting" section.) (If on MacOS, Rust may indeed generate .dylib, so change the last command to use ...dylib instead of ...so,)

P.S. You will only see some tests passing - no fancy UI or functionality in this example.

Safety concerns

This library has CI that runs Valgrind automatically on the setup that a Dart program calls a Rust program using this package, so memory problems should be found by Valgrind. (Notice that, even when running a simple hello-world Dart program, Valgrind will report hundreds of errors. See this Dart lang issue for more details. Therefore, I both look at "definitely lost" in Valgrind, and manually search things related to this library - if all reported errors are unrelated to this library then we are safe.)

In addition, Flutter integration tests are also done in CI. This ensures a real Flutter application using this library does not suffer from problems.

Most of the code are written in safe Rust. The unsafe code mainly comes from support::box_from_leak_ptr and support::vec_from_leak_ptr. They are used for pointers and arrays, and I follow the high-upvoted answers and official doc when writing down that few lines of code.

I use this library heavily in my own Flutter project (yplusplus, or why++). That app is in production and it works quite well. If I observe any problems, I will fix it in this library.

The CI also runs the run_codegen workflow, which ensure that the code generator can compile and generate desired results. Lastly, the CI also runs formatters and linters (fmt, clippy, dart analyze, dart format), and linters can also catch some common problems.

Troubleshooting

The generated store_dart_post_cobject() has the wrong signature / 'stdarg.h' file not found in Linux / stdbool.h / ...

Try to run code generator with working directory at /, or set the environment variable:

export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include"

as described in ffigen #257, or add include path as is described in #108. This is a problem with Rust's builtin Command. See also: #472 & #494.

Issue with store_dart_post_cobject

If calling rust function gives the error below, please consider running cargo build again. This can happen when the generated rs file is not included when building is being done.

[ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: Invalid argument(s): Failed to lookup symbol 'store_dart_post_cobject': target/debug/libadder.so: undefined symbol: store_dart_post_cobject

Error running cargo ndk: ld: error: unable to find library -lgcc

Downgrade Android NDK to version 22. This is an ongoing issue with cargo-ndk, a library unrelated to flutter_rust_bridge but solely used to build the examples, when using Android NDK version 23. (See #149)

Fail to run flutter_rust_bridge_codegen on MacOS, "Please supply one or more path/to/llvm..."

If you are running macOS, you will need to specify a path to your llvm:

flutter_rust_bridge_codegen --rust-input path/to/your/api.rs --dart-output path/to/file/being/bridge_generated.dart --llvm-path /usr/local/homebrew/opt/llvm/

You can install llvm using brew install llvm and it will be installed at /usr/local/homebrew/opt/llvm/ by default.

Freezed file is sometimes not generated when it should be

If your .freezed.dart or .g.dart seems outdated, ensure you have run the build_runner.

Related: https://github.com/fzyzcjy/flutter_rust_bridge/issues/330

Can't create typedef from non-function type.

Ensure min sdk version of Flutter pubspec.yaml is at least 2.17.0 to let ffigen happy.

https://github.com/fzyzcjy/flutter_rust_bridge/issues/334

Imported from both bridge_definitions.dart and bridge_generated.io.dart

If you use a Rust type with Kind in it's name it may conflict with some generated types which can cause a duplicate import error. The workaround is to avoid using Kind as a suffix for a type name in Rust. See issue #757 for more details.

Error on iOS TestFlight only (store_dart_post_cobject)

You may have an iOS app that works fine in Debug and Release modes locally but when deployed to TestFlight an error occurs trying to locate the store_dart_post_cobject - this is because the nested XCode project for the native bindings maybe be stripping symbols from the linked product.

Select the scheme (eg: Product > Scheme > native-staticlib) and go to Build Settings then under the Deployment section change Strip Linked Product to No; you may also need to change Strip Style to Debugging Symbols.

Generated code is so long

Indeed all generated code are necessary (if you find something that can be simplified, file an issue). Moreover, other code generation tools also generate long code - for example, when using Google protobuf, a very popular serialization library, I see >10k lines of Java code generated for a quite simple source proto file.

Why need Dart 2.17.0

Dart SDK >=2.15.0 is supported by this library, but by the latest version of the ffigen tool requires >=2.17.0. Therefore, write sdk: ">=2.17.0 <3.0.0" in the environment section of pubspec.yaml. If you do not want that, consider installing a older version of the ffigen tool.

Why doesn't flutter_rust_bridge_serve work on Firefox?

This is a known issue stemming from Firefox's stricter rules regarding cross-origin requests. Use Chromium for testing, and check out this guide on enabling crossOriginIsolated for your production servers.

"android context was not initialized", or ndk_context initialization.

Related issue: #1323.

On android, when attempting to use crates that interact with the JavaVM through the JNI (like oboe-rs via cpal), you may get panics that typically have this message:

[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: FfiException(PANIC_ERROR, android context was not initialized, null)

This is due to a interesting quirk of Rust NDK interaction, where the ndk_context crate does not have it's JVM and Android context initialized. Typically, in a normal application, the Android JVM would System.loadLibrary() the library through the Activity inside the JVM. It looks for symbols related to the JNI and executes them in accordance with the JNI standard. This would initialize the ndk_context normally via JNI_OnLoad. However, using the DartVM this step is skipped while loading the library, as the DartVM is not the JVM. So, the Android specific variables are not initialized, and therefore you cannot interact with the system via the Java interface.

MainActivity.kt

Add these lines to your FlutterActivity subclass:

package com.example.frontend

import io.flutter.embedding.android.FlutterActivity

// https://github.com/dart-lang/sdk/issues/46027
class MainActivity : FlutterActivity() {
    // this `init` block, where "foo" is the name of your library
    // ex: if it's libfoo.so, then use "foo"
    init {
        System.loadLibrary("foo")
    }
}

This handles loading the library before Dart does, and also executes the JNI related initialization.

Rust

Cargo.toml

[target.'cfg(target_os = "android")'.dependencies]
jni = "0.21"
ndk-context = "0.1"

lib.rs

#![allow(unused)]
fn main() {
#[cfg(target_os = "android")]
#[no_mangle]
pub extern "C" fn JNI_OnLoad(vm: jni::JavaVM, res: *mut std::os::raw::c_void) -> jni::sys::jint {
    use std::ffi::c_void;

    let vm = vm.get_java_vm_pointer() as *mut c_void;
    unsafe {
        ndk_context::initialize_android_context(vm, res);
    }
    jni::JNIVersion::V6.into()
}
}

This is the bit of JNI glue that allows for ndk_context to be initialized.

"Could not resolve symbol __cxa_pure_virtual", or libc++_shared issues.

At the time of writing this, linking with libc++_static or not linking at all may lead to symbol resolution errors when launching the flutter application, after loading your dynamic library. Adding a fix is quite easy, create a build.rs script in the root of your Rust code:

build.rs

fn main() {
    #[cfg(target_os = "android")]
    println!("cargo:rustc-link-lib=c++_shared");
}

Then, in each jniLibs architecture directory, put the corresponding libc++_shared.so from the Android NDK. libc++_shared.so is typically located in $ANDROID_NDK/toolchains/llvm/prebuilt/. You will have to search for it, as it's different for each operating system.

  • arm-linux-androideabi -> armeabi-v7a
  • aarch64-linux-android -> arm64-v8a
  • i686-linux-android -> x86
  • x86_64-linux-android -> x86_64

Issues on Web?

Check out Limitations on WASM for some common problems and solutions to adapt existing code to WASM.

Other problems?

Don't hesitate to open an issue! I usually reply within minutes or hours (except when sleeping, of course).

Command line arguments

Simply add --help to see full documentation. The following is a snapshot when running the command with --help:

$ flutter_rust_bridge_codegen --help
Usage: flutter_rust_bridge_codegen [OPTIONS] --rust-input <RUST_INPUT>... --dart-output <DART_OUTPUT>...
       flutter_rust_bridge_codegen [CONFIG_FILE]

Arguments:
  [CONFIG_FILE]
          Path to a YAML config file.

          If present, other options and flags will be ignored. Accepts the same options as the CLI, but uses snake_case keys.

Options:
  -r, --rust-input <RUST_INPUT>...
          Path of input Rust code

  -d, --dart-output <DART_OUTPUT>...
          Path of output generated Dart code

      --dart-decl-output <DART_DECL_OUTPUT>
          If provided, generated Dart declaration code to this separate file

  -c, --c-output <C_OUTPUT>
          Output path (including file name) of generated C header, each field corresponding to that of --rust-input

  -e, --extra-c-output-path <EXTRA_C_OUTPUT_PATH>
          Extra output path (excluding file name) of generated C header

      --rust-crate-dir <RUST_CRATE_DIR>...
          Crate directory for your Rust project

      --rust-output <RUST_OUTPUT>...
          Output path of generated Rust code

      --class-name <CLASS_NAME>...
          Generated class name

      --dart-format-line-length <DART_FORMAT_LINE_LENGTH>
          Line length for Dart formatting

          [default: 80]

      --dart-enums-style
          The generated Dart enums will have their variant names camelCased

      --skip-add-mod-to-lib
          Skip automatically adding `mod bridge_generated;` to `lib.rs`

      --llvm-path <LLVM_PATH>...
          Path to the installed LLVM

      --llvm-compiler-opts <LLVM_COMPILER_OPTS>
          LLVM compiler opts

      --dart-root <DART_ROOT>...
          Path to root of Dart project, otherwise inferred from --dart-output

      --no-build-runner
          Skip running build_runner even when codegen-required code is detected

      --no-use-bridge-in-method
          No use bridge in Model

      --extra-headers <EXTRA_HEADERS>
          extra_headers is used to add dependencies header

          Note that when no_use_bridge_in_method=true and extra_headers is not set, the default is `import 'ffi.io.dart' if (dart.library.js_interop) 'ffi.web.dart'`.

  -v, --verbose
          Show debug messages

      --wasm
          Enable WASM module generation. Requires: --dart-decl-output

      --inline-rust
          Inline declaration of Rust bridge modules

      --skip-deps-check
          Skip dependencies check

      --dump [<DUMP>...]
          A list of data to be dumped. If specified without a value, defaults to all

          [possible values: config, ir]

      --no-dart3
          Disable language features introduced in Dart 3

      --keep-going
          If set, the program will delay error reporting until all codegen operations have completed

  -h, --help
          Print help (see a summary with '-h')

  -V, --version
          Print version

flutter_rust_bridge_serve

$ dart run flutter_rust_bridge_serve --help
flutter_rust_bridge_serve 1.82.6
Develop Rust WASM modules with cross-origin isolation.

USAGE:
	flutter_rust_bridge_serve [OPTIONS] [..REST]
	flutter_rust_bridge_serve --dart-input <ENTRY> --root <ROOT> [OPTIONS] [..REST]

OPTIONS:
-p, --port=<PORT>             HTTP port to listen to
                              (defaults to "8080")
-r, --root=<ROOT>             Root of the Flutter/Dart output
-c, --crate=<CRATE>           Directory of the crate
                              (defaults to "native")
-d, --dart-input=<ENTRY>      Run "dart compile" with the specified input instead of "flutter build"
-w, --wasm-output=<PKG>       WASM output path
-v, --[no-]verbose            Display more verbose information
    --[no-]relax-coep         Set COEP to credentialless
                              Defaults to true for Flutter
    --[no-]open               Open the webpage in a browser
                              (defaults to on)
    --run-tests               Run tests in headless Chromium
    --release                 Compile in release mode
    --[no-]weak-refs          Enable the weak references proposal
                              Requires wasm-bindgen in path
    --[no-]reference-types    Enable the reference types proposal
                              Requires wasm-bindgen in path
-h, --help                    Print this help message
    --[no-]build              Whether to build the library.
                              (defaults to on)
    --features                A comma-separated list of features to pass to `cargo build`.
    --no-default-features     Whether to disable all features, useful with --features

Configuration files

You can run flutter_rust_bridge_codegen with no arguments, provided any of these files exists in the working directory (in order of priority):

  • .flutter_rust_bridge.yml
  • .flutter_rust_bridge.yaml
  • .flutter_rust_bridge.json

The codegen will try to read a configuration from these files. Otherwise, you can pass to the CLI any YAML file that contains the config. The same arguments from the CLI are accepted, but they will be in snake_case.

# in .flutter_rust_bridge.yml
rust_input:
  - path/to/api.rs
dart_output:
  - path/to/bridge_generated.dart

Similarly, if you're calling flutter_rust_bridge_codegen from the root of your Dart project, you can also fill in your config under the flutter_rust_bridge entry in pubspec.yaml:

# put this somewhere in your pubspec.yaml
flutter_rust_bridge:
  rust_input:
    - path/to/api.rs
  dart_output:
    - lib/src/bridge_generated.dart

Set up Flutter/Dart+Rust support from scratch

This documentation is archived, though technically still correct. Have a look at integrating with existing projects chapters for a more detailed demonstration.

I suggest that you can start with the Flutter example first, and modify it to satisfy your needs. It can serve as a template for new projects. It is run against CI so we are sure it works.

Indeed, this library is nothing but a code generator that helps your Flutter/Dart functions call Rust functions. Therefore, "how to create a Flutter app that can run Rust code" is actually out of the scope of this library, and there are already several tutorials on the Internet.

However, I can sketch the outline of what to do if you want to set up a new Flutter+Rust project as follows.

Step 1

Create a new Flutter project (or use an existing one). The Dart SDK should be >=2.17.0 if you want to use the latest ffigen tool.

Step 2

Create a new Rust project, say, at directory rust under the Flutter project.

Step 3

Edit Cargo.toml and add:

[lib]
name = "flutter_rust_bridge_example" # whatever you like
# notice this type. `cdylib` for android, and `staticlib` for iOS. I write down a script to change it before build.
+ crate-type = ["cdylib"]

Step 4

Follow the standard steps of "how iOS uses static libraries".

  1. In XCode, edit Strip Style in Build Settings to Debugging Symbols.
  2. Add your lib{crate}.a to Link Binary With Libraries in Build Phases.
  3. Add binding.h to Copy Bundle Resources.
  4. Add #import "binding.h" to Runner-Bridging-Header.
  5. Last but not least, add a never-to-be-executed dummy function in Swift that calls any of the generated C bindings. This lib has already generated a dummy method for you, so you simply need to add print("dummy_value=\(dummy_method_to_enforce_bundling())"); to swift file's override func application(...) {}, and this will prevent symbol stripping - especially in the release build for iOS (i.e. when building ipa file or releasing to App Store). Notice that, we have to use that dummy_method_to_enforce_bundling(), otherwise the symbols will not maintain in the release build, and Flutter will complain it cannot find the symbols.

Step 5

Lastly, in order to build the Rust library automatically when you are building Flutter, follow this tutorial.

Building a WASM binary manually

Here are the complete commands for building a WASM binary with this library:

export RUSTUP_TOOLCHAIN=nightly
export RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals"
wasm-pack build \
    -t no-modules \
    -d <WASM_OUTPUT_PATH> \
    --no-typescript -- \
    -Z build-std=std,panic_abort

Continue reading for more details.


flutter_rust_bridge_codegen expects a certain setup that is modeled after the wasm_bindgen raytracing example and by extension consumes the wasm_bindgen library and its ecosystem. The requirements are:

  • The standard library being built with the panic_abort feature
  • The library and standard library being built with the target features atomics, bulk_memory and mutable_globals
  • wasm-pack called with -t no-modules (to be relaxed in the future)

Note that these features also represent a hard requirement on your users' browser versions.

Furthermore, this library does not support JavaScript runtimes as of writing.

WASM_OUTPUT_PATH refers to the output directory of the WASM module. If running Flutter, this is usually web/pkg.

Setting up the web server

Once you have built your binary and are ready to deploy, you will also need to configure your web server to respond with these two headers:

Here is a sample web server that accomplishes this task (excerpt from flutter_rust_bridge_serve):

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_static/shelf_static.dart';

void main() async {
    final root = "/* directory containing index.html */";
    final staticFilesHandler = createStaticHandler(root, defaultDocument: 'index.html');
    final handler = const Pipeline().addMiddleware((handler) {
        return (req) async {
            final res = await handler(req);
            return res.change(headers: const {
                'Cross-Origin-Opener-Policy': 'same-origin',
                'Cross-Origin-Embedder-Policy': 'require-corp',
            });
        };
    }).addHandler(staticFilesHandler);
    await serve(handler, 'localhost', 8080);
}
1

When running Flutter Web, you may encounter issues with downloading Flutter support scripts which have not been marked as crossorigin="anonymous" and therefore cannot be loaded. For local testing, you can specify credentialless instead.

Limitations of WASM

  • Safari cannot spawn nested Workers. A workaround is to build two variants of the library, one with multithreading and one without, and serve Safari users the single-threaded variant. For a more general solution, check out wasm-feature-detect or nested-worker.
  • std::thread::spawn is unimplemented and replacements (e.g. wasm_thread) are not fully supported. If you must use them, consider wrapping your return type in a SyncReturn<_> to avoid the internal thread pool interfering with your threads. This library includes a spawn! macro which spawns a new thread using the internal thread pool.
  • When a Rust thread panics, it aborts and throws a JavaScript RuntimeError that cannot be caught by name in Dart. This is expected to change as the exception handling story for WASM improves, but a rule of thumb is to replace .unwrap with .expect or Errs.
  • As a consequence, panic::catch_unwind does not work on the Web. As of writing, the implementation to catch these errors resides within the bodies of the workers, i.e. it is not straightforward enough to generalize for other use-cases.
  • Int64List and Uint64List throws when used on Web platforms. They are left intentionally unimplemented by the Dart language developers, perhaps due to the differences between int and BigInt. This library provides a barebones pure Dart shim whose behavior may differ from the specifications, so please create an issue/PR if you encounter any significant digression.
  • Int64List and Uint64List arithmetics clamp on native platforms, but wrap on the Web. If your use-case requires precision around large integer values, please be mindful of these platform-specific differences.
  • Support for the various components of WASM is not universal among browsers. Here is a (non-exhaustive) list of trackers for how widely available some of the features are across browsers:
  • JavaScript runtimes (Node.js, Deno, etc.) support is not yet implemented.

Articles

This chapter contains some articles related to flutter_rust_bridge.

Async in Rust

Author: @AlienKevin

This library does not yet support returning a Future type from Rust and this has to do with the difficulty of uniting the various approaches to async in Rust. The Rust Book summarized the current state of async support succinctly:

The most fundamental traits, types and functions, such as the Future trait are provided by the standard library. The async/await syntax is supported directly by the Rust compiler.

Many utility types, macros and functions are provided by the futures crate. They can be used in any async Rust application.

Execution of async code, IO and task spawning are provided by "async runtimes", such as Tokio and async-std. Most async applications, and some async crates, depend on a specific runtime.

While the futures crate provides an executor called futures::executor::block_on, libraries that use Tokio runtime cannot use this executor. According to Rust-lang community wiki, crates like Tokio that provide both a runtime and IO abstractions often have their IO depend on the runtime. This can make it difficult to write runtime-agnostic code. First, we demonstrate a common use case of async programming in Rust by attempting to fetch the content of a file from the internet using the popular HTTP Client Reqwest:

use anyhow;

async fn get() -> anyhow::Result<String> {
    let url = "https://link/to/file/download";
    let data = reqwest::get(url).await?.text().await?;
    Ok(data)
}

When you try to generate bindings for the get function, the generated code will contain errors because this library does not support returning Future from Rust.

Mismatched runtime

The next logic thing to try would be to convert the asynchronous code to synchronous by directly blocking the current thread and execute the code. For our first attempt, we wrap futures::executor::block_on around an async block containing reqwest calls.

use anyhow;
use futures::executor::block_on;

fn get() -> anyhow::Result<String> {
    block_on(async {
        let url = "https://link/to/file/download";
        let data = reqwest::get(url).await?.text().await?;
        Ok(data)
    })
}

Since Reqwest uses the Tokio runtime instead of the futures runtime, our code panicked with the error "there is no reactor running, must be called from the context of a Tokio 1.x runtime". To fix this error, we have two ways to execute async codes using the Tokio runtime. Approach 1 is the simplest and uses the convenient tokio::main macro to turn an async function to a synchronous one. Approach 2 requires you to explicitly create a new Tokio runtime and use its block_on function to run the future to completion.

Approach 1 (macro)

use anyhow;

#[tokio::main(flavor = "current_thread")]
async fn get() -> anyhow::Result<String> {
    let url = "https://link/to/file/download";
    let data = reqwest::get(url).await?.text().await?;
    Ok(data)
}

It has the following dependencies:

[dependencies]
futures = "0.3"
reqwest = "0.11.6"
tokio = { version = "1.14.0", features = ["rt", "macros"] }
anyhow = { version = "1.0.49" }

Approach 2 (runtime)

use anyhow;
use tokio::runtime::Runtime;

fn get() -> anyhow::Result<String> {
    let rt = Runtime::new().unwrap();
    rt.block_on(async {
        let url = "https://link/to/file/download";
        let data = reqwest::get(url).await?.text().await?;
        Ok(data)
    })
}

It has the following dependencies:

[dependencies]
futures = "0.3"
reqwest = "0.11.6"
tokio = { version = "1.14.0", features = ["rt-multi-thread"] }
anyhow = { version = "1.0.49" }

Plain futures

If you are using the plain futures crate without runtimes like Tokio, you should be safe to wrap the asynchronous code in an async block and use the futures::executor::block_on to run the future to completion:

use futures::executor::block_on;

async fn hello_world() -> String {
    "hello, world!".to_string()
}

fn get() -> String {
    block_on(async {
        hello_world().await
    })
}

fn main() {
    println!("{}", get()); // prints "hello, world!"
}

Avoid async

Lastly, you can avoid async code all together by using synchronously/blocking version of the functions if they are available. In Reqwest, there's a module called reqwest::blocking designed specifically for this purpose. So you can achieve the same thing above without using async.

use anyhow;
use reqwest;

fn get() -> anyhow::Result<String> {
    let url = "https://link/to/file/download";
    let data = reqwest::blocking::get(url)?.text()?;
    Ok(data)
}

It has the following dependencies:

[dependencies]
futures = "0.3"
reqwest = { version = "0.11.6", features = ["blocking"] }
anyhow = { version = "1.0.49" }

Generating multiple files

Author: @dbsxdbsx

This article describes some thoughts and implementations about the feature of generating multiple files.

Before, like the pure_dart's api.rs, all APIs are exposed together in a single file(block). This is not bad when the whole project is simple. But it would become quite hard to maintain or develop, when the project becomes more and more complex, especially when it is a team project. Therefore, it is time to reconstruct code --- classify the exposed Api into proper blocks(files).

(Before going on reading, make sure that you are quite familiar with how to use template to generate code with flutter_rust_bridge. If not, take a look at the former chapters or the basic example again, please.)

Try to classify Api into different blocks(files)

Suppose, you only have two Api in api.rs originally, like this:

#![allow(unused_variables)]

pub fn simple_add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn simple_minus(a: i32, b: i32) -> i32 {
    a - b
}

Now you want to classify these 2 Api into 2 blocks for some reason-- say, you put the simple_add Api into file api_1.rs and the other into api_2.rs. And then make a little modification in lib.rs:

mod api_1;
mod api_2;

Ok, now the question is how to deal with them with flutter_rust_bridge? From the template justfile, we know code from a single API file called api_rs can be generated with a command like this:

gen:
    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated.dart" \
...

(For simplicity, only two necessary flags rust-input and dart-output here.)

Then, to generate code within 2 blocks(files), you may come out with an approach like this:

gen:
    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_1.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_1.dart" \

    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_2.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_2.dart" \
...

But here comes a problem, how to use them in dart? Like await API.simpleAdd(1,2) or await API.simpleMinus(1,2) as before? The point here is, to thoroughly decouple Api from different blocks (which is the main reason for using multiple blocks of API), flag class-name is needed. So the command should be modified like this:

gen:
    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_1.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_1.dart" \
        --class-name ApiClass1

    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_2.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_2.dart" \
        --class-name ApiClass2
...

(The class name ApiClass1 and ApiClass2 are chosen arbitrarily here.)

So now it seems to be perfect to generate code and using Api in Dart like ApiClass1.simpleAdd(1,2) or ApiClass2.simpleMinus(1,2).

But actually, the above command is still not enough to generate code correctly. Because multiple blocks need to be translated respectively through FFI. So on the rust side, instead of generating code to a single file bridge_generated.rs, now there are 2 files needed. But, what are the names of these 2 auto-generated rust files? Here, for less misunderstanding, flutter_rust_bridge decides to ask for another compulsory flag rust-output. So the command should be modified like this:

gen:
    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_1.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_1.dart" \
        --class-name ApiClass1 \
        --rust-output generated_api_1

    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_2.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_2.dart" \
        --class-name ApiClass2 \
        --rust-output generated_api_2
...

(Still, the rust output name generated_api_1 and generated_api_2 are chosen arbitrarily here.)

That is, flutter_rust_bridge asks you to manually define the generated rust file names, feel free to choose any name you like.

Some issues with separate commands

Based on the last commands we come up with, everything seems to be fine --- the code generated, you can use them in Dart, and the whole project is compilable. And you would also notice some changes in lib.rs:

mod api_1;
mod api_2;
mod generated_api_1; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */
mod generated_api_2; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */

But actually, it is not good enough.

issue from explicit Api conflict

Let's say one day, you decide to add another API, say simpleDivide. But when you compile the whole project, the Dart compiler just complains "The symbol simpleDivide has already been defined ...". Then you check whether this simpleDivide is defined duplicated. Finally, you find that it's already defined in another block. This situation occurs quite a lot, when the other block is in the charge of someone else, especially in a big project. It is easy to see that the whole routine is a little inefficient since you don't realize the Api conflict until doing compiling when you've probably coded a lot with this "new defined" Api --- and the more time compiling takes, the more inefficient.

issue from implicit Api conflict

And what makes the Api conflict issue more catastrophic? Say you define another Api with parameter String in api_1.rs:

pub fn test_string_1(s1: String) {
    println!("test implicit parameter conflicts {}", s1);
}

And then you put another Api with parameter String in api_2.rs:

pub fn test_string_2(s2: String) {
    println!("test implicit parameter conflicts {}", s2);
}

These 2 Apis don't violate the uniqueness required by FFI. They should be compilable with no error. But the truth is no! Why? Because for the String parameter, flutter_rust_bridge would automatically generate API like this:

#[no_mangle]
pub extern "C" fn new_uint_8_list(len: i32) -> *mut wire_uint_8_list

which is used to let rust code easily cooperate with Dart through FFI. So if there are 2 APIs both taking String as parameters over blocks, you should notice a similar panic like "the symbol new_uint_8_list is already defined ..." during compiling(issue #511).

(Actually, since version 1.37, even with the separated commands with no Api defined, the whole project is still not compilable with error "symbol free_WireSyncRust2DartStruct is already defined... ", the symbol free_WireSyncRust2DartStruct is another implicitly Api generated by flutter_rust_bridge.)

So these kinds of explicit/implicit Api conflicts are annoying and frustrating. How to resolve it?

Theoretically, the conflict can be detected earlier during generating code, when flutter_rust_bridge knows every detail about API. But the key is that flutter_rust_bridge has to know all Api over all blocks before generating code. That is, with the separated command stated above, flutter_rust_bridge can't do the check for you in practice. Therefore, it is necessary to unite the separated commands into ONE command.

correct command for generating code with multiple blocks

Now comes the joined command to resolve the above issue:

gen:
    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_1.rs" "$REPO_DIR/native/src/api_2.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_1.dart" "$REPO_DIR/lib/bridge_generated_api_2.dart" \
        --class-name ApiClass1 ApiClass2 \
        --rust-output generated_api_1 generated_api_2
...

Here, with just 1 command, flutter_rust_bridge would smartly check if there are conflicts over all Api over all blocks, be it defined explicitly or implicitly.

That is, for the explicitly defined APIs like simple_add and simple_minus, if there are duplicated ones, flutter_rust_bridge would throw a panic like "thread 'main' panicked at 'symbol [simple_add] has already been defined'...", and you are responsible to fix it. And for the implicitly defined API like new_uint_8_list, since it is essential, flutter_rust_bridge would try to work around it by adding suffix starting from 0, like new_uint_8_list_0 and new_uint_8_list_1.

To sum up, there are 4 compulsory flags when you deal with multiple blocks. They are rust-input, dart-output, class-name and rust-output. Also, the number of fields following each flag should be consistent. You can try to cargo build with fewer flags or inconsistent fields to see what kind of panic would be popped up with the pure_dart_multi example when doing generation.

bizarre, weird but compilable command with the disorder

Flutter_rust_bridge doesn't do semantic correction over all flags. So, it is syntactically correct with the following generation command:

gen:
    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_orange.rs" "$REPO_DIR/native/src/api_apple.rs" \
        --dart-output "$REPO_DIR/lib/gen_api_apple.dart" "$REPO_DIR/lib/gen_api_orange.dart" \
        --class-name ApiClassOrange ApiClassApple \
        --rust-output generated_api_apple generated_api_orange

NOTE: the suffix apple and orange are quite disordered for each flag here on purpose. It is compilable and usable. But as you should know, it is not a good practice, semantically. It is all up to you to decide the field names for each flag, so be beware of it!