最近一直使用 Rust 开发程序,就研究了一下如何使用 rust 进行桌面程序的开发,发现有两个比较流行的方法,其一是使用 Tauri,基于 WebVie;其二则是使用 Flutter,因为担心 web 的性能问题,所以研究了一下 Flutter。在这里记录一下基本方法。
Flutter 的示例小程序是一个计数器,通过点击按钮来使屏幕上的数字自增。本文的目的就是改变这个小程序,使得自增这个过程在 rust 中完成。
首先我们先来写 RUST 后端,使用 cargo new
创建一个新的 RUST 程序。
cargo new rust-ffi-backend
接下来我们来写点代码,编辑 lib.rs
文件,实现调用一次函数,全局变量自增,为了和 Flutter 的示例程序做出区别,我们调用一次自增 2。
static mut COUNT: u32 = 0;
#[no_mangle]
pub unsafe extern "C" fn count_add_self() -> u32 {
COUNT += 2;
COUNT
}
现在添加需要编辑 Cargo.toml
文件,将目标生成为符合 C ABI 的动态链接库。
[lib]
name = "rust_ffi_backend"
crate-type = ["cdylib"]
我们先来编译一下我们的 RUST 程序:
cargo build --release
接下来我们可以使用一个开源项目 cbindgen,他会帮助我们生成 C 语言的头文件。
首先我们安装它:
cargo install --force cbindgen
注意 cbingen 可能依赖于 LLVM 等许多其他程序,如果安装失败请详细看提示,安装缺少的组件。
然后在我们的项目目录下创建一个 cbindgen.toml
文件,比如它可能是下面这个样子:
# This is a template cbindgen.toml file with all of the default values.
# Some values are commented out because their absence is the real default.
#
# See https://github.com/eqrion/cbindgen/blob/master/docs.md#cbindgentoml
# for detailed documentation of every option here.
language = "C"
############## Options for Wrapping the Contents of the Header #################
header = "/* Text to put at the beginning of the generated file. Probably a license. */"
no_includes = true
after_includes = "typedef unsigned int uint32_t;"
############################ Code Style Options ################################
braces = "SameLine"
line_length = 100
tab_width = 4
documentation = true
documentation_style = "auto"
documentation_length = "full"
line_endings = "LF" # also "CR", "CRLF", "Native"
############################# Codegen Options ##################################
style = "both"
sort_by = "Name" # default for `fn.sort_by` and `const.sort_by`
usize_is_size_t = true
[export]
include = []
exclude = []
item_types = []
renaming_overrides_prefixing = false
[fn]
rename_args = "None"
args = "auto"
sort_by = "Name"
[struct]
rename_fields = "None"
derive_constructor = false
derive_eq = false
derive_neq = false
derive_lt = false
derive_lte = false
derive_gt = false
derive_gte = false
[enum]
rename_variants = "None"
add_sentinel = false
prefix_with_name = false
derive_helper_methods = false
derive_const_casts = false
derive_mut_casts = false
derive_tagged_enum_destructor = false
derive_tagged_enum_copy_constructor = false
enum_class = true
private_default_tagged_enum_constructor = false
[const]
allow_static_const = true
allow_constexpr = false
sort_by = "Name"
[macro_expansion]
bitflags = false
############## Options for How Your Rust library Should Be Parsed ##############
[parse]
parse_deps = false
exclude = []
clean = false
extra_bindings = []
[parse.expand]
crates = []
all_features = false
default_features = true
features = []
现在我们尝试将他转换为 C 语言的头文件吧, 使用:
cbindgen --config cbindgen.toml --crate rust-ffi-backend --output target/release/rust-ffi-backend.h
可以在 target/release/rust-ffi-backend-ffi.h
文件内看到 cbindgen 问我们生成的 C 语言头文件了,他可能长下面这个样子:
/* Text to put at the beginning of the generated file. Probably a license. */
typedef unsigned int uint32_t;
uint32_t count_add_self();
现在我们来完成基于 Flutter 的前端桌面程序,首先新创建一个 flutter 工程:
flutter create flutter_frontend
打开它,我们就能看到默认的计数器小程序了,先不着急修改程序,我们先把我们的 RUST 后端导入进来。
新建一个名叫 rust-ffi
的文件夹存放 RUST 的库和头文件:
mkdir rust-ffi
将刚才生成的文件都拷贝过来:
cp ../rust-ffi-backend/target/release/rust-ffi-backend.so rust-ffi/
cp ../rust-ffi-backend/target/release/rust-ffi-backend.h rust-ffi/
现在,我们要依赖 dart 官方的一个名为 ffigen 的程序,它可以帮助我们把 C ABI 的头文件生成 dart 源程序。
首先编辑 pubspec.yaml
文件,在最后添加下面内容:
ffigen:
output: 'lib/rust-ffi-backend.dart'
headers:
entry-points:
- 'rust-ffi/rust-ffi-backend.h'
然后使用工具生成 dart 文件:
dart run ffigen
dart 的 ffigen 工具同样需要依赖 LLVM 等程序,如果报错请详细查看错误原因,安装必要的组件。
这是我们看到在 lib
文件夹下应该生成了我们的 rust-ffi-backend.dart
文件,它可能是长这样的:
// AUTO GENERATED FILE, DO NOT EDIT.
//
// Generated by `package:ffigen`.
import 'dart:ffi' as ffi;
class NativeLibrary {
/// Holds the symbol lookup function.
final ffi.Pointer Function(String symbolName)
_lookup;
/// The symbols are looked up in [dynamicLibrary].
NativeLibrary(ffi.DynamicLibrary dynamicLibrary)
: _lookup = dynamicLibrary.lookup;
/// The symbols are looked up with [lookup].
NativeLibrary.fromLookup(
ffi.Pointer Function(String symbolName)
lookup)
: _lookup = lookup;
int count_add_self(
ffi.Pointer> process,
) {
return _count_add_self(
process,
);
}
late final _count_add_selfPtr = _lookup<
ffi.NativeFunction<
ffi.Uint32 Function(
ffi.Pointer<
ffi.NativeFunction>)>>(
'count_add_self');
late final _count_add_self = _count_add_selfPtr.asFunction<
int Function(
ffi.Pointer>)>();
看到 ffigen 为我们生成了一个 NativeLibrary
类,它拥有一个 count_add_self
方法,对应就是我们 RUST 中的同名方法。
接下来我们就来使用它。
打开 main.dart
文件,找到 _MyHomePageState
类,首先引入我们的库,将我们的库作为一个类的成员使用,在 class
内添加如下内容:
NativeLibrary nativelib =
NativeLibrary(DynamicLibrary.open('rust-ffi/rust-ffi-backend.so'));
添加好以后,Flutter 就会在每次初始化 _MyHomePageState
类是加载我们的名为 rust-ffi-backend.so
动态链接库了。
找到 _incrementCounter
这个函数,可以看到如下内容:
void _incrementCounter() async {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
将其中的 _counter++
这行代码,替换为如下代码:
_counter = nativelib.count_add_self();
这样,在 setState
方法中就不会在直接将计数自增,而是会调用我们 RUST 中的方法将 RUST 中的全局变量 COUNT
自增,再返回 COUNT
的值,也就是说,实际上代码到 RUST 中兜了一圈。
现在运行我们的 Flutter 工程:
flutter run
可以看到,每当我们点击一下,计数自增 2, 说明是调用到了 RUST 的函数,实现了 Flutter 和 RUST 的结合。
先写到这吧,后面补上。