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 initinside$rust_part. This will createsrcfolder andCargo.tomlfile. - In the
srcfolder there is one file:main.rs. It can be deleted. Create 2 new files calledlib.rsandapi.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.rsfile 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.rsfile: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_bridgedocumentation (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.sofiles for two Android architectures. - For Windows:
cargo build --release(has to be executed on Windows OS) . Important: Thecrate-typeinCargo.tomlhas to be changed to"dylib". In folderrust_part/target/releaseyou will find files calledrust_part.dllandrust_part.dll.lib. Remove the.dllpart from the second one and the Windows files are ready. - For iOS:
cargo lipo. In folderrust_part/target/universal/releaseyou will findlibrust_part.afile. - For MacOS:
cargo build --release(has to be executed on Windows OS) . Important: Thecrate-typeinCargo.tomlhas to be changed to"dylib". In folderrust_part/target/releaseyou 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 xcodein$rust_part. This will create a.xcodeprojfile. This file will be soon opened in Xcode to change symbol stripping method. - Run
cargo lipoin$rust_part. To specify target, run with-p $targetflag. To build a release library (smaller in size), use--releaseflag. - 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.hActually, the location ofbridge_generated.his 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
.alibrary:ln -s ../$rust_part/target/universal/release/librust_part.aYou may also move the.afile to theiosfolder, this way there is no need for the symlink as the library is directly accessible. - Then append the contents of
bridge_generated.hto/ios/Classes/$Plugin.h:cat ios/bridge_generated.h >> ios/Classes/$Plugin.h - Then add in
ios/Classes/.swiftfile dummy method:1 2 3 4
public func dummyMethodToEnforceBundling() { // This will never be executed dummy_method_to_enforce_bundling(); }
- Next, edit
podspecfile 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
.xcodeprojin$rust_partand.xcodeworkspacein example (if you want to run the example). - Remember to edit
pubspec.yamlfile 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
pluginClasshere for iOS stands for.hfile in Classes folder.
iOS Troubleshooting
- run
pod installin ios folder with Runner (helps withmodule not founderror in Xcode) - to run a different dart file than
main.darteditFLUTTER_TARGETin 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,
.dylibfile type is necessary. To generate it, editCargo.toml, so that it has following structure:1 2
[lib] crate-type = ["dylib"]
Then run
cargo buildin your$cratedirectory. Remember to use the flag--releaseto make the lib much smaller. - Move your
.dylibfile tomacosfolder in your project. - In
.swiftfile inmacos/Classesadd the dummy method (more about it influtter_rust_bridgedocumentation):1 2 3 4
public func dummyMethodToEnforceBundling() { // This will never be executed dummy_method_to_enforce_bundling() }
- Don’t forget to edit
pubspec.yamland add the MacOS support:1 2 3 4
plugin: platforms: macos: pluginClass: FlutterRustPlugin
- Edit the
.podspecfile 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.hfile fromiosfolder 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 moduleerror while running the example, enterexample/macosfolder in project and executepod installin 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 deintegrateandpod installto reinstall pods. - Try deleting all folders from
/Users/<your username>/Library/Developer/Xcode/DerivedDataand 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=windowsin 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
windowsdirectory, let us refer to it by$crate. - Place the
.dlland.libfiles in$cratedirectory and change their names to$crate.dlland$crate.lib. - In your
$cratedirectory 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.cmakefile will be created in the next steps. - In your root folder, create
cmakedirectory. - Under
cmakedirectory create$crate.cmakefile. 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
cmakedirectory createmain.cmakefile. Append the following lines to the file:1 2
add_subdirectory($crate) target_link_libraries(${PLUGIN_NAME} PRIVATE ${CRATE_NAME}) - Edit the
windows/CMakeLists.txtfile. Add the following lines:1
include(../cmake/main.cmake)
Put this line after
target_link_librariesline.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.yamlfile: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
.libfolder should have a similar structure (old plugin template):1 2 3
├── lib ├── bridge_generated.dart └── flutter_rust_plugin.dartWhere
bridge_generated.dartis a file generated usingflutter_rust_bridge_codegenandflutter_rust_plugin.dartis the main plugin file. For more information on flutter plugin check out the official documentation. flutter_rust_plugin.dartfile 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
RustPartImplis 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.