[官文翻译]绑定Flutter和Rust高级的内存安全代码生成器 flutter_rust_bridge - 特性(三)

510 阅读7分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」


[官文翻译]绑定Flutter和Rust高级的内存安全代码生成器 flutter_rust_bridge - 特性(三)-

官网:Introduction - flutter_rust_bridge (cjycode.com)

pub: flutter_rust_bridge | Dart Package (flutter-io.cn)

译时版本:1.49.1

原文链接:


Dart 中的同步

如果需要生成 Dart 中的同步函数,可以使用 SyncReturn<T> 作为返回类型。 建议只对很快的 Rust 函数使用,否则 Dart 的 UI 会被阻塞。

现在, SyncReturn 支持 String、 Vec<u8> 和基本类型(bool、 u8、 u16、 u32、 u64、 i8、 i16、 i32、 i64、 f32、 f64)。如果需要其它类型,请提出 issue 。

并发

多个 Rust 函数可以同时运行,它们会并发执行。 这是因为默认情况下使用线程池来执行 Rust 函数。 尽管如此,也可以完全定制该行为(甚至可以不用线程池)。

示例

考虑下面的 Rust 代码:

pub fn compute() {
  thread::sleep(Duration::from_millis(1000));
}

然后下面的 Dart 代码会使用它:

var a = compute();
var b = compute();
var c = compute();
await Future.wait([a, b, c]); // 可能需要学习 Dart 中的 `Future` 和 `async` 来理解该点

这会花费 1 秒钟,而不是 3 秒钟 来完成运行,因为多个 compute 可以并发运行。

Handler

默认会使用 DefaultHandler 。也可以实现自己的  Handler 来做任何想做的事。 要这样做的话,需要在 Rust 的输入文件(可能用 lazy_static)中创建一个名为 FLUTTER_RUST_BRIDGE_HANDLER 的变量。可能不需要创建全新的结构体来实现  Handler ,取而代之的是,使用 SimpleHandler ,然后定制它的泛型,如它的 Executor

示例

示例:除了向 Dart 报告错误之外也向后台报告错误

pub struct MyErrorHandler(ReportDartErrorHandler);

impl ErrorHandler for MyErrorHandler {
    fn handle_error(&self, port: i64, error: handler::Error) {
        send_error_to_your_backend(&error);
        self.0.handle_error(port, error)
    }
    
    ...
}

示例:执行开始和结束时打日志

pub struct MyExecutor(ThreadPoolExecutor<MyErrorHandler>);

impl Executor for MyExecutor {
    fn execute<TaskFn, TaskRet>(&self, wrap_info: WrapInfo, task: TaskFn) {
        let debug_name_string = wrap_info.debug_name.to_string();
        self.thread_pool_executor
            .execute(wrap_info, move |task_callback| {
                Self::log_around(&debug_name_string, move || task(task_callback))
            })
    }
}

impl MyExecutor {
    fn log_around<F, R>(debug_name: &str, f: F) -> R where F: FnOnce() -> R {
        let start = Instant::now();
        debug!("(Rust) execute [{}] start", debug_name);
        let ret = f();
        debug!("(Rust) execute [{}] end delta_time={}ms", debug_name, start.elapsed().as_millis());
        ret
    }
}

初始化

如果需要该特性,看一下 Dart 中的 FlutterRustBridgeSetupMixin 。 (会添加更多信息;如果现在有问题可以创建 issue 。)

Rust 中的异步

要从 Rust 函数中使用 async/await 或者返回一个 Future 类型,可以参考该文档。 如果对其它集成生成器也有兴趣,请提出 issue 。

多文件

当有一个大工程时,如果把所有内容都放在单个 api.rs 文件中通常会很低效, 取而代之的是可能想要分成 api_of_one_module.rs 、 api_of_another_module.rs 、等。 这也是有该特性的原因。

基本上,仅需要指定所有输入的 Rust 文件和所有的输出位置就可以了。 这里有个示例:

flutter_rust_bridge_codegen \
  --rust-input "$REPO_DIR/native/src/api_1.rs" "$REPO_DIR/native/src/api_2.rs" \
  --dart-output "$REPO_DIR/lib/bridge_generated_api_1.dart" "$REPO_DIR/lib/bridge_generated_api_2.dart" \
  --class-name ApiClass1 ApiClass2 \
  --rust-output generated_api_1 generated_api_2

更多细节看下该篇文章.

build.rs 中运行

有两种途径可以执行该代码生成器。 第一种且最明显的方式是直接在命令行运行 flutter_rust_bridge 。

第二种方式是集成到工程的 build.rs 中。 用这种方式,代码生成器会在编译 Rust 工程时自动触发。 示例配置可以看下这个 build.rs 文件。

可取消的任务

Rust代码的计算很重时,可能想要在过程中取消该任务。 例如,用户不再需要该处理。 这样可以节省宝贵的计算力。

安装:现在,该特性已完成,并且已经在自己(作者)的应用中使用了一些时间。(还没有将该PR合并到主分支中,仅仅是因为需要确定一下如果在  api.rs 中如何放置这些代码)。所以,访问 #333 然后把代码直接复制到工程中,像通常一样使用它即可。

对象池

在 Rust 端有一些大的对象时,可能不想在 Rust 和 Dart 之间一遍又一遍地复制。 这个时候对象池就有用了:只需要在 Rust 和 Dart 之间传递一个 "对象句柄"(实际上只是一些整数),然后 Rust 端会在该句柄和真正的对象之间进行转换。

安装:和 可取消的任务一样,看一下相关文档。

WASM

flutter_rust_bridge_codegen 也可以使用 wasm_bindgen 生成可在浏览器中运行的代码。 要生成 WASM 特定文件,需要在调用时传递两个选项:

flutter_rust_bridge_codegen .. --wasm --dart-decl-output <DECL>

DECL 是公用的类/函数声明文件。 例如,如果生成的Dart桥接内容输出到 lib/bridge_generated.dart 中,可以设置声明文件为  lib/bridge_definitions.dart

该操作默认会创建一些新文件:

├── lib
│   ├── bridge_definitions.dart
│   ├── bridge_generated.io.dart
│   └── bridge_generated.web.dart
└── native/src
    ├── bridge_generated.io.rs
    └── bridge_generated.web.rs

.io 和 .web 模块实现了不同平台的辅助类。 由于 Dart 的模块系统,这种分割是强制的。 尽管如此,如果更喜欢在单个文件中保持 Rust Bridge ,也可以传递 --inline-rust 标志。

查看 Web集成 中如何消费 Web Bridge 的说明。

其它项目

从实现中分离生成的定义

生成的 bridge_generated.dart 默认会包含 API 的定义以及实现。 使用 --dart-decl-output 标志,可以分离开这两部分,定义不会包含任何内容如 dart:ffi

更多信息:#298.

日志

由于看到一些问题是问如何在 Flutter + Rust 应用中实现日志功能,这里有一些示例。

生产环境的日志

我自己的应用在生产环境下,使用了下面的策略用于 Rust 日志:使用通常的 Rust 日志方法,如  info! 和 debug! 宏。日志会在两个地方被消费:通过平台独有的方法打印出来(如 Android 的 Logcat 和 iOS 的 NSLog),也可用 Stream (流)将它们发送到 Dart 端,这样,Dart 代码和进一步的处理会使用相同的渠道将其作为 Dart 的日志(例,保存为文件、发送到服务器、等)

我的应用中 和日志相关的 完整 代码可以在这里看到:#486

简单的日志记录器

Let us implement a simple logging system (adapted from the logging system I use with flutter_rust_bridge in my app in production), where Rust code can send logs to Dart code.

让我们实现一个简单的日志系统(我的生产环境中的应用中使用了 flutter_rust_bridge 适配日志系统), 它可以从 Rust 代码向 Dart 代码发送日志。

Rust api.rs:

pub struct LogEntry {
    pub time_millis: i64,
    pub level: i32,
    pub tag: String,
    pub msg: String,
}

// 为了演示已简化
// 编译的话,需要 OnceCell 或 Mutex 或 RwLock
// 可以看下 https://github.com/fzyzcjy/flutter_rust_bridge/issues/398
lazy_static! { static ref log_stream_sink: StreamSink<LogEntry>; }

pub fn create_log_stream(s: StreamSink<LogEntry>) {
    stream_sink = s;
}

现在 Rust 很可能会抱怨因为没有为 LogEntry 实现 IntoDart。 这正是所期望的,因为 flutter_rust_bridge 会生成这个行为的实现。 要改正这个错误,只需要返回 flutter_rust_bridge_codegen

生成的 Dart 代码:

Stream<LogEntry> createLogStream();

现在在 Dart 中使用它:

Future<void> setup() async {
    createLogStream().listen((event) {
      print('log from rust: ${event.level} ${event.tag} ${event.msg} ${event.timeMillis}');
    });
}

现在可以愉快地在 Rust 中记录任何信息了:

log_stream_sink.add(LogEntry { msg: "hello I am a log from Rust", ... })

当然,也可以按照 包装数据流的 Sink 的 Rust 的 log crate 实现一个日志记录器,然后使用如 info! 之类的标准的 Rust 日志机制。 我在我的工程中正是这样做的。

示例:简单的计数器

这个帖子 和 #347.

use anyhow::Result;
use std::{thread::sleep, time::Duration};

use flutter_rust_bridge::StreamSink;

const ONE_SECOND: Duration = Duration::from_secs(1);

// 这里还不能返回类型,这是个 BUG
pub fn tick(sink: StreamSink<i32>) -> Result<()> {
    let mut ticks = 0;
    loop {
        sink.add(ticks);
        sleep(ONE_SECOND);
        if ticks == i32::MAX {
            break;
        }
        ticks += 1;
    }
    Ok(())
}

在 Dart 中使用它:

import 'package:flutter/material.dart';
import 'ffi.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late Stream<int> ticks;

  @override
  void initState() {
    super.initState();
    ticks = api.tick();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text("Time since starting Rust stream"),
            StreamBuilder<int>(
              stream: ticks,
              builder: (context, snap) {
                final style = Theme.of(context).textTheme.headline4;
                final error = snap.error;
                if (error != null)
                  return Tooltip(
                      message: error.toString(),
                      child: Text('Error', style: style));

                final data = snap.data;
                if (data != null) return Text('$data second(s)', style: style);

                return const CircularProgressIndicator();
              },
            )
          ],
        ),
      ),
    );
  }
}

本文正在参加「金石计划 . 瓜分6万现金大奖」