【小声团队】 Flutter Desktop 实战 - 我们是如何在Flutter中使用C++代码的

本文由小声团队出品,小声团队是一个专注于音频&音乐技术的初创团队,深度使用Flutter构建跨平台应用,希望与大家一起共同探索Flutter在桌面端&移动端的可能性。

背景

作为一个音频工具软件,势必会涉及到大量的计算(音频效果器,合成器生成等),并且具备实时性要求。此类场景对于性能有极高的敏感度,因此我们把所有的核心逻辑使用C++封装成一个引擎,然后在Flutter中进行调用。本文主要介绍我们Flutter与原生通讯的发展过程。

原生通讯架构

在架构上,我们按照分层的方式组织起与原生代码的交互,核心思想是使用C语言来包装C++与Rust的功能,形成线程安全的原子性API(比如 engine_load_midi_file(const char* file) 这样的接口来加载MIDI文件)。

image.png

  • 最底层是大部分C++与小部分Rust组成的引擎层
  • 在这之上使用C语言封装一个个原子性的C接口
  • 大部分的C语言接口使用Flutter Plugin + Method Channel的方式进行交互,另外一部分使用FFI进行交互。
  • 最顶层则是用Dart将原生操作全部封装起来,Flutter业务层只会与这一层Wrapper交互。

FFI

我们最初全部的交互都由FFI来完成,Dart代码则全部使用Package - ffigen自动生成。

这样比Flutter Plugin的好处是完全不需要写原生代码,而且调用也是同步的,操作的体验是很好的,但是这里出现了新的问题 - Dart是一个VM语言,它有自己独立的线程工作,而相当多的原生 UI API(NSWindow*)只能在Main Thread中工作,这样就造成无法直接通过FFI的方式来调度包含此类API的接口,另一个方面,FFI是直接工作在Dart线程,则会造成线程阻塞,对于部分稍微耗时的操作就会严重影响Dart VM的执行。

因此,我们将大部分的交互迁移到了Flutter Plugin

Flutter Plugin

在前面我们提到了我们的C接口层,无论是FFI还是Flutter Plugin 我们都是依赖的这一层来进行调度,在FFI中是直接把C接口生成Dart函数,而在Flutter Plugin中,我们需要手写Method Channel的原生代码来进行调度,为了继续享受到类似ffigen这样一键生成到处使用的优势,我们内部研发了一个基于C Header的代码生成器,自动生成不同平台的Plugin 代码,假设我们有接口

EXPORT_API int engine_load_midi_file(const char* file);

复制代码

通过解析语法,得到了AST


{
 "name" : "engine_load_midi_file",
 "return_type" : "int",
 "arguments" : [
     {
         "type" : "const char*"
     }
 ]   
}
复制代码

得到了AST并不能直接进行代码生成,因为不同平台的类型是不一样的,因此我们要主要类型转换,Flutter 提供了类型映射表来定义Dart与原生类型的关系, Dart与原生的类型映射

同理,我们在进行代码生成的时候也需要把C语言类型与对应平台的类型进行映射,比如int 在Dart中是Int,而在SWift中则可能是Int32,我们需要根据自己的实际情况进行类型映射,之后就可以很方便的利用这个AST自动生成不同平台的代码了,

比如,在 Swift 里面就是

let call_args = call.arguments as! [Any]
 switch method.call {
    case "engine_load_midi_file" :
        let ret = engine_load_midi_file(call_args[0] as! String);
        result(ret);
        break;
 }
复制代码

其他平台类似的进行自动代码生成。

Flutter Plugin的方式最大的问题在于所有的操作都是基于queue的异步操作,这样对于一些低成本的接口会额外带来不必要的性能损失。

如何从原生代码调用Dart

这里,我们的实际应用主要是一些底层的事件通知到Dart Callback,比如设备情况发生变更了,我们需要通知到Flutter层做相应的处理,这类需求我们都是通过NativePort的方式完成的,

setupNativeCallback() {
    //定义一个port
    final interactiveCppRequests = ReceivePort()..listen(midiKeyboardCallback);
    //转换成为native port,本身是一个int64类型
    final int nativePort = interactiveCppRequests.sendPort.nativePort;
    //传递native port给底层
    await np().device_add_listener(1, nativePort);
}

midiKeyboardCallback(dynamic message) {
      // DO YOU WANT TO DO
}
复制代码

在CPP中则使用Dart_PostCObject_DL 接口进行写数据到NativePort

总结

到目前为止,我们还是比较赞同基于操作/行为/事件的方式来设计C接口。在交互上,FFI和Flutter Plugin两种方式各有利弊,甚至还有RPC等基于网络的交互方案也可以尝试。

在Flutter原生交互有什么疑问都可以留言一起探讨

猜你喜欢

转载自juejin.im/post/7047777138585370660