Flutter and Rust combined. Creating a plugin to support various operating systems
Both, Flutter and Rust are pretty novel technologies in the industry. Both also introduce a paradigm shift of how to approach portability, a very old and diffcult problem to solve. Portability is diffcult due to lack of common denominator across platforms, devices and operating systems. To achieve it, Flutter comes with a concept of MethodChannel, a cross-boundary interface that allows to write and call platform native code. It then enables seamless integrations that are essential when working with Operating System specific user interface or natively accessing device peripherals. No more tweaks thanks to proper integration mechanisms. Rust, on the the other hand, is getting traction in various ecosystems and there are at least several reasons why it becomes more and more popular as general purpose programming language. Rust is in essence a C-based language with novel concepts and modern tooling supporting the language. It has steep learning curve due to the architectural decisions baked in into the language. However once it is overcame it pays off. One especially interesing characteristic of the language is its adaptability in almost any environment. As a C-based language, program written in Rust can be exposed as a binary to many modern Operating Systems. Not only that, thanks to Foreign Function Interface (FFI) integration possibilities of Rust-based code, it became viable alternative to write platform agnostic code and expose it through FFI. In other words one Rust library can be consumed by any other C-based language. The core business logic is then encapsulated into one library that is later consumed within platform specific languages.
This post guides the reader how to benefit from Flutter and Rust collaboration in a best form. When native programming lanugages available in FlutterMethodChannel
don’t come in handy, flutter_rust_bridge might be the solution. It allows the use of Rust code in Flutter application through an externally generated library. This tutorial however will not be introducing to the usage of the plugin. It assumes the user is familiar with flutter_rust_bridge
documentation and knows the basics. Moreover, to build for iOS and MacOS it is necessary to have access to Xcode and MacOS device. To build for Windows, Windows OS is needed as well. flutter_rust_bridge
provided tutorial for Android + Rust plugin so it will not be covered here.
A proof of concept plugin can be found here.
Initial steps
- In the root folder of your project create a new directory. It will be later referred here as
$rust_part
. - Run
cargo init
inside$rust_part
. This will createsrc
folder andCargo.toml
file. - In the
src
folder there is one file:main.rs
. It can be deleted. Create 2 new files calledlib.rs
andapi.rs
. The first one will call all modules from rust project while the other is a module containing all functions that should be bridged to dart. - Modify the
api.rs
file and add your library functionality. In this case it will be a simple hello world string function:1 2 3
pub fn hello() -> String { return "Hello World!".to_string(); }
- Modify the
lib.rs
file:pub mod api;
- Add the following lines to
Cargo.toml
(Notice: The lib lines may change depending on the platform you are building for. ):1 2 3 4 5
[lib] crate-type = ["staticlib", "cdylib"] [dependencies] flutter_rust_bridge = "1"
- Run the following commands in
$rust_part
:1 2 3 4 5
cargo install flutter_rust_bridge_codegen flutter pub add --dev ffigen flutter pub add ffi flutter pub add flutter_rust_bridge cargo install cargo-xcode
- Cross compiling targets setup will not be covered here. For more information on the topic please check the recommended
flutter_rust_bridge
documentation (here is an example of Android target setup). - The Rust part is ready to be built. For different targets use:
- For Android:
cargo ndk -o ../android/src/main/jniLibs build --release
. This command results in twolibrust_part.so
files for two Android architectures. - For Windows:
cargo build --release
(has to be executed on Windows OS) . Important: Thecrate-type
inCargo.toml
has to be changed to"dylib"
. In folderrust_part/target/release
you will find files calledrust_part.dll
andrust_part.dll.lib
. Remove the.dll
part from the second one and the Windows files are ready. - For iOS:
cargo lipo
. In folderrust_part/target/universal/release
you will findlibrust_part.a
file. - For MacOS:
cargo build --release
(has to be executed on Windows OS) . Important: Thecrate-type
inCargo.toml
has to be changed to"dylib"
. In folderrust_part/target/release
you will find file calledlibrust_part.dylib
.
- For Android:
iOS
- Make sure you created support for iOS in your project with
flutter create --platform=ios .
Warning: This command will create all files that are automatically created when making new Flutter project. If for some reason you deleted some of them, you might need to get rid of them again. - Run
cargo xcode
in$rust_part
. This will create a.xcodeproj
file. This file will be soon opened in Xcode to change symbol stripping method. - Run
cargo lipo
in$rust_part
. To specify target, run with-p $target
flag. To build a release library (smaller in size), use--release
flag. - Next, run the generator:
flutter_rust_bridge_codegen --rust-input $rust_part/src/api.rs --dart-output lib/bridge_generated.dart --c-output ios/bridge_generated.h
Actually, the location ofbridge_generated.h
is not that important, as it is created only to have its content appended to another file. - Then create a symbolic link in iOS folder to
.a
library:ln -s ../$rust_part/target/universal/release/librust_part.a
You may also move the.a
file to theios
folder, this way there is no need for the symlink as the library is directly accessible. - Then append the contents of
bridge_generated.h
to/ios/Classes/$Plugin.h
:cat ios/bridge_generated.h >> ios/Classes/$Plugin.h
- Then add in
ios/Classes/.swift
file dummy method:1 2 3 4
public func dummyMethodToEnforceBundling() { // This will never be executed dummy_method_to_enforce_bundling(); }
- Next, edit
podspec
file and add the following lines:1 2 3
s.public_header_files = 'Classes**/*.h' s.static_framework = true s.vendored_libraries = "**/*.a"
- Next, remember to set the strip style to non global symbols on both the
.xcodeproj
in$rust_part
and.xcodeworkspace
in example (if you want to run the example). - Remember to edit
pubspec.yaml
file so it has following structure:1 2 3 4 5 6 7
plugin: platforms: android: package: com.example.flutter_rust_plugin pluginClass: FlutterRustPlugin ios: pluginClass: FlutterRustPlugin
The
pluginClass
here for iOS stands for.h
file in Classes folder.
iOS Troubleshooting
- run
pod install
in ios folder with Runner (helps withmodule not found
error in Xcode) - to run a different dart file than
main.dart
editFLUTTER_TARGET
in Xcode in Runner Build Settings. - check
iOS Deployment Target
, 9.0 might be too old for some releases.
MacOS
This tutorial is made for a multiplatform project and it assumes the iOS support is already working.
-
Add support for MacOS in your project by executing
flutter create --platform=macos .
Warning: This command will create all files that are automatically created when making new Flutter project. If for some reason you deleted some of them, you might need to get rid of them again. - To link your Rust library with MacOS,
.dylib
file type is necessary. To generate it, editCargo.toml
, so that it has following structure:1 2
[lib] crate-type = ["dylib"]
Then run
cargo build
in your$crate
directory. Remember to use the flag--release
to make the lib much smaller. - Move your
.dylib
file tomacos
folder in your project. - In
.swift
file inmacos/Classes
add the dummy method (more about it influtter_rust_bridge
documentation):1 2 3 4
public func dummyMethodToEnforceBundling() { // This will never be executed dummy_method_to_enforce_bundling() }
- Don’t forget to edit
pubspec.yaml
and add the MacOS support:1 2 3 4
plugin: platforms: macos: pluginClass: FlutterRustPlugin
- Edit the
.podspec
file and add following lines:1 2 3
s.vendored_libraries = "**/*.dylib" s.public_header_files = 'Classes**/*.h' s.static_framework = true
- Copy the
bridge_generated.h
file fromios
folder tomacos/Classes
. This file has been generated when enabling support for iOS. To generate it, run:flutter_rust_bridge_codegen --rust-input $rust_part/src/api.rs --dart-output lib/bridge_generated.dart --c-output macos/Classes/bridge_generated.h
MacOS Troubleshooting
- If you run into
no such module
error while running the example, enterexample/macos
folder in project and executepod install
in the command line. This installs the missing module. - If during testing the example you run into
cannot find 'dummy_method_to_enforce_bundling' in scope
, runpod update
. - For other errors, try
pod deintegrate
andpod install
to reinstall pods. - Try deleting all folders from
/Users/<your username>/Library/Developer/Xcode/DerivedData
and cleaning your build folder.
Windows
This part of the tutorial assumes the user has generated library files .dll
and .lib
as described in Initial steps.
- If your plugin project does not have Windows support activated, execute
flutter create --platform=windows
in project root folder:
Warning: This command will create all files that are automatically created when making new Flutter project. If for some reason you deleted some of them, you might need to get rid of them again.
- Make a new folder under created in previous point
windows
directory, let us refer to it by$crate
. - Place the
.dll
and.lib
files in$crate
directory and change their names to$crate.dll
and$crate.lib
. - In your
$crate
directory create a new file,CMakeLists.txt
. Append the following lines to the file:1 2 3 4
include(../../cmake/$crate.cmake) set_property(TARGET ${CRATE_NAME} PROPERTY IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/$crate.dll") set_property(TARGET ${CRATE_NAME} PROPERTY IMPORTED_IMPLIB "${CMAKE_CURRENT_SOURCE_DIR}/$crate.lib")
The included
$crate.cmake
file will be created in the next steps. - In your root folder, create
cmake
directory. - Under
cmake
directory create$crate.cmake
file. Append the following lines to the file:1 2 3 4 5 6 7 8
message("-- Linking Rust") set(CRATE_NAME "$crate") set(CRATE_NAME ${CRATE_NAME} PARENT_SCOPE) if(CRATE_STATIC) add_library(${CRATE_NAME} STATIC IMPORTED GLOBAL) else() add_library(${CRATE_NAME} SHARED IMPORTED GLOBAL) endif()
- Under
cmake
directory createmain.cmake
file. Append the following lines to the file:1 2
add_subdirectory($crate) target_link_libraries(${PLUGIN_NAME} PRIVATE ${CRATE_NAME})
- Edit the
windows/CMakeLists.txt
file. Add the following lines:1
include(../cmake/main.cmake)
Put this line after
target_link_libraries
line.1 2 3 4 5 6 7
# 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_rust_plugin_bundled_libraries "$<TARGET_FILE:${CRATE_NAME}>" PARENT_SCOPE )
Here, change
""
to"$<TARGET_FILE:${CRATE_NAME}>"
. - Don’t forget to declare support for windows in
pubspec.yaml
file:1 2 3 4 5 6 7
plugin: platforms: android: package: com.example.flutter_rust_plugin pluginClass: FlutterRustPlugin windows: pluginClass: FlutterRustPluginCApi
Integration with Dart
- Your
.lib
folder should have a similar structure (old plugin template):1 2 3
├── lib ├── bridge_generated.dart └── flutter_rust_plugin.dart
Where
bridge_generated.dart
is a file generated usingflutter_rust_bridge_codegen
andflutter_rust_plugin.dart
is the main plugin file. For more information on flutter plugin check out the official documentation. flutter_rust_plugin.dart
file contains all methods that will be available in the plugin for the users. The libraries is loaded there. Here is an example of code used to load the libraries:1 2 3 4 5 6 7 8
static const base = 'rust_part'; static final path = Platform.isWindows? '$base.dll' : 'lib$base.so'; static late final dylib = Platform.isIOS ? DynamicLibrary.process() : Platform.isMacOS ? DynamicLibrary.executable() : DynamicLibrary.open(path); static late final api = RustPartImpl(dylib);
The
RustPartImpl
is the name of the class inbridge_generated.dart
, the one class that extendsFlutterRustBridgeBase
. In order to call the method from library, use:await api.methodName();
References
- iOS: This tutorial was created using the official documentation of flutter_rust_bridge and mozilla github post. If something is not clear, checking out these sources might help you.
- Windows: This tutorial was created using the official documentation of flutter_rust_bridge and this proof of concept for Flutter+Rust plugin. If something is not clear, checking out these sources might help you.