使用Dart FFI訪問Flutter中的本地庫

語言: CN / TW / HK

Dart是一種功能豐富的語言,有很好的文件記錄,易於學習;然而,當涉及到Flutter應用開發時,它可能缺乏一些功能。例如,可能需要一個應用程式連結到一個外部二進位制庫,或者用C、C+或Rust等低階語言編寫一些程式碼可能是有益的。

幸運的是,Flutter應用程式能夠通過dart:ffi library.com使用外國函式介面(FFI)。FFI使用一種語言編寫的程式可以呼叫用其他語言編寫的庫。例如,通過FFI,Flutter應用程式可以呼叫一個基於C語言的編譯庫,如cJSON.dylib ,或直接從Dart呼叫C語言的原始碼,如lib/utils.c

在Dart中擁有FFI互操作機制的一個核心好處是,它使我們能夠用任何編譯為C庫的語言編寫程式碼。一些例子是Go和Rust。

FFI還使我們能夠使用相同的程式碼在不同的平臺上提供相同的功能。例如,假設我們想在所有媒體中利用一個特定的開源庫,而不需要投入時間和精力在每個應用程式的開發語言(Swift、Kotlin等)中編寫相同的邏輯。一個解決方案是用C或Rust實現程式碼,然後用FFI將其暴露在Flutter應用程式中。

Dart FFI開闢了新的開發機會,特別是對於需要在團隊和專案之間共享原生程式碼或提升應用效能的專案。

在這篇文章中,我們將研究如何使用Dart FFI來訪問Flutter中的本地庫。

首先,讓我們從基本知識和基礎開始。

使用Dart FFI來訪問動態庫

讓我們從用C語言編寫一個基本的數學函式開始,我們將在一個簡單的Dart應用程式中使用它。

``` /// native/add.c

int add(int a, int b) { return a + b; }

```

一個本地庫可以靜態或動態地連結到一個應用程式中。一個靜態連結的庫被嵌入到應用程式的可執行影象中。它在應用程式啟動時載入。相比之下,動態連結的庫則分佈在應用程式中的一個單獨的檔案或資料夾中。它是按需載入的。

我們可以通過執行以下程式碼將我們的C 檔案轉換為動態庫dylib

gcc -dynamiclib add.c -o libadd.dylib

這將導致以下輸出:add.dylib

我們將按照三個步驟在Dart中呼叫這個函式。

  1. 開啟包含該函式的動態庫
  2. 查閱函式(N.B,因為C和Dart中的型別不同,我們必須分別指定)
  3. 呼叫該函式

/// run.dart import 'dart:developer' as dev; import 'package:path/path.dart'; import 'dart:ffi';void main() { final path = absolute('native/libadd.dylib'); dev.log('path to lib $path'); final dylib = DynamicLibrary.open(path); final add = dylib.lookupFunction('add'); dev.log('calling native function'); final result = add(40, 2); dev.log('result is $result'); // 42 }

這個例子說明,我們可以採用FFI在Dart應用程式中輕鬆使用任何動態庫。

現在,是時候介紹一個可以通過程式碼生成幫助生成FFI繫結的工具了。

用FFIGEN在Dart中生成FFI繫結

可能有的時候,為Dart FFI編寫繫結程式碼會過於耗時或繁瑣。在這種情況下,Foreign Function Interface GENerator(ffigen)會很有幫助。ffigen 是一個FFI的繫結生成器。它幫助解析C 標頭檔案並自動生成dart 程式碼。

讓我們使用這個包含基本數學函式的C 標頭檔案的例子。

``` /// native/math.h

/ Adds 2 integers. */ int sum(int a, int b); / Subtracts 2 integers. / int subtract(int a, int b); / Multiplies 2 integers, returns pointer to an integer,. / int multiply(int a, int b); / Divides 2 integers, returns pointer to a float. / float divide(int a, int b); / Divides 2 floats, returns a pointer to double. / double dividePercision(float a, float b);

```

為了在Dart中生成FFI的繫結,我們將在pubspec.yml 檔案中把ffigen 新增到dev_dependencies

``` /// pubspec.yaml dev_dependencies: ffigen: ^4.1.2

```

ffigen 要求將配置作為一個單獨的config.yaml 檔案新增,或者新增在pubspec.yamlffigen 下,如圖所示。

``` /// pubspec.yaml ....

ffigen: name: 'MathUtilsFFI' description: 'Written for the FFI article' output: 'lib/ffi/generated_bindings.dart' headers: entry-points: - 'native/headers/math.h'

```

應該生成的entry-pointsoutput 檔案是強制性的欄位;但是,我們也可以定義幷包括一個namedescription

接下來,我們將執行以下程式碼。
dart run ffigen

這將導致以下輸出。generated_bindings.dart

現在,我們可以在我們的Dart檔案中使用MathUtilsFFI 類。

在一個演示中使用FFIGEN

現在我們已經介紹了ffigen 的基本知識,讓我們來看看一個演示。

生成動態庫

在這個演示中,我們將使用cJSON,它是一個超輕量級的JSON解析器,可用於FlutterDart 應用程式。

整個cJSON庫由一個C檔案和一個頭檔案組成,所以我們可以簡單地將cJSON.ccJSON.h 複製到我們專案的原始碼中。然而,我們還需要使用CMake構建系統。CMake被推薦用於樹外構建,即構建目錄(包含編譯檔案)與原始檔目錄(包含原始檔)分開。截至目前,CMake的版本為2.8.5或更高。

要在Unix平臺上用CMake構建cJSON,我們首先建立一個build 目錄,然後在該目錄下執行CMake。

``` cd native/cJSON // where I have copied the source files mkdir build cd build cmake ..

```

下面是輸出結果。

``` -- The C compiler identification is AppleClang 13.0.0.13000029 -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped -- Detecting C compile features -- Detecting C compile features - done -- Performing Test FLAG_SUPPORTED_fvisibilityhidden -- Performing Test FLAG_SUPPORTED_fvisibilityhidden - Success -- Configuring done -- Generating done -- Build files have been written to: ./my_app_sample/native/cJSON/build

```

這將建立一個Makefile,以及其他幾個檔案。

我們用這個命令來編譯。

``` make

```

構建進度條會前進,直到完成。

``` [ 88%] Built target readme_examples [ 91%] Building C object tests/CMakeFiles/minify_tests.dir/minify_tests.c.o [ 93%] Linking C executable minify_tests [ 93%] Built target minify_tests [ 95%] Building C object fuzzing/CMakeFiles/fuzz_main.dir/fuzz_main.c.o [ 97%] Building C object fuzzing/CMakeFiles/fuzz_main.dir/cjson_read_fuzzer.c.o [100%] Linking C executable fuzz_main [100%] Built target fuzz_main

```

動態庫是根據平臺生成的。例如,Mac使用者會看到libcjson.dylib ,而Windows使用者可能看到cjson.dll ,Linux使用者可能看到libcjson.so

生成Dart FFI繫結檔案

接下來,我們需要生成Dart FFI繫結檔案。為了演示如何使用分離配置,我們將建立一個新的配置檔案,cJSON.config.yaml ,並配置cJSON庫。

``` // cJSON.config.yaml

output: 'lib/ffi/cjson_generated_bindings.dart' name: 'CJson' description: 'Holds bindings to cJSON.' headers: entry-points: - 'native/cJSON/cJSON.h' include-directives: - '**cJSON.h' comments: false typedef-map: 'size_t': 'IntPtr'

```

為了生成FFI繫結檔案。我們必須執行dart run ffigen --config cJSON.config.yaml

```

flutter pub run ffigen --config cJSON.config.yaml Changing current working directory to: //my_app_sample Running in Directory: '//my_app_sample' Input Headers: [native/cJSON/cJSON.h] Finished, Bindings generated in /**/my_app_sample/lib/ffi/cjson_generated_bindings.dart

```

為了使用這個庫,我們建立一個JSON檔案。

``` /// example.json

{ "name": "Majid Hajian", "age": 30, "nicknames": [ { "name": "Mr. Majid", "length": 9 }, { "name": "Mr. Dart", "length": 8 } ] }

```

這個JSON檔案的例子很簡單,但想象一下同樣的過程,重的JSON,需要執行解析。

載入該庫

首先,我們必須確保我們正在正確地載入動態庫。

``` /// cJSON.dart import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; import 'package:ffi/ffi.dart'; import 'package:path/path.dart' as p; import './lib/ffi/cjson_generated_bindings.dart' as cj;

String _getPath() { final cjsonExamplePath = Directory.current.absolute.path; var path = p.join(cjsonExamplePath, 'native/cJSON/build/'); if (Platform.isMacOS) { path = p.join(path, 'libcjson.dylib'); } else if (Platform.isWindows) { path = p.join(path, 'Debug', 'cjson.dll'); } else { path = p.join(path, 'libcjson.so'); } return path; }

```

接下來,我們開啟動態庫。

final cjson = cj.CJson(DynamicLibrary.open(_getPath()));

現在,我們可以使用生成的cJSON綁定了。

``` /// cJSON.dart

void main() { final pathToJson = p.absolute('example.json'); final jsonString = File(pathToJson).readAsStringSync(); final cjsonParsedJson = cjson.cJSON_Parse(jsonString.toNativeUtf8().cast()); if (cjsonParsedJson == nullptr) { print('Error parsing cjson.'); exit(1); } // The json is now stored in some C data structure which we need // to iterate and convert to a dart object (map/list). // Converting cjson object to a dart object. final dynamic dartJson = convertCJsonToDartObj(cjsonParsedJson.cast()); // Delete the cjsonParsedJson object. cjson.cJSON_Delete(cjsonParsedJson); // Check if the converted json is correct // by comparing the result with json converted by dart:convert. if (dartJson.toString() == json.decode(jsonString).toString()) { print('Parsed Json: $dartJson'); print('Json converted successfully'); } else { print("Converted json doesn't match\n"); print('Actual:\n' + dartJson.toString() + '\n'); print('Expected:\n' + json.decode(jsonString).toString()); } }

```

接下來,我們可以使用輔助函式來解析(或轉換)cJSON為Dart物件。

``` /// main.dart dynamic convertCJsonToDartObj(Pointer parsedcjson) { dynamic obj; if (cjson.cJSON_IsObject(parsedcjson.cast()) == 1) { obj = {}; Pointer? ptr; ptr = parsedcjson.ref.child; while (ptr != nullptr) { final dynamic o = convertCJsonToDartObj(ptr!); _addToObj(obj, o, ptr.ref.string.cast()); ptr = ptr.ref.next; } } else if (cjson.cJSON_IsArray(parsedcjson.cast()) == 1) { obj = []; Pointer? ptr; ptr = parsedcjson.ref.child; while (ptr != nullptr) { final dynamic o = convertCJsonToDartObj(ptr!); _addToObj(obj, o); ptr = ptr.ref.next; } } else if (cjson.cJSON_IsString(parsedcjson.cast()) == 1) { obj = parsedcjson.ref.valuestring.cast().toDartString(); } else if (cjson.cJSON_IsNumber(parsedcjson.cast()) == 1) { obj = parsedcjson.ref.valueint == parsedcjson.ref.valuedouble ? parsedcjson.ref.valueint : parsedcjson.ref.valuedouble; } return obj; } void _addToObj(dynamic obj, dynamic o, [Pointer? name]) { if (obj is Map) { obj[name!.toDartString()] = o; } else if (obj is List) { obj.add(o); } }

```

使用FFI將字串從C語言傳給Dart

[ffi] 包可以用來將字串從C語言傳遞到Dart。我們把這個包新增到我們的依賴項中。

``` /// pubspec.yaml

dependencies: ffi: ^1.1.2

```

測試呼叫

現在,讓我們檢查一下我們的演示是否成功

我們可以看到在這個例子中,name,age, 和nicknames 的C語言字串被成功解析為Dart。

```

dart cJSON.dart

Parsed Json: {name: Majid Hajian, age: 30, nicknames: [{name: Mr. Majid, length: 9}, {name: Mr. Dart, length: 8}]} Json converted successfully

```

現在我們已經回顧了FFI的要點,讓我們看看如何在Flutter中使用它們。

使用FFI將動態庫新增到Flutter應用程式中

Dart FFI的大部分概念也適用於Flutter。為了簡化本教程,我們將專注於Android和iOS,但這些方法也適用於其他應用程式。

為了使用FFI向Flutter應用程式新增動態庫,我們將遵循以下步驟。

配置Android Studio C語言編譯器

為了配置Android Studio C編譯器,我們將遵循三個步驟。

  1. 轉到android/app

  2. 建立一個CMakeLists.txt

    file:cmake

  3. 開啟android/app/build.gradle ,新增以下程式碼段:

    android { ....externalNativeBuild { cmake { path "CMakeLists.txt" } }... }

這段程式碼告訴Android構建系統在構建應用程式時用CMakeLists.txt 來呼叫CMake 。它將在Android上把.c 原始檔編譯成一個共享物件庫,字尾為.so

配置Xcode的C編譯器

為了確保Xcode將用本地C程式碼構建我們的應用程式,我們將遵循以下10個步驟。

  1. 通過執行來開啟Xcode工作區。

open< ios/Runner.xcworkspace

  1. 從頂部導航欄的Targets下拉選單中,選擇Runner
  2. 從標籤行中,選擇構建階段
  3. 展開Compile Sources標籤,並點選+鍵。
  4. 在彈出的視窗中,點選新增其他
  5. 導航到C檔案的儲存位置,例如:FLUTTER_PROJCT_ROOT/DART/native/cJSON/cJSON.c ,並新增cJSON.ccJSON.h 兩個檔案。
  6. 展開 "編譯源"標籤,點選 "+"鍵
  7. 在彈出的視窗中,點選新增其他
  8. 導航到r.c 檔案的儲存位置,例如。FLUTTER_PROJCT_ROOT/DART/native/cJSON/cJSON.c
  9. 選擇複製專案(如果需要),然後點選完成

現在,我們準備將生成的Dart繫結程式碼新增到Flutter應用程式中,載入庫,並呼叫函式。

生成FFI繫結程式碼

我們將使用ffigen 來生成繫結程式碼。首先,我們將新增ffigen 到Flutter應用程式。

``` /// pubspec.yaml for my Flutter project ... dependencies: ffigen: ^4.1.2 ...

ffigen: output: 'lib/ffi/cjson_generated_bindings.dart' name: 'CJson' description: 'Holds bindings to cJSON.' headers: entry-points: - 'DART/native/cJSON/cJSON.h' include-directives: - '**cJSON.h' comments: false typedef-map: 'size_t': 'IntPtr'

```

接下來,我們將執行ffigen

``` flutter pub run ffigen

```

我們需要確保example.json 檔案被新增到assets下。

``` /// pubspec.yaml ... flutter: uses-material-design: true assets: - example.json ...

```

載入動態庫

就像靜態連結庫可以被嵌入到應用程式啟動時載入一樣,靜態連結庫中的符號可以使用DynamicLibrary.executableDynamicLibrary.process 來載入。

在Android上,動態連結庫是以一組.so (ELF)檔案的形式釋出的,每個架構都有一個。在iOS上,動態連結的庫以.framework 資料夾的形式釋出。

一個動態連結的庫可以通過DynamicLibrary.open 命令載入到Dart中。

我們將使用下面的程式碼來載入該庫。

``` /// lib/ffi_loader.dart

import 'dart:convert'; import 'dart:developer' as dev_tools; import 'dart:ffi'; import 'dart:io'; import 'package:ffi/ffi.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:my_app_sample/ffi/cjson_generated_bindings.dart' as cj;

class MyNativeCJson { MyNativeCJson({ required this.pathToJson, }) { final cJSONNative = Platform.isAndroid ? DynamicLibrary.open('libcjson.so') : DynamicLibrary.process(); cjson = cj.CJson(cJSONNative); } late cj.CJson cjson; final String pathToJson; Future load() async { final jsonString = await rootBundle.loadString('assets/$pathToJson'); final cjsonParsedJson = cjson.cJSON_Parse(jsonString.toNativeUtf8().cast()); if (cjsonParsedJson == nullptr) { dev_tools.log('Error parsing cjson.'); } final dynamic dartJson = convertCJsonToDartObj(cjsonParsedJson.cast()); cjson.cJSON_Delete(cjsonParsedJson); if (dartJson.toString() == json.decode(jsonString).toString()) { dev_tools.log('Parsed Json: $dartJson'); dev_tools.log('Json converted successfully'); } else { dev_tools.log("Converted json doesn't match\n"); dev_tools.log('Actual:\n$dartJson\n'); dev_tools.log('Expected:\n${json.decode(jsonString)}'); } } dynamic convertCJsonToDartObj(Pointer parsedcjson) { dynamic obj; if (cjson.cJSON_IsObject(parsedcjson.cast()) == 1) { obj = {}; Pointer? ptr; ptr = parsedcjson.ref.child; while (ptr != nullptr) { final dynamic o = convertCJsonToDartObj(ptr!); _addToObj(obj, o, ptr.ref.string.cast()); ptr = ptr.ref.next; } } else if (cjson.cJSON_IsArray(parsedcjson.cast()) == 1) { obj = []; Pointer? ptr; ptr = parsedcjson.ref.child; while (ptr != nullptr) { final dynamic o = convertCJsonToDartObj(ptr!); _addToObj(obj, o); ptr = ptr.ref.next; } } else if (cjson.cJSON_IsString(parsedcjson.cast()) == 1) { obj = parsedcjson.ref.valuestring.cast().toDartString(); } else if (cjson.cJSON_IsNumber(parsedcjson.cast()) == 1) { obj = parsedcjson.ref.valueint == parsedcjson.ref.valuedouble ? parsedcjson.ref.valueint : parsedcjson.ref.valuedouble; } return obj; } void _addToObj(dynamic obj, dynamic o, [Pointer? name]) { if (obj is Map) { obj[name!.toDartString()] = o; } else if (obj is List) { obj.add(o); } } }

```

對於安卓系統,我們呼叫DynamicLibrary ,找到並開啟libcjson.so 共享庫。

``` final cJSONNative = Platform.isAndroid ? DynamicLibrary.open('libcJSON.so') : DynamicLibrary.process();

cjson = cj.CJson(cJSONNative);

```

在iOS中不需要這個特別的步驟,因為所有連結的符號在iOS應用執行時都會對映。

測試Flutter中的呼叫

為了證明本地呼叫在Flutter中是有效的,我們在main.dart 檔案中新增用法。

``` // main.dart

import 'package:flutter/material.dart'; import 'ffi_loader.dart';

void main() { runApp(const MyApp());

final cJson = MyNativeCJson(pathToJson: 'example.json'); await cJson.load(); }

```

接下來,我們執行該應用程式。flutter run

Voilà!我們已經成功地從我們的Flutter應用中呼叫了本地庫。

我們可以在控制檯中檢視本地呼叫的日誌。

``` Launching lib/main_development.dart on iPhone 13 in debug mode... lib/main_development.dart:1 Xcode build done. 16.5s Connecting to VM Service at ws://127.0.0.1:53265/9P2HdUg5_Ak=/ws [log] Parsed Json: {name: Majid Hajian, age: 30, nicknames: [{name: Mr. Majid, length: 9}, {name: Mr. Dart, length: 8}]} [log] Json converted successfully

```

展望未來,我們可以在我們的Flutter應用中的不同部件和服務中使用這個庫。

總結

Dart FFI為將本地庫整合到Dart和Flutter應用程式提供了一個簡單的解決方案。在這篇文章中,我們已經演示瞭如何使用Dart FFI在Dart中呼叫C函式,並將C庫整合到Flutter應用程式中。

你可能想進一步嘗試使用Dart FFI,使用用其他語言編寫的程式碼。我對實驗Go和Rust特別感興趣,因為這些語言是記憶體管理的。Rust特別有趣,因為它是一種記憶體安全的語言,而且效能相當好。

本文中使用的所有例子都可以在GitHub上找到。

The postUsing Dart FFI to access native libraries in Flutterappeared first onLogRocket Blog.