Flutter & Rust的一些探索与实践

1,233 阅读4分钟

I 前置介绍

Tauri

Tauri 是一个框架,用于为所有主要桌面平台构建微小、极快的二进制文件。 开发人员可以集成任何可编译为HTML、JS 和CSS 的前端框架来构建他们的用户界面。 应用程序的后端是一个来自rust 的二进制文件,带有一个前端可以与之交互的API。

官网

Flutter

Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。 Flutter可以与现有的代码一起工作。 在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的,可以用一套代码同时构建Android\iOS\Windows\Linux\macOS应用,性能可以达到原生应用一样的性能。

Flutter_rust_bridge

用于 Flutter 和 Rust 的高级内存安全绑定生成器

中文文档 英文文档

II 问题记录

macOS支持问题

不支持macOS下打开wry子窗口,错误如下

  • 代码,macOS下EventLoop 没有 new_any_thread 函数
       fn show_webview(url: String) {
           use wry::{
               application::{
                   event::{Event, StartCause, WindowEvent},
                   event_loop::{ControlFlow, EventLoop},
                   window::WindowBuilder,
               },
               webview::WebViewBuilder,
           };

           let event_loop = EventLoop::<()>::with_user_event();

           let window = WindowBuilder::new()
               .with_title("Hello World")
               // .with_window_icon(false)
               .build(&event_loop)
               .unwrap();

           let _webview = WebViewBuilder::new(window)
               .unwrap()
               .with_url(&url)
               .unwrap()
               .build()
               .unwrap();

           event_loop.run(move |event, _, control_flow| {
               *control_flow = ControlFlow::Wait;

               match event {
                   Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
                   Event::WindowEvent {
                       event: WindowEvent::CloseRequested,
                       ..
                   } => *control_flow = ControlFlow::Exit,
                   _ => (),
               }
           });
       }
       

错误

        thread 'frb_workerpool' panicked at 'On macOS, `EventLoop` must be created on the main thread!' , /Users/apanda/.cargo/registry/src/mirrors.sjtug.sjtu.edu.cn-7a04d2510079875b/tao-0.15.4/src/platform_impl/macos/event_loop.rs:119:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: FfiException(PANIC_ERROR, On macOS, `EventLoop` must be created on the main thread!, null) #0      FlutterRustBridgeBase._transformRust2DartMessage (package:flutter_rust_bridge/src/basic.dart:101:9)  #1      FlutterRustBridgeBase.executeNormal.<anonymous closure> (package:flutter_rust_bridge/src/basic.dart:49:9)  <asynchronous suspension> 

关键字 On macOS, EventLoop must be created on the main thread!

  • 亲测在Windows下正常调用
        fn show_webview(url: String) {
            use wry::{
                application::{
                    event::{Event, StartCause, WindowEvent},
                    event_loop::{ControlFlow, EventLoop},
                    window::WindowBuilder,
                },
                webview::WebViewBuilder,
            };

            let event_loop = EventLoop::<()>::new_any_thread();

            let window = WindowBuilder::new()
                .with_title("Hello World")
                // .with_window_icon(false)
                .build(&event_loop)
                .unwrap();

            let _webview = WebViewBuilder::new(window)
                .unwrap()
                .with_url(&url)
                .unwrap()
                .build()
                .unwrap();

            event_loop.run(move |event, _, control_flow| {
                *control_flow = ControlFlow::Wait;

                match event {
                    Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
                    Event::WindowEvent {
                        event: WindowEvent::CloseRequested,
                        ..
                    } => *control_flow = ControlFlow::Exit,
                    _ => (),
                }
            });
        }

Flutter_rust_bridge 启动问题

按照Flutter_rust_bridge文档集成完成后,

  • 在终端运行
       flutter run
  • 报错
       Building macOS application...                                           
       dyld[17757]: Library not loaded: '/usr/local/lib/rust.dylib'
         Referenced from: '/Users/apanda/MEGAsync/Projects/flutter/FlutterProjects/toolbox/build/macos/Build/Products/Debug/ToolBox.app/Contents/MacOS/ToolBox'
         Reason: tried: '/usr/local/lib/rust.dylib' (no such file), '/usr/lib/rust.dylib' (no such file)
       Error waiting for a debug connection: The log reader stopped unexpectedly, or never started.
       Error launching application on macOS.
  • 应用也会弹出错误提示

解决思路

1、通过XCode启动项目,启动成功后,查看控制台输出

       2022-11-18 15:19:45.196617+0800 ToolBox[23009:3084138] Metal API Validation Enabled flutter: The Dart VM service is listening on http://127.0.0.1:60419/ps3b1UPu-Fs=/ flutter: ╔════════════════════════════════════════════════════╗ flutter:                 ISAR CONNECT STARTED                 flutter: ╟────────────────────────────────────────────────────╢ flutter:         Open the link to connect to the Isar         flutter:        Inspector while this build is running.        flutter: ╟────────────────────────────────────────────────────╢ flutter:  https://inspect.isar.dev/3.0.5/ #/60419/ps3b1UPu-Fs ║  flutter: ╚════════════════════════════════════════════════════╝ flutter: 

  • 2、在项目根目录执行命令
        futter attach --debug-url=http://127.0.0.1:60419/ps3b1UPu-Fs=/ 
        # 输出如下 
        Syncing files to device macOS...                                   22.8s

        Flutter run key commands.
        r Hot reload. 🔥🔥🔥
        R Hot restart.
        h List all available interactive commands.
        d Detach (terminate "flutter run" but leave application running).
        c Clear the screen
        q Quit (terminate the application on the device).

        💪 Running with sound null safety 💪

        An Observatory debugger and profiler on macOS is available at: http://127.0.0.1:60654/6CzYD5OKqko=/
        The Flutter DevTools debugger and profiler on macOS is available at: http://127.0.0.1:9102?uri=http://127.0.0.1:60654/6CzYD5OKqko=/

又到了熟悉的终端输出与操作了。

III 总结

To Flutter_rust_bridge

总体来说,flutter_rust_bridge 给flutter端提供了 另外一个思路,对于需要同时编写macOS Android iOS Windows 以及linux 多个端的插件而言,使用 Rust 无疑是更加简便,同时也对非各端专业人员提供了一个更快的捷径去实现多端的功能。

To Taury

因为之前有实践过一个项目Angular+Tauri的桌面应用,表现比Electron更加出色,客户的接受度更高。而这次Flutter接入Rust 本意是想实践一个Flutter Desktop的webview环境。

由于在macOS下使用的Safari的内核,Windows下使用的webview2 的内核。Tauri的兼容性会差一点,但是我在使用过程中以及生产项目中暂无出现兼容性问题。

想从Tauri中借鉴一些思路,直接发现了wry的库。

而Tauri的 wry 库,更确切的说是一个调用系统的webview功能展示html控件的方式。 不是全功能的浏览器(非Electron)接入,提供了更小内存、更小打包的方式。

IV 小技巧

在使用Flutter_rust_bridge过程中,桥接方法如果暴露太多影响美观与架构 ,可以借鉴接口方式,入参 request ,返回response的逻辑来统一处理桥接函数,其他的功能在rust内部进行私有 。

例如:

// 桥接函数
pub fn request(req: Request) -> Response {
    if req.rust_type == 0 {
        return fun1(req.data);
    }
    if req.rust_type == 1 {
        return  fun2(req.data);
    }
    return Response {
        succeed: true,
        data: String::from("hello world"),
    };
}

// 具体处理业务的函数不暴露 这样生成的代码也不会包含
fn fun1()->Request{xxxx}

fn fun2()->Request{xxxx}


// 入参 Request
pub struct Request {
    pub rust_type: u32,
    pub data: String,
}

// 返回 Response
pub struct Response {
    pub succeed: bool,
    pub data: String,
}
// 自动生成的 bridge_generated.dart
abstract class Rust {
  Future<Response> request({required Request req, dynamic hint});

  FlutterRustBridgeTaskConstMeta get kRequestConstMeta;
}

class Request {
  final int rustType;
  final String data;

  Request({
    required this.rustType,
    required this.data,
  });
}

class Response {
  final bool succeed;
  final String data;

  Response({
    required this.succeed,
    required this.data,
  });
}

class RustImpl implements Rust {
  final RustPlatform _platform;
  factory RustImpl(ExternalLibrary dylib) => RustImpl.raw(RustPlatform(dylib));

  /// Only valid on web/WASM platforms.
  factory RustImpl.wasm(FutureOr<WasmModule> module) => RustImpl(module as ExternalLibrary);
  RustImpl.raw(this._platform);
  Future<Response> request({required Request req, dynamic hint}) => _platform.executeNormal(FlutterRustBridgeTask(
        callFfi: (port_) => _platform.inner.wire_request(port_, _platform.api2wire_box_autoadd_request(req)),
        parseSuccessData: _wire2api_response,
        constMeta: kRequestConstMeta,
        argValues: [req],
        hint: hint,
      ));

  FlutterRustBridgeTaskConstMeta get kRequestConstMeta => const FlutterRustBridgeTaskConstMeta(
        debugName: "request",
        argNames: ["req"],
      );
      
  xxxxx....    
  }

通过ffi处理后调用

// ffi 处理
const _base = 'rust';
final _dylib = io.Platform.isWindows ? '$_base.dll' : 'lib$_base.so';
final Rust api = RustImpl(io.Platform.isIOS || io.Platform.isMacOS ? DynamicLibrary.executable() : DynamicLibrary.open(_dylib));

// 实际调用
api.request(req: Request(rustType: 1, data: "https://blog.apanda.online"));