位元詩人 告別 Dart ffigen 設定檔地獄:如何用 C Bridge 暴力破解路徑問題

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在開發 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 工程師進階路徑上的必修課。

關於作者

位元詩人 (ByteBard) 是資訊領域碩士,喜歡用開源技術來解決各式各樣的問題。這類技術跨平台、重用性高、技術生命長。

除了開源技術以外,位元詩人喜歡日本料理和黑咖啡,會一些日文,有時會自助旅行。

近期在學習韓文,並將語言學習的心得轉化為開源專案,回饋社群。

這裡是位元詩人的 GitHub 個人頁