前言
在開發 Dart FFI 程式時,如何優雅且正確地載入 C 函式庫路徑,始終是令開發者頭痛的議題。
與其在 Dart 層級進行複雜且繁瑣的配置,本文將展示如何透過 C Bridge(C 橋接層) 模式封裝 C 函式庫路徑,從根本上簡化 Dart 端的路徑管理負擔。
ffigen 的工程議題
在 Dart FFI 的標準流程中,我們通常使用 ffigen 掃描 C Header 檔來自動生成 Dart 程式碼。然而,這種自動化模式在實際工程應用中常會遇到以下挑戰:
- 冗餘程式碼:自動掃描常會產生大量專案中根本用不到的函式。
- 路徑耦合:無法靈活且自動化地處理 C 函式庫的動態路徑。
為了優化開發體驗並解決上述痛點,改採「手動撰寫 C Bridge」反而是更為高效且精確的做法。
C Bridge:精簡且可控的橋接層
所謂的 C Bridge,是指一個輕量化的 C 函式庫中間層,它只實作 Dart 端真正需要的函式。這樣一來,Dart 端只需要專注於對接這層 C Bridge,而不必直接面對複雜的原生底層。
以下範例展示了一個極簡的 C Bridge。這個函式不設回傳值,僅負責在終端機輸出文字:
#ifndef HELLO_H
#define HELLO_H
#ifdef __cplusplus
extern "C" {
#endif
void hello(const char *name);
#ifdef __cplusplus
}
#endif
#endif
實作如下:
#include <stdio.h>
#include "hello.h"
void hello(const char *name)
{
printf("Hello %s\n", name);
fflush(stdout);
}
設計重點: 雖然這個範例看似簡單,但我們實際上避開了直接處理 printf 等不定參數函式在 FFI 介接上的困難。透過 C Bridge,我們成功將複雜的底層介面簡化為單一參數的固定介面。
編譯該動態函式庫的指令如下:
$ gcc -c -fPIC hello.c -o hello.o
$ gcc -shared -o libhello.so hello.o
注意:編譯時務必加上 -fPIC 參數,以確保生成的動態函式庫能在記憶體中被自由定位(Position-Independent Code)。
連結 C Bridge 的 Dart 實作
接著,我們在 Dart 端撰寫對應的 FFI 程式碼來呼叫 hello 函式:
import 'dart:io';
import 'dart:ffi' as ffi;
import 'package:ffi/ffi.dart';
typedef cHelloFunc = ffi.Void Function(ffi.Pointer<Utf8>);
typedef dartHelloFunc = void Function(ffi.Pointer<Utf8>);
void main() {
final libPath =
Uri.file(Platform.script.path).resolve('libhello.so').toFilePath();
final dylib = ffi.DynamicLibrary.open(libPath);
final dartHelloFunc hello =
dylib.lookup<ffi.NativeFunction<cHelloFunc>>('hello').asFunction();
const str = "World";
final ffi.Pointer<Utf8> cStr = str.toNativeUtf8();
try {
hello(cStr);
} finally {
malloc.free(cStr);
}
}
你會發現,在 C Bridge 模式下,Dart 程式碼完全不需要理解 libc.so 的實際路徑或複雜的系統環境,只需要定位好我們自定義的 libhello.so 即可。
針對執行環境的思考
若你的專案對跨平台相容性有高度要求,可以加入自動偵測系統環境邏輯來調整 C Bridge 的副檔名(如 .so, .dll, .dylib)。
然而,過度的抽象有時是不必要的。如果你的程式定位是跑在伺服器端或 GNU/Linux 環境,直接針對該平台編譯即可。畢竟許多後端服務根本不會運行在 Windows 和 macOS 上,過早優化反而會增加開發維護的負擔。
結語
本文透過 Dart FFI 版本的「Hello World」,展示了如何利用 C Bridge 模式高效地呼叫 C 函式。
考慮到 Dart 生態系相對精簡,且語言特性並非針對大規模數值運算進行優化,學會如何透過 FFI 借力使力,將高效能運算交給 C 語言處理,已成為 Dart 工程師進階路徑上的必修課。