#flutter #dart #ffi #code-generation #bindings

flutter_rust_bridge

High-level memory-safe binding generator for Flutter/Dart <-> Rust

14 stable releases

new 1.8.0 Nov 27, 2021
1.7.0 Nov 26, 2021
1.2.2 Oct 29, 2021

#42 in FFI

Download history 48/week @ 2021-10-05 45/week @ 2021-10-12 35/week @ 2021-10-19 50/week @ 2021-10-26 51/week @ 2021-11-02 870/week @ 2021-11-09 2010/week @ 2021-11-16 2124/week @ 2021-11-23

5,083 downloads per month

MIT license

43KB
341 lines

flutter_rust_bridge: High-level memory-safe binding generator for Flutter/Dart <-> Rust

Codacy Badge Rust Package Flutter Package CI Documentation Example

Logo

Want to combine the best between Flutter, a cross-platform hot-reload rapid-development UI toolkit, and Rust, a language empowering everyone to build reliable and efficient software? Here it comes!

🚀 Advantages

  • Memory-safe: Never need to think about malloc/free.
  • Type support: Unlike low-level binding generator which only provide primitives and pointers, this package provides things like Vec<u8>(Uint8List), Vec<T>(List<T>), any custom struct(class)s, and even recursive structs (e.g. a tree node).
  • Zero-copy: Pass big array of bytes from Rust to Dart without any memory copies.
  • Async programming: Simply call functions directly in main isolate (thread) of Dart/Flutter, and Rust code will not block the Flutter UI.
  • Easy to use: All you need to do is write down your Rust code. The code generator will do everything and expose an API in Dart/Flutter's style.
  • Lightweight: This is not a huge framework that includes everything, so you are free to use your favorite Flutter and Rust libraries. For example, state-management with Flutter library (e.g. MobX) can be elegant and simple (contrary to implementing in Rust); implementing a photo manipulation algorithm in Rust will be fast and safe (countrary to implementing in Flutter).
  • Easy to code-review & convince yourself: This package simply simulates how human beings write down boilerplate code. If you want to convince yourself (or your team) that it is safe, there is not much code for you to look at. The runtime is only hundreds of loc, and the generated code follows simple patterns. No magic and also no blackbox macros at all! (More about safety concerns.)
  • Pure-Dart compatible: Despite the name, this package is 100% compatible with pure Dart. It does not require anything specific to Flutter. See this pure-Dart example.

🧭 Show me the code

What you write down (in Rust):

pub fn my_function(a: MyTreeNode, b: SomeOtherStruct) -> Result<Vec<u8>> {
    ... do my heavy computations ...
}

// you can use structs (even recursive)
pub struct TreeNode { pub value: String, pub children: Vec<MyTreeNode> }

With bindings automatically generated, you can simply use the following API in Flutter/Dart. Nothing more.

Future<Uint8List> myFunction(MyTreeNode a, SomeOtherStruct b);

Remark: Why Future in Flutter: Flutter is single-threaded. If not using future, just like what you 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 a second, your UI will fully freeze for one second.

💡 Quickstart

Install

  • Install dependency cbindgen: cargo install cbindgen (may need latest version, thanks @gmorenz)
  • Install dependency ffigen: dart pub global activate ffigen, and install LLVM. If you are running macOS, check below under MacOS Caveats for instructions on how to install and use llvm.
  • Install this code generator binary by cargo install flutter_rust_bridge_codegen.
  • Add flutter_rust_bridge = "1.0" (where 1.0 should be the latest version) to Rust's Cargo.toml.
  • Add flutter_rust_bridge: ^1.0 (same as above, should be latest version) to Flutter/Dart's pubspec.yaml under the section of dependencies.

Run

flutter_rust_bridge_codegen --rust-input path/to/your/api.rs --dart-output path/to/file/being/bridge_generated.dart

If you have problems (such as failure on MacOS), please see the "Troubleshooting" section below.

(For more options, use --help; To see what types and function signatures can you write in Rust, have a look at this example.) (For Windows, you may need \\ instead of / for paths.)

Enjoy

Use the class in the generated .dart file, as if it is a normal Flutter/Dart class! (The abstract class at the top of the generated file.)

Want to see a Flutter tutorial with UI? See the tutorial section below. Want pure-Dart example? Here is another tutorial.

Remark: If you are interested, why abstractclass can be used - it is because of the factory language feature.

📪 Safety

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.

📚 Tutorial: A Flutter+Rust app

Remark: The flutter_*_test sections of the CI workflow can also be useful, if you want details of each command.

Get example code

Please install Flutter, 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/with_flutter.

(Optional) Run code generator

I have generated the source code already (in quickstart), 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/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

Remark: If you have problems, see "Troubleshooting" section. For Windows, you may need \\ instead of / for paths.

Run "Flutter+Rust" app

With this app, you will see a Mandelbrot set plotted in Flutter UI and generated by Rust algorithm.

If Android

Run cargo ndk -o ../android/app/src/main/jniLibs build. Then run the Flutter app normally as is taught in official tutorial. For example, flutter run.

Remark: Since my quickstart app is so baremetal, I do not integrate the Rust building process into Flutter building process. But you can look at this tutorial to easily do that.

If iOS

Modify Cargo.toml to change cdylib to staticlib. (Again, this is baremetal example so it is done manually. For your project, you can automate it.)

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 as is taught in official tutorial. For example, flutter run. (Similarly, this tutorial can automate the process.)

If Desktop (Windows/Linux/MacOS)

Remark: If you only want to develop a mobile app, skip this section - it is for creating desktop apps.

Run it directly using flutter run assuming Flutter desktop support has been configured.

Flutter can run on Windows/Linux/MacOS without any problem, and this lib does nothing but generates some code like a human being. Therefore, this package should work well as long as you set up the Flutter desktop app's ffi functionality successfully.

Windows/Linux

This example (frb_example/with_flutter) already demonstrated how to integrate Cargo with CMake on Linux and Windows, and more details can be seen in #66.

MacOS

To integrate a dynamic library to your macOS app, you need to configure your Runner.xcworkspace in Xcode. Here, I show the instructions for Xcode 13.1:

  1. Open the yourapp/macos/Runner.xcworkspace in Xcode.
  2. Drag your precompiled library (libyourlibrary.dylib) into Runner/Frameworks.
  3. Click Runner (with a blue app store logo on the left, not the folder underneath) and go to the Build Phases tab.
    1. Drag libyourlibrary.dylib into the Copy Bundle Resources list.
    2. Under Bundle Frameworks, drag libyourlibrary.dylib to the list, Code sign on copy should be checked by default.
    3. Under Link Binary With Libraries, set status of libyourlibrary.dylib to Optional. (We use dynamic linking, no need to statically link.)
  4. Click Runner and go to the General tab.
    1. You should see libyourlibrary.dylib in the Frameworks, Libararies and Embedded Content list, with the option saying Embed & Sign.
    2. If not, drag your library to the list and select the option.
  5. Click Runner and go to the Build Settings tab.
    1. In the Search Paths section configure the Library Search Paths to include the absolute path where libyourlibrary.dylib is located.

In lib/main.dart, you can now use DynamicLibrary.open('libyourlibrary.dylib') to dynamically link to the symbols. This example app shows how to do dynamic linking on other platforms as well so be sure to take a look at frb_example/with_flutter/lib/main.dart. Reference the flutter documentation for details, do note that the Xcode version they demonstrates in may not be the latest.

(Optional) See more types that this library can generate

Have a look at the function arguments and return types in this file: api.rs. With this library, we have a generated API that resides at generated_api.dart (of course, that is auto generated, and you can use it in other Dart code).

(Optional) Remarks

The mod

If you are adding this lib to your own existing code, please put mod generated_wire; (where generated_wire is the name of the wire file that you choose) into your lib.rs or main.rs. Only by doing this, Rust can understand that this generated file is a part of your project.

Version

Dart SDK >=2.14.0 is needed not by this library, but by the latest version of the ffigen tool. Therefore, write sdk: ">=2.14.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.

📚 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) Run code generator

Remark: I have generated the source code already (in quickstart), 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.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.

Command line arguments

Simply add --help to see full documentation.

flutter_rust_bridge_codegen

USAGE:
    flutter_rust_bridge_codegen [FLAGS] [OPTIONS] --dart-output <dart-output> --rust-input <rust-input>

FLAGS:
        --skip-add-mod-to-lib    Skip automatically adding `mod bridge_generated;` to `lib.rs`
    -h, --help                   Prints help information
    -V, --version                Prints version information

OPTIONS:
    -r, --rust-input <rust-input>                              Path of input Rust code
    -d, --dart-output <dart-output>                            Path of output generated Dart code
    -c, --c-output <c-output>                                  Path of output generated C header
        --rust-crate-dir <rust-crate-dir>                      Crate directory for your Rust project
        --rust-output <rust-output>                            Path of output generated Rust code
        --class-name <class-name>                              Generated class name
        --dart-format-line-length <dart-format-line-length>    Line length for dart formatting
        --llvm-path <llvm-path>                                Path to the installed LLVM

What this library is & isn't

This library is nothing but a code generator that helps your Flutter/Dart functions call Rust functions. Therefore, you may refer to external materials to learn Flutter, learn Rust, learn Flutter FFI (Dart FFI) and so on. With material on the Internet, you will know how to create a mobile application using Flutter, and how that app can call Rust functions via Dart FFI (in the C ABI). Then this package comes in, and ease you from the burden to write down tons of boilerplate code ;)

Troubleshooting

Have problems when using Linux? (The generated store_dart_post_cobject() has the wrong signature / 'stdarg.h' file not found / ...)

Try to run code generator with working directory at /, or add include path as is described in #108. This is a problem with Rust's builtin Command. See #108 for more details.

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

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/

If you are on Intel, you can install llvm using brew install llvm and it will be installed at /usr/local/homebrew/opt/llvm/ by default.

If you are on M1, you need to install the x86 versions of everything and run them through Rosetta 2, since Flutter does not support M1 yet. Start by installing Rosetta 2 if you haven't already:

/usr/sbin/softwareupdate --install-rosetta

Then, install an x86 version of brew to /usr/local:

arch -x86_64 zsh
cd /usr/local && mkdir homebrew
curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew

Then, you need to use the x86 brew to install the x86 version of llvm:

arch -x86_64 /usr/local/homebrew/bin/brew install llvm

Reference this article for details.

And when you build with cargo, you need to select x86 as the target:

cargo build --target=x86_64-apple-darwin

Other problems?

Don't hesitate to open an issue! I usually reply within minutes or hours (except when sleeping, of course).

Feature details

Here is a list of types that the code generator can generate:

[WIP] You can read the pure_dart example code currently, before I put the full list here.

Here are other functionalities:

[WIP] (e.g. Stream is supported but not documented yet).

Advanced

Customize handler's behavior

By default, the DefaultHandler is used. You can implement your own Handler doing whatever you want. In order to do this, create a variable named FLUTTER_RUST_BRIDGE_HANDLER in the Rust input file (probably using lazy_static). 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.

Setup/init FFI call

If you want that feature, have a look at FlutterRustBridgeSetupMixin in the Dart side.

Appendix: Set up Flutter/Dart+Rust support

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.14.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
crate-type = ["cdylib"] # <-- notice this type. `cdylib` for android, and `staticlib` for iOS. I write down a script to change it before build.

Step 4: Follow the standard steps of "how iOS uses static libraries". For example, in XCode, edit Strip Style in Build Settings to Debugging Symbols. Also, add your libyour_generate_file.a to Link Binary With Libraries in Build Phases. Add binding.h to Copy Bundle Resources. Add #import "binding.h" to Runner-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's override func application(...) {}, and this will prevent symbol stripping - especially in the release build for iOS (i.e. when building ipa file or releasing to App Store). Notice that, we have to use that dummy_method_to_enforce_bundling(), otherwise the symbols will not maintain in the release build, and Flutter will complain it cannot find the symbols.

Lastly, in order to build Rust automatically when you are building Flutter, follow this tutorial.

Appendix: Future work

I plan to support the following features. Of course, if you want to have other features, feel free to make an issue or PR.

  • Support async in Rust (currently only async in Dart). Should be quite easy to implement; I have not done it because my use case currently does not includ that, but feel free to PR.
  • Beautify the generated code, possibly making the cases (camel/snake/...) consistent with the language guide.
  • Make the code generator more robust to invalid inputs.

Appendix: Contributing

Please look at contributing guide.

Contributors ✨

All Contributors

Thanks goes to these wonderful people (emoji key):


fzyzcjy

💻 📖 💡 🤔 🚧

Viet Dinh

💻 ⚠️ 📖

Marcel

💻

rustui

📖

Michael Bryan

💻

bus710

📖

Sebastian Urban

💻

Daniel

💻

Kevin Li

💻 📖

This project follows the all-contributors specification. Contributions of any kind welcome!

Dependencies

~1MB
~17K SLoC