Flutter calling Rust code operation guide

insert image description here

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 Flutterand . RustThis library is just a code generator to help your Flutter / Dartcall Rustfunction. 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 --libto 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.rsin:

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.tomlThe 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.yamlis 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_bridgeneed 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-xcodeif 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.rsThe rust code path.
  • lib/ffi/rust_ffi.dartGenerate dart code paths.
  • ios/Runner/bridge_generated.hCreate 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/configconfigured 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.gradleadd 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
        }
    }
}
  • ../../nativeIt is the rust code path.
  • ANDROID_NDKIt is android/gradle.propertiesthe 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/jniLibsin 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 xcodeprojsuffix folder will be generated. It can be used to import into other Xcode projects.
Please add a picture description
Open it in Xcode ios/Runner.xcodeproj, click on the menu File ---> Add Files to "Runner"and xxx.xcodeprojadd as subproject.

  • Click Runnerthe root project, click the plus sign Build Phasesunder Tab to add files.Target Dependencies$crate-staticlib
    Please add a picture description
  • Next, expand the following and click Link Binary With Librariesthe plus sign to add lib $crate_static.afiles for IOS.
    Please add a picture description
    After completion, the picture is as follows:
    Please add a picture description

Bind the header files that were initially generated bridge_generated.h.

In ios/Runner/Runner-Bridging-Header.hthe add: bridge_generated.h.

#import "GeneratedPluginRegistrant.h"
#import "bridge_generated.h"

ios/Runner/AppDelegate.swiftAdd 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!

reference

Guess you like

Origin blog.csdn.net/qq_17766199/article/details/131141194