[Flutter翻译]使用 Dart FFI 在桌面 Flutter 应用程序中集成 C 库

1,581 阅读7分钟

使用 Dart FFI 在桌面 Flutter 应用程序中集成 C 库 | 作者:Igor Kharakhordin | Flutter 社区 | 2021 年 12 月 | 中型

创建。2021年12月16日下午3点42分 URL: medium.com/flutter-com…

image.png

图片来源: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中的调试打印

然后就可以开始了。

image.png

在Windows上运行的应用程序

image.png

在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唯一不同的是,增加了两个本地框架。AccelerateOpenCL

如果你为iOS设置了应用程序,不需要对native_opencv.dart和native_opencv.cpp文件进行修改。如果你不这样做--只要按照我以前的文章中关于iOS的说明。

image.png

在MacOS上运行的应用程序

image.png

调试输出信息

在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})
...

image.png

在Ubuntu上运行的应用程序

image.png

调试输出信息

在VSCode中调试Linux/Windows上的本地代码

像往常一样打开项目。通过简单地启动一个应用程序生成一个build文件夹。安装CMake工具C/C++扩展

image.png

调用CMake: Configure动作。它会告诉你在根目录上没有找到CMakeLists.txt文件。按Locate并从一个平台的文件夹中选择CMakeLists.txt文件(例如,linux/CMakeLists.txt)。

image.png

image.png

image.png

现在创建一个新的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的文档、断点、调试输出等等。

image.png

在Linux中进行调试

image.png

在Windows中调试

请注意,要应用对.cpp文件的修改,你必须重新构建/运行Flutter项目。


借助Dart FFI的力量,我们能够在5个不同的平台上共享同一个C++源代码文件和同一个库。 谢谢你的阅读,并欢迎你留下任何反馈。该应用程序的源代码可在此获得。

github.com/westracer/f…