[Flutter翻译]用Flutter构建和部署本地C库

1,541 阅读7分钟

原文地址:medium.com/flutter-com…

原文作者:medium.com/@jpnurmi

发布时间:2020年10月12日 - 6分钟阅读

外来函数接口(Foreign Function Interface)又称FFI,是一种整洁的机制,通过利用原生C库的力量来为你的Dart和Flutter应用增压。 多亏了dart:fi,人们不需要在Dart中重新发明轮子,而是可以在那里抓取一个现成的C库,并直接从Dart中调用C API。


尽管dart:ffi还处于测试阶段,但它已经开始流行了。pub.dev上已经有几十个基于FFI的包。FFI可以说在Dart和Flutter生态系统中扮演着重要的角色,因为它使得利用现有的本地库对接硬件、网络、数据库以及其他无穷无尽的东西成为可能,而不需要花费时间和精力将它们移植到Dart上。


当从Dart调用C时,需要一些管道来连接两个世界。本质上,有两件事是需要的;1)C API必须绑定到Dart,2)C库和它的符号必须以某种方式提供。有一个很好的 ffigen 工具可以通过从 C 头文件生成 Dart 绑定来帮助解决前者。至于后者,这个过程取决于库和选择的部署方法。

考虑到以下几类库。

  • 静态库
  • 共享系统库
  • 共享的非系统库

静态库和共享系统库是最容易处理的。静态库必须在构建时被链接到,这使得在运行时可以通过DynamicLibrary.executable()来访问它们。共享系统库不需要链接--在运行时可以通过[DynamicLibrary.open](https://api.dart.dev/stable/2.10.1/dart-ffi/DynamicLibrary/DynamicLibrary.open.html)(String name)简单地传递库名来访问它们。鉴于系统库在动态加载器的搜索路径中是可用的,所以不需要指定路径。对于最后一类,即共享的非系统库,事情就变得有点复杂了。这些库不一定在搜索路径中可用,所以DynamicLibrary可能需要帮助在某些平台上定位库。

尽管共享非系统库的事情往往会变得有点复杂,但它们有一个重要的用例。在这里必须声明,我不是专门研究开源许可证的律师,但最突出的案例是在闭源的专有应用程序中利用弱拷贝许可证(如LGPL)授权的库。由于其他两类情况相当直接,所以文章剩下的部分主要介绍如何构建和部署专门的共享非系统库。


Flutter是一个跨平台的框架,总的来说,让它真的很容易针对多个平台。然而,当为每个目标平台构建和部署额外的C库时,Flutter无法将其抽象化--你需要遵守每个目标平台的规则。即使可以使用Maven仓库等服务发布预建库,但作为Flutter应用或插件的一部分,在飞行中构建一个原生库往往更省事。请注意,如果C库的大小是合理的,而且构建时间不会太长的话,这种方法特别好用。

说到构建C库,最常见的构建系统是AutotoolsCMake。在老的C库中,Autotools比较常见,而CMake则是在过去的半年多时间里成为流行的选择。Autotools的工作往往比较繁琐,可能会涉及到例如为某些目标平台手动预生成config.h文件,但基于CMake的库可以直接与Flutter集成。

下面的章节重点介绍了CMake方面的内容,并介绍了在每个平台上集成C库的步骤。这些内容在Flutter文档中的Binding to native code using dart:ffi页面中也有部分涉及,尽管该文档还没有涵盖还在alpha阶段的Windows和Linux桌面平台。

安卓平台

在Android上构建本地C库的首要前提是安装Android NDK。NDK提供了为Android构建原生库所需的一切。其余的由Gradle处理,它内置了对外部原生构建的支持。使用Gradle和CMake构建本地库是一个问题,在build.gradle中指定一个路径到库的CMakeLists.txt

diff --git a/android/build.gradle b/android/build.gradle
index 3b85c9f..cdec0d1 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -36,6 +36,12 @@ android {
     lintOptions {
         disable 'InvalidPackage'
     }
+
+    externalNativeBuild {
+        cmake {
+            path "path/to/CMakeLists.txt"
+        }
+    }
 }
 
 dependencies {

在Android上集成外部库

根据库的不同,可能需要指定编译器选项等。基本上,仅externalNativeBuild块就足以在Android上构建和部署一个本地库。该库是为每个目标架构自动构建的,在生成的APK中的lib-目录看起来像这样。

lib/
├──arm64-v8a
│ └── libfoo.so
├──armeabi-v7a
│ └── libfoo.so
├── x86
│ └── libfoo.so
└── x86_64
    └── libfoo.so

因此,这足以让[DynamicLibrary.open](https://api.dart.dev/stable/2.10.1/dart-ffi/DynamicLibrary/DynamicLibrary.open.html)('libfoo.so')不需要指定路径就能工作。这就是Android的全部内容。

Windows系统

在Windows上,Flutter使用CMake来构建应用程序运行器和插件的原生位。要将额外的本地库集成到构建系统中,只需在windows/CMakeLists.txt中指定它即可。

diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt
index 2140414..99f6ffc 100644
--- a/windows/CMakeLists.txt
+++ b/windows/CMakeLists.txt
@@ -17,6 +17,8 @@ target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin)
 
 # List of absolute paths to libraries that should be bundled with the plugin
 set(flutter_foo_plugin_bundled_libraries
-  ""
+  "$<TARGET_FILE:foo>"
   PARENT_SCOPE
 )
+
+add_subdirectory(path/to/foo)

在Windows上集成外部库

注意,$<TARGET_FILE:foo>中的名称必须与path/to/foo/CMakeLists.txt中的外部库目标名称相匹配。在任何情况下,在flutter_xxx_bundled_libraries中指定库,都会使Flutter将生成的DLL部署到应用程序可执行文件旁边,这又允许用DynamicLibrary.open('libfoo.dll')打开库,而不必指定路径。

在Linux下

在Linux上集成外部库的步骤与Windows上的步骤是一样的,只是在运行时定位库需要一些额外的步骤。下面是一个利用Flutter插件代码解析正确路径的例子,并使用环境变量将信息传递给Dart。

diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt
index 0bac83e..6ec6ea1 100644
--- a/linux/CMakeLists.txt
+++ b/linux/CMakeLists.txt
@@ -18,6 +18,8 @@ target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
 
 # List of absolute paths to libraries that should be bundled with the plugin
 set(flutter_foo_plugin_bundled_libraries
-  ""
+  "$<TARGET_FILE:foo>"
   PARENT_SCOPE
 )
+
+add_subdirectory(path/to/foo)
diff --git a/linux/flutter_foo_plugin_plugin.cc b/linux/flutter_foo_plugin_plugin.cc
index 03a6b5f..c490270 100644
--- a/linux/flutter_foo_plugin_plugin.cc
+++ b/linux/flutter_foo_plugin_plugin.cc
@@ -3,6 +3,7 @@
 #include <flutter_linux/flutter_linux.h>
 #include <gtk/gtk.h>
 #include <sys/utsname.h>
+#include <glib.h>
 
 #define FLUTTER_FOO_PLUGIN(obj) \
   (G_TYPE_CHECK_INSTANCE_CAST((obj), flutter_foo_plugin_get_type(), \
@@ -51,10 +52,33 @@ static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call,
   flutter_foo_plugin_handle_method_call(plugin, method_call);
 }
 
+// Gets the directory the current executable is in, borrowed from:
+// https://github.com/flutter/engine/blob/master/shell/platform/linux/fl_dart_project.cc#L27
+//
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in https://github.com/flutter/engine/blob/master/LICENSE.
+static gchar* get_executable_dir() {
+  g_autoptr(GError) error = nullptr;
+  g_autofree gchar* exe_path = g_file_read_link("/proc/self/exe", &error);
+  if (exe_path == nullptr) {
+    g_critical("Failed to determine location of executable: %s",
+               error->message);
+    return nullptr;
+  }
+
+  return g_path_get_dirname(exe_path);
+}
+
 void flutter_foo_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
   FlutterFooPlugin* plugin = FLUTTER_FOO_PLUGIN(
       g_object_new(flutter_foo_plugin_get_type(), nullptr));
 
+  g_autofree gchar* executable_dir = get_executable_dir();
+  g_autofree gchar* libfoo_path =
+      g_build_filename(self_exe, "lib", "libfoo.so", nullptr);
+  setenv("LIBFOO_PATH", libfoo_path, 0);
+
   g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
   g_autoptr(FlMethodChannel) channel =
       fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),

Flutter:在Linux上集成一个基于CMake的外部库。

Flutter的构建系统将外部库捆绑到应用程序可执行文件旁边的lib子目录中。

lib/
├── libflutter_linux_gtk.so。
├── libflutter_foo_plugin.so。
└── libfoo.so

请注意,要在运行时定位库,不能依赖当前的工作目录,它可以是任何东西,这取决于应用程序的启动方式。它必须相对于应用程序的可执行文件进行解析。上图中,我们使用从Flutter引擎的Linux shell中借来的一个帮助函数来完成这个任务。一旦我们解决了正确的位置,我们就可以使用环境变量来查找Dart中的库。DynamicLibrary.open(Platform.environment['LIBFOO_PATH'])

iOS & macOS

最后是果味平台。根据Flutter的文档,应该可以在Xcode中指定外部原生库。不过我不太清楚这一点如何转化为Flutter为我们设置的构建系统,而且还有一个恼人的限制,CocoaPods不允许在podspec文件上面包含源码。

与其让这个限制决定如何构建你的仓库和在哪里放置第三方代码,不如选择为外部库制作一个单独的podspec,并将其作为Flutter应用或插件的依赖关系包含进去。CocoaPods上有很多纯C库,可以作为一个起点。剩下的创建podspec的工作就留给读者去做了。

一旦你有了外部库的Pod,它就可以作为一个依赖项包含在Flutter应用或插件的podspec中。构建系统会根据情况对其进行捆绑。为了帮助在Dart中定位库,我们可以在Swift中解析我们可以访问捆绑库的路径,并将信息作为环境变量传递,类似于我们之前在Linux上的做法。

diff --git a/macos/Classes/FlutterFooPlugin.swift b/macos/Classes/FlutterFooPlugin.swift
index 1c11b9a..64fb4ef 100644
--- a/macos/Classes/FlutterFooPlugin.swift
+++ b/macos/Classes/FlutterFooPlugin.swift
@@ -1,8 +1,11 @@
 import Cocoa
 import FlutterMacOS
+import Foundation
 
 public class FlutterFooPlugin: NSObject, FlutterPlugin {
   public static func register(with registrar: FlutterPluginRegistrar) {
+    setenv("LIBFOO_PATH", Bundle.main.privateFrameworksPath! + "/libfoo.framework/libfoo", 0);
+
     let channel = FlutterMethodChannel(name: "flutter_foo_plugin", binaryMessenger: registrar.messenger)
     let instance = FlutterFooPlugin()
     registrar.addMethodCallDelegate(instance, channel: channel)
diff --git a/macos/flutter_foo_plugin.podspec b/macos/flutter_foo_plugin.podspec
index c7997fa..a7bff4a 100644
--- a/macos/flutter_foo_plugin.podspec
+++ b/macos/flutter_foo_plugin.podspec
@@ -15,6 +15,7 @@ A new flutter plugin project.
   s.source           = { :path => '.' }
   s.source_files     = 'Classes/**/*'
   s.dependency 'FlutterMacOS'
+  s.dependency 'libfoo'
 
   s.platform = :osx, '10.11'
   s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }

在macOS上集成外部库


就是这样!这就是我的第一个FFI包。希望这篇文章能帮助你创建可以开箱即用的FFI包,这样用户就不需要想办法构建和部署本地的依赖关系,我的第一个FFI包就是这种情况。


通过www.DeepL.com/Translator (免费版)翻译