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):
-
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 ofx86_64
. -
-
Create 4 text files named
libgcc.a
in the four folders mentioned above with these contentsINPUT(-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.
Rust | Dart |
---|---|
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 |
DartOpaque | Arbitrary Dart types (opaque) |
Result::Err , panic | throw Exception |
Box<T> | T |
comments | same |
i8 , u8 , .., usize | int |
f32 , f64 | double |
bool | bool |
String | String |
() | void |
type A = B | type 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.
struct
s
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
, wheresomename
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;
}
enum
s
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]);
Option
s
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 dynamic
s
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
- For Result/Error, the
anyhow::Result
/anyhow::Error
is supported. It will be automatically converted to a Dart Exception. - For
panic
s, it will also be automatically captured and converted to Dart exceptions. - For error hierarchy, or arbitrary error types, it is also supported. For example, you can create your own
CustomError
(such as usingthiserror
), 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 withconst
.
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.
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 Iterator
s.
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 Future
s 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
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
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-type
s 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 benchmarksstaticlib
is required for iOScdylib
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 runbuild_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:
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.
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:
flutter_rust_bridge_codegen
, the core codegen for Rust-Dart glue codeffigen
, to generate Dart code from C headers- A working installation of LLVM, see Installing LLVM, used by
ffigen
- (Optional)
cargo-xcode
, if you want to generate Xcode projects for iOS and MacOS
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.
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 benchmarksstaticlib
is required for iOScdylib
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:
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.
Then, expand the Link Binary With Libraries phase, and add lib$crate_static.a for iOS, or $crate.dylib for MacOS.
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:
- In Xcode, go to Target Runner > Build Settings > Strip Style.
- 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 Flutterdart 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
- Dart/Flutter unit & integration testing
- 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
- You need a Mac to compile to macOS/iOS (at least locally)
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
- Previously, NDK version 21 (
- Most NDK versions should work nowadays due to fixes in
- Android NDK
- 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
- Need to run
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) & dependabotpackages/
where our Flutter/Dart packages will livelibrary_name/
the Dart-only (library) package using flutter_rust_bridge (FRB)native/
the Rust library used by Darttest/
unit tests for our Dart-only libraryexample/
an example project showing how to uselibrary_name
from Dart-onlytest/
(optional) tests for the example; can be used to ensure example continues to work in CI
flutter_library_name/
the Flutter (library) package wrapping aroundlibrary_name
for ease of useandroid/
,ios/
,linux/
,macos/
, &windows/
for platform-specific wrappers in order to bundle our library binaries with Flutter applicationstest/
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 useflutter_library_name
from within a Flutter applicationintegration_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 creationplatform-build/
the output (build) folder for all created Flutter binariesanalysis_options.yaml
to enable consistent Dart analysis in our Dart/Flutter librariesCargo.toml
so IDEs can find our Rust project underpackages/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
- 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. - 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 thepubspec.yaml
of the applicable package(s):
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
- In
/packages/flutter_library_name/lib/flutter_library_name.dart
, add the following near the top of the file, replacinglibrary_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
.
- Finally, we will need to write some code to be able to handle FFI in Flutter.
Modify the following as needed (replacing
library_name
andLibraryName
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());
- 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):
- 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
. - These binaries are uploaded to somewhere online; as mentioned previously, we will use GitHub releases in this guide (which is automated in ci).
- 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.
- 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
- iOS (
- 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
- If you know what an
- Windows:
/packages/flutter_library_name/windows/
- Linux:
/packages/flutter_library_name/linux/
- iOS:
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.txt
s:
- The minimum CMake version supported
- 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 - On linux, dynamic library names follow the form of
liblibrary_name.so
and on Windows, dynamic library names follow the form oflibrary_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
- Unit tests and code checks on pushes/PRs to
- 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
- Manual version/release creation with Melos through a workflow dispatch
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:
- Create new versions/releases with Melos
- 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:
--
-> callmelos 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
becomes1.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 inputsapi.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 offrb_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, andsrc/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:
- 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 affi.Pointer<wire_uint_8_list>
(becauseString
s usepub struct wire_uint_8_list { ptr: *mut u8, len: i32 }
under the hood). - Now we call the Dart version of
wire_func
, with low-level data likewire_uint_8_list
. We have used our codegen to create a Rustwire_func
function, and usecbindgen
to generate the corresponding C function, and useffigen
to get the corresponding Dart function. Here, we call the Dart version ofwire_func
. Since Dart FFI and Rust FFI is C-compatible, it seamlessly calls the Rust version ofwire_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. - 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). - 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-writtenfunc
Rust function inapi.rs
. - The user-written
fn func(str: String) -> String { ... }
is called, and we get a return value. - 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 wrapperallo-isolate
for it. We deliberately choose this, because this enables Dart code to be async instead of sync. - 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, sinceDart_PostCObject
does not provide a way to construct arbitrary structs(classes), we have to pass Rust structs as lists, and use thewire2api
to convert them to corresponding Dart classes. - 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.
Rust | Rust IO Wire | Dart IO Wire | Rust JS Wire | Dart JS Wire | Dart |
---|---|---|---|---|---|
i{8..32} | i{8..32} | int 1 | i{8..32} | int | int |
u{8..32} | u{8..32} | int 1 | u{8..32} | int | int |
i64 | i64 | int | BigInt | BigInt | int |
u64 | u64 | int | BigInt | BigInt | int |
usize | usize | int | usize | int | int |
bool | bool | bool | bool | bool | bool |
Vec<i{8..32}> | wire_int_{8..32}_list | wire_int_{8..32}_list | Box<[i{8..32}]> | Int{8..32}Array | Int{8..32}List |
Vec<u{8..32}> | wire_uint_{8..32}_list | wire_uint_{8..32}_list | Box<[u{8..32}]> | Uint{8..32}Array | Uint{8..32}List |
Vec<i64> | wire_int_64_list | wire_int_64_list | Box<[i64]> | BigInt64Array | Int64List 2 |
Vec<u64> | wire_uint_64_list | wire_uint_64_list | Box<[u64]> | BigUint64Array | Uint64List 2 |
String | wire_uint_8_list | wire_uint_8_list | String | String | String |
Vec<String> | wire_StringList | wire_StringList | Box<[String]> | List | List<String> |
Vec<T> | wire_list_t | wire_list_t | Box<[JsValue]> | List | List<T> |
Box<T> | *mut T | ffi.Pointer<T> | T | T | T |
Option<T> | *mut T | ffi.Pointer<T> | Option<T> | T? | T? |
Option<Box<T>> | *mut T | ffi.Pointer<T> | Option<T> | T? | T? |
enum/struct T | *mut wire_t | ffi.Pointer<T> | Array | List | class T |
enum T 3 | i32 4 | int 1 | i32 4 | int | enum T |
DartAbi | DartCObject | dynamic | JsValue | dynamic | dynamic |
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 StreamSink
s, 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.
BroadcastChannel
s 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,
BroadcastChannel
s and Worker
s, 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.
- 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 likeList<String?>
difficult to implement without hacks. - 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. - 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 :)
When behind a ffi.Pointer
, they are their respective types from dart:ffi
: ffi.Int8
, ffi.Int16
, etc.
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.
Refers to C-style enums only (no fields).
This is currently implemented as a monotonically-increasing index.
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 dispose
d.
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".
- In XCode, edit
Strip Style
inBuild Settings
toDebugging Symbols
. - Add your
lib{crate}.a
toLink Binary With Libraries
inBuild Phases
. - Add
binding.h
toCopy Bundle Resources
. - Add
#import "binding.h"
toRunner-Bridging-Header
. - 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'soverride 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 thatdummy_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
andmutable_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:
Cross-Origin-Resource-Policy
set tosame-origin
Cross-Origin-Embedder-Policy
set torequire-corp
1
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);
}
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
Worker
s. 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 aSyncReturn<_>
to avoid the internal thread pool interfering with your threads. This library includes aspawn!
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
orErr
s. - 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
andUint64List
throws when used on Web platforms. They are left intentionally unimplemented by the Dart language developers, perhaps due to the differences betweenint
andBigInt
. 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
andUint64List
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!