In the previous article on developing a gadget using Rust and Flutter , we used Rust code to implement a simple WebSocket sending function. The packaging of the Rust library and the use of double-ended are also introduced in the cross-compilation of the Rust library and the use of Android and iOS .
Today we continue to use the previous WebSocket code as an example to introduce how to use it in the Flutter project.
Preparation
The protagonist of this article is flutter_rust_bridge , which is an advanced memory-safe binding generator for Flutter
and . Rust
This library is just a code generator to help your Flutter / Dart
call Rust
function. It just generates some boilerplate code instead of writing it by hand.
First we can put the Rust code into the root directory of the Flutter project, or run cargo new --lib
to create a new Rust crate. After completion, the project structure is as follows:
├── android
├── ios
├── lib
├── linux
├── macos
├── $crate
│ ├── Cargo.toml
│ └── src
├── test
├── web
└── windows
Note: Setting the crate's root directory at the same level as other projects will help simplify the configuration process.
Slightly modify the previous rust code (note that the code should not be written directly in lib.rs, otherwise the generated file will not be able to obtain the import package):
Here we put the previous code api.rs
in:
use std::collections::HashMap;
use std::sync::Mutex;
use ws::{
connect, Handler, Sender, Handshake, Result, Message, CloseCode, Error};
use ws::util::Token;
lazy_static! {
static ref DATA_MAP: Mutex<HashMap<String, Sender>> = {
let map: HashMap<String, Sender> = HashMap::new();
Mutex::new(map)
};
}
struct Client {
sender: Sender,
host: String,
}
impl Handler for Client {
fn on_open(&mut self, _: Handshake) -> Result<()> {
DATA_MAP.lock().unwrap().insert(self.host.to_owned(), self.sender.to_owned());
Ok(())
}
fn on_message(&mut self, msg: Message) -> Result<()> {
println!("<receive> '{}'. ", msg);
Ok(())
}
fn on_close(&mut self, _code: CloseCode, _reasonn: &str) {
DATA_MAP.lock().unwrap().remove(&self.host);
}
fn on_timeout(&mut self, _event: Token) -> Result<()> {
DATA_MAP.lock().unwrap().remove(&self.host);
self.sender.shutdown().expect("shutdown error");
Ok(())
}
fn on_error(&mut self, _err: Error) {
DATA_MAP.lock().unwrap().remove(&self.host);
}
fn on_shutdown(&mut self) {
DATA_MAP.lock().unwrap().remove(&self.host);
}
}
pub fn websocket_connect(host: String) {
if let Err(err) = connect(host.to_owned(), |out| {
Client {
sender: out,
host: host.to_owned(),
}
}) {
println!("Failed to create WebSocket due to: {:?}", err);
}
}
pub fn send_message(host: String, message: String) {
let binding = DATA_MAP.lock().unwrap();
let sender = binding.get(&host.to_owned());
match sender {
Some(s) => {
if s.send(message).is_err() {
println!("Websocket couldn't queue an initial message.")
};
} ,
None => println!("None")
}
}
pub fn websocket_disconnect(host: String) {
DATA_MAP.lock().unwrap().remove(&host.to_owned());
}
api.rs
mod api;
#[macro_use]
extern crate lazy_static;
Cargo.toml
The configuration is as follows:
[package]
name = "rust_demo"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "rust_demo"
crate-type = ["staticlib", "cdylib"]
[profile.release]
lto = true
opt-level = 'z'
strip = true
codegen-units = 1
# panic = 'abort'
[dependencies]
ws = "0.9.2"
lazy_static = "1.4.0"
flutter_rust_bridge = "=1.77.1"
flutter_rust_bridge_macros = "=1.77.1"
[build-dependencies]
flutter_rust_bridge_codegen = "=1.77.1"
The configuration in Flutter pubspec.yaml
is as follows:
dependencies:
flutter_rust_bridge: 1.77.1
ffi: ^2.0.1
dev_dependencies:
ffigen: ^8.0.2
The versions noted here flutter_rust_bridge
need to be consistent. I am currently using 1.77.1 here. Then execute in the rust project:
cargo install flutter_rust_bridge_codegen
# 如果为iOS或MacOS应用构建
cargo install cargo-xcode
flutter_rust_bridge_codegen
, which generates the core of the Rust-Dart glue code.ffigen
, to generate Dart code from C header files/- To install LLVM, see Installing LLVM , ffigen will use it.
- (Optional)
cargo-xcode
if you want to generate Xcode projects for IOS and MacOS.
After completing the above preparatory work, we can execute the command under the Flutter project and successfully glue the code.
flutter_rust_bridge_codegen -r native/src/api.rs -d lib/ffi/rust_ffi.dart -c ios/Runner/bridge_generated.h
native/src/api.rs
The rust code path.lib/ffi/rust_ffi.dart
Generate dart code paths.ios/Runner/bridge_generated.h
Create a C header file that lists all the symbols exported by the Rust library, we need to use it to ensure that Xcode will not strip the symbols.
Android configuration
Installed first cargo-ndk
, it compiles the code to a suitable JNI without additional configuration. In our previous article, we manually .cargo/config
configured the path of the clang linker in . It is more cumbersome, this plug-in is to simplify this operation.
Install command:
// ndk低于22
cargo install cargo-ndk --version 2.6.0
// ndk高于22
cargo install cargo-ndk
Cross-compiling to Android requires some additional components, as explained in our previous article:
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android
Next, android/app/build.gradle
add the following lines at the end of the :
[
Debug: null,
Profile: '--release',
Release: '--release'
].each {
def taskPostfix = it.key
def profileMode = it.value
tasks.whenTaskAdded {
task ->
if (task.name == "javaPreCompile$taskPostfix") {
task.dependsOn "cargoBuild$taskPostfix"
}
}
tasks.register("cargoBuild$taskPostfix", Exec) {
workingDir "../../native"
environment ANDROID_NDK_HOME: "$ANDROID_NDK"
commandLine 'cargo', 'ndk',
// the 2 ABIs below are used by real Android devices
'-t', 'armeabi-v7a',
'-t', 'arm64-v8a',
'-o', '../android/app/src/main/jniLibs', 'build'
if (profileMode != null) {
args profileMode
}
}
}
../../native
It is the rust code path.ANDROID_NDK
It isandroid/gradle.properties
the NDK path that is being configured.- Every time Android runs, it will package the rust code and put the so file
android/app/src/main/jniLibs
in it. So if the Rust code is unchanged, you can comment out the code here after generating the release file.
ANDROID_NDK=/Users/weilu/android/android-sdk-macosx/ndk/21.4.7075529
iOS configuration
Install cross-compilation components:
rustup target add aarch64-apple-ios x86_64-apple-ios
Then execute it in the rust project directory cargo xcode
. After execution, a xcodeproj
suffix folder will be generated. It can be used to import into other Xcode projects.
Open it in Xcode ios/Runner.xcodeproj
, click on the menu File ---> Add Files to "Runner"
and xxx.xcodeproj
add as subproject.
- Click
Runner
the root project, click the plus signBuild Phases
under Tab to add files.Target Dependencies
$crate-staticlib
- Next, expand the following and click
Link Binary With Libraries
the plus sign to add lib$crate_static.a
files for IOS.
After completion, the picture is as follows:
Bind the header files that were initially generated bridge_generated.h
.
In ios/Runner/Runner-Bridging-Header.h
the add: bridge_generated.h
.
#import "GeneratedPluginRegistrant.h"
#import "bridge_generated.h"
ios/Runner/AppDelegate.swift
Add in dummy_method_to_enforce_bundling()
:
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let dummy = dummy_method_to_enforce_bundling()
print(dummy)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Flutter call
So far, the configuration work has been completed. Let's see how to call it. First, we simply encapsulate a method call class.
import 'dart:ffi';
import 'dart:io';
import 'package:flutter_ffi/ffi/rust_ffi.dart';
class NativeFFI {
NativeFFI._();
static DynamicLibrary? _dyLib;
static DynamicLibrary get dyLib {
if (_dyLib != null) return _dyLib!;
if (Platform.isIOS) {
_dyLib = DynamicLibrary.process();
} else if (Platform.isAndroid) {
_dyLib = DynamicLibrary.open('librust_demo.so');
} else {
throw Exception('DynamicLibrary初始化失败');
}
return _dyLib!;
}
}
class NativeFun {
static final _ffi = RustDemoImpl(NativeFFI.dyLib);
static Future<void> websocketConnect(String host) async {
return await _ffi.websocketConnect(host: host);
}
static Future<void> sendMessage(String host, String message) async {
return await _ffi.sendMessage(host: host, message: message);
}
static Future<void> websocketDisconnect(String host) async {
return await _ffi.websocketDisconnect(host: host);
}
}
When used, calling NativeFun.xxx()
the method directly will be fine.
I have submitted the above sample code to Github , you can run and view it if you need it. If it is helpful to you, please like it and save it~ See you next month!