使用 Dart FFI 在桌面 Flutter 应用程序中集成 C 库 | 作者:Igor Kharakhordin | Flutter 社区 | 2021 年 12 月 | 中型
创建。2021年12月16日下午3点42分 URL: medium.com/flutter-com…
图片来源:Maksym Zakharyak on Unsplash
这篇文章是我之前的文章的后续,我在那里讨论了在Android和iOS上的移动Flutter应用程序中整合一个C++ OpenCV库。
已经过去一年了,从那时起事情发生了变化。一个框架的新的第二个主要版本已经发布了。Flutter 2.0不仅引入了Dart FFI的稳定版本和期待已久的null-safety支持,而且还引入了桌面支持,所以现在Flutter应用程序可以在另外3个平台上构建--Windows、macOS和Linux。现在连Canonical自己都认可Flutter了。
再说一遍,这篇文章的目的是帮助Flutter开发者设置他们的项目,以使用本地的C或C++库,并编写一个使用它的单一代码库。作为一个例子,文章描述了将OpenCV库添加到Flutter应用项目中的过程。但是这一次--都是桌面的。Windows、macOS和Linux。
这到底是怎么回事?
如果你不知道Dart FFI是什么,为什么需要它,请参考我以前的文章。在这篇文章中,我将假设你已经建立并准备好了一个项目(至少是Dart部分),并且将使用相同的C++源文件。或者你可以直接使用git资源库。
创建一个插件
首先,确保你已经为Flutter桌面的框架和项目都设置好了。相关说明可在官方网站上找到。
一个桌面Flutter插件必须为每个平台单独创建。让我们来创建它们。
flutter create --template=plugin --platforms=windows native_opencv_windows
flutter create --template=plugin --platforms=linux native_opencv_linux
flutter create --template=plugin --platforms=macos native_opencv_macos
现在项目的结构应该是这样的。
我们的主插件现在可以包括每个平台的插件,所以在一个项目中包括一个主插件会导致包括每个平台的插件。这种插件被称为认可的联合插件。为了使其发挥作用,请更新主插件(native_opencv)的一个pubspec。
flutter:
plugin:
platforms:
android:
package: com.example.native_opencv
pluginClass: NativeOpencvPlugin
ios:
pluginClass: NativeOpencvPlugin
macos:
default_package: native_opencv_macos
linux:
default_package: native_opencv_linux
windows:
default_package: native_opencv_windows
在Windows上设置该插件
OpenCV for Windows是以一个自解压的.exe压缩包的形式发布的,其中包含了预先建立的.dll库。解压后,设置一个环境变量OpenCV_DIR,指向库的build文件夹。
(If you use CMD) setx -m OpenCV_DIR D:\src\opencv\build
(If you use PS)[System.Environment]::SetEnvironmentVariable('OpenCV_DIR','D:\src\opencv\build')
最初,我们的C++源文件在native_opencv\ios\Classes\native_opencv.cpp。现在我们要通过建立一个硬链接来使用同一个文件。
(CMD) mklink /H native_opencv_windows\windows\native_opencv.cpp native_opencv\ios\Classes\native_opencv.cpp
(PS) New-Item -ItemType HardLink -Name native_opencv_windows\windows\native_opencv.cpp -Target native_opencv\ios\Classes\native_opencv.cpp
将链接也添加到*.gitignore*文件中。
早些时候,我使用image_picker包来选择手机上的图片。由于它还不支持台式机,我将使用file_picker包来显示一个选择图像文件的本地对话框。
现在我们将设置项目的构建。就像在Android上一样,cmake用于构建项目,所以打开位于native_opencv_windows\windows\CMakeLists.txt的*CMakeLists.txt。
首先,定义.dll库的名称,这样我们就可以在以后的构建中包含它们。你可以在opencv\build\x64\vc15\bin文件夹中找到DLLs。
注意:必须添加的行是黑体的。
添加C++源代码文件(准确地说,是一个硬链接)。
add_library(${PLUGIN_NAME} SHARED
"native_opencv_windows_plugin.cpp"
"native_opencv.cpp"
)
为了区分不同的构建配置以选择正确的库,请添加这些行。
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_compile_definitions(${PLUGIN_NAME} PRIVATE
OpenCV_DLL_NAME=
$<$<CONFIG:Debug>:${OpenCV_DEBUG_DLL_NAME}>
$<$<CONFIG:Profile>:${OpenCV_RELEASE_DLL_NAME}>
$<$<CONFIG:Release>:${OpenCV_RELEASE_DLL_NAME}>
)
最后,将OpenCV库添加到目标中。
target_compile_definitions(${PLUGIN_NAME} PRIVATE
OpenCV_DLL_NAME=
$<$<CONFIG:Debug>:${OpenCV_DEBUG_DLL_NAME}>
$<$<CONFIG:Profile>:${OpenCV_RELEASE_DLL_NAME}>
$<$<CONFIG:Release>:${OpenCV_RELEASE_DLL_NAME}>
)
set("OpenCV_DIR" $ENV{OpenCV_DIR})
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
target_link_libraries(${PLUGIN_NAME} PRIVATE ${OpenCV_LIBS} flutter flutter_wrapper_plugin)
# List of absolute paths to libraries that should be bundled with the plugin
set(native_opencv_windows_bundled_libraries
""
"${_OpenCV_LIB_PATH}/${OpenCV_DLL_NAME}"
PARENT_SCOPE
)
在Dart部分,打开相应的动态.dll库。
// Getting a library that holds needed symbols
ffi.DynamicLibrary _openDynamicLibrary() {
if (Platform.isAndroid) {
return ffi.DynamicLibrary.open('libnative_opencv.so');
} else if (Platform.isWindows) {
return ffi.DynamicLibrary.open("native_opencv_windows_plugin.dll");
}
return ffi.DynamicLibrary.process();
}
ffi.DynamicLibrary _lib = _openDynamicLibrary();
打开动态.dll库
Windows插件的设置已经完成,但如果我们现在运行编译,就会失败。原因是Windows上使用的是另一个编译器--MSVC。它不支持一些在GNU编译器中可用的东西。
MSVC中没有__attribute__,必须用__declspec(dllexport)代替,所以函数被导出到一个.dll文件中。进入native_opencv.cpp,创建一个FUNCTION_ATTRIBUTE宏,并将其用于所需函数。
...
#if defined(__GNUC__)
// Attributes to prevent 'unused' function from being removed and to make it visible
#define FUNCTION_ATTRIBUTE __attribute__((visibility("default"))) __attribute__((used))
#elif defined(_MSC_VER)
// Marking a function for export
#define FUNCTION_ATTRIBUTE __declspec(dllexport)
#endif
...
FUNCTION_ATTRIBUTE
const char* version() {
return CV_VERSION;
}
FUNCTION_ATTRIBUTE
void process_image(char* inputImagePath, char* outputImagePath) {
...
为函数标记创建一个宏
另外,用简单的printf打印到调试输出是不行的--就像在Android中一样。到调试器的信息是由OutputDebugStringA函数代替发送的。创建一个宏来检测WinAPI,包括windows.h,并调用OutputDebugStringA。
#define IS_WIN32 defined(WIN32) || defined(_WIN32) || defined(__WIN32)
#ifdef __ANDROID__
#include <android/log.h>
#endif
#ifdef IS_WIN32
#include <windows.h>
#endif
...
void platform_log(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
#ifdef __ANDROID__
__android_log_vprint(ANDROID_LOG_VERBOSE, "ndk", fmt, args);
#elif defined(IS_WIN32)
char *buf = new char[4096];
std::fill_n(buf, 4096, '\0');
_vsprintf_p(buf, 4096, fmt, args);
OutputDebugStringA(buf);
delete[] buf;
#else
vprintf(fmt, args);
#endif
va_end(args);
}
修复Windows中的调试打印
然后就可以开始了。
在Windows上运行的应用程序
在Visual Studio中的调试输出
在macOS上设置该插件
OpenCV发布的版本并不包含用于macOS和Linux的预建库,这意味着我们需要自己构建它们。首先,下载OpenCV的源代码或克隆其仓库。在位于opencv/platforms/apple/readme.md的readme文件中,有关于如何为macOS构建Xcode框架的说明。简而言之,只要像这样运行python脚本。
python3 ~/dev/lib/opencv/platforms/apple/build_xcframework.py --macos_archs=x86_64,arm64 --build_only_specified_archs --out ./build_xcframework
从iOS插件创建一个硬链接到native_opencv.cpp文件。
ln native_opencv/ios/Classes/native_opencv.cpp native_opencv_macos/macos/Classes/native_opencv.cpp
之后,说明与iOS类似。将opencv2.xcframework复制(或创建一个符号链接)到native_opencv_macos/macos。编辑native_opencv_macos.podspec。
s.platform = :osx, '10.11'
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
s.swift_version = '5.0'
# telling CocoaPods not to remove framework
s.preserve_paths = 'opencv2.xcframework'
# telling linker to include opencv2 framework
s.xcconfig = { 'OTHER_LDFLAGS' => '-framework opencv2' }
# including OpenCV framework
s.vendored_frameworks = 'opencv2.xcframework'
# including native framework
s.frameworks = 'AVFoundation', 'Accelerate', 'OpenCL'
# including C++ library
s.library = 'c++'
end
与iOS的podspec唯一不同的是,增加了两个本地框架。Accelerate和OpenCL。
如果你为iOS设置了应用程序,不需要对native_opencv.dart和native_opencv.cpp文件进行修改。如果你不这样做--只要按照我以前的文章中关于iOS的说明。
在MacOS上运行的应用程序
调试输出信息
在Linux上设置该插件
和macOS一样,OpenCV版本不包含用于Linux的预建库。关于如何建立它们的说明可在OpenCV官方网站上找到。
设置一个环境变量OpenCV_DIR,通过将其添加到".XXXrc "文件(例如.bashrc)中,指向库的build文件夹。
export OpenCV_DIR=/home/west/dev/lib/opencv/build
从iOS插件创建一个硬链接到native_opencv.cpp文件。
ln native_opencv/ios/Classes/native_opencv.cpp native_opencv_linux/linux/native_opencv.cpp
现在编辑native_opencv_linux/linux/CMakeLists.txt。
...
add_library(${PLUGIN_NAME} SHARED
"native_opencv_linux_plugin.cc"
"native_opencv.cpp"
)
set("OpenCV_DIR" $ENV{OpenCV_DIR})
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
apply_standard_settings(${PLUGIN_NAME})
set_target_properties(${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
target_include_directories(${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include")
target_link_libraries(${PLUGIN_NAME} PRIVATE flutter)
target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
target_link_libraries(${PLUGIN_NAME} PRIVATE ${OpenCV_LIBS})
...
在Ubuntu上运行的应用程序
调试输出信息
在VSCode中调试Linux/Windows上的本地代码
像往常一样打开项目。通过简单地启动一个应用程序生成一个build文件夹。安装CMake工具和C/C++扩展。
调用CMake: Configure动作。它会告诉你在根目录上没有找到CMakeLists.txt文件。按Locate并从一个平台的文件夹中选择CMakeLists.txt文件(例如,linux/CMakeLists.txt)。
现在创建一个新的C/C++配置。对于Linux,它是 "gdb Launch",对于Windows,它是 "Windows "。配置文件*.vscode/launch.json*有一个叫做 "*program "*的属性。把它指向一个可执行文件。
(Windows)
${workspaceFolder}/build/windows/runner/Debug/flutter_native_opencv.exe
(Linux)
${workspaceFolder}/build/linux/x64/debug/bundle/native_opencv
{
"version": "0.2.0",
"configurations": [
{
"name": "(Windows) Launch",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/build/windows/runner/Debug/flutter_native_opencv.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${fileDirname}",
"environment": [],
"console": "externalTerminal"
},
{
"name": "(gdb) Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/linux/x64/debug/bundle/native_opencv",
"args": [],
"stopAtEntry": false,
"cwd": "${fileDirname}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
在Linux上,通过调用cmake install确保你在构建OpenCV后已经安装了它。为头文件创建一个符号链接。
sudo ln -s /usr/local/include/opencv4/opencv2 /usr/local/include/opencv2
在Windows上,将OpenCV的include文件夹添加到*.vscode/c_cpp_properties.json.中的includePath参数中,这里是两个平台的最终c_cpp_properties.json*。
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**",
"D:/src/opencv/build/include"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"windowsSdkVersion": "10.0.19041.0",
"compilerPath": "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\VC\\Tools\\MSVC\\14.26.28801\\bin\\Hostx64\\x64\\cl.exe",
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "windows-msvc-x64",
"configurationProvider": "ms-vscode.cmake-tools"
},
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [],
"compilerPath": "/usr/bin/clang",
"cStandard": "c17",
"cppStandard": "c++14",
"intelliSenseMode": "linux-clang-x64",
"configurationProvider": "ms-vscode.cmake-tools"
}
],
"version": 4
}
运行配置,一切都应该正常了。OpenCV的文档、断点、调试输出等等。
在Linux中进行调试
在Windows中调试
请注意,要应用对.cpp文件的修改,你必须重新构建/运行Flutter项目。
借助Dart FFI的力量,我们能够在5个不同的平台上共享同一个C++源代码文件和同一个库。 谢谢你的阅读,并欢迎你留下任何反馈。该应用程序的源代码可在此获得。