FFI: kiến thức cơ bản, Foreign Function Interface và tích hợp với C

Tác giả: IT Sectr Đã đăng: 2026-06-05 Thời gian đọc: 10 phút

FFI (Foreign Function Interface) là một cơ chế của ngôn ngữ Dart, được cung cấp bởi gói dart:ffi, cho phép gọi các hàm từ thư viện C gốc trực tiếp, không cần các lớp trung gian trong Kotlin, Swift hay Java. Nhà phát triển tải một thư viện động (.so trên Android, .dylib trên iOS, .dll trên Windows), khai báo chữ ký của các hàm C và gọi chúng như các hàm Dart thông thường. Theo Tài liệu tham khảo Dart API (2025), FFI giảm chi phí đầu vào cho các cuộc gọi liên ngôn ngữ xuống còn 0,1 µs, nhanh hơn hàng chục lần so với Method Channel.

Những điểm chính

  • FFI (Foreign Function Interface) — cơ chế gọi hàm C trực tiếp từ Dart
  • dart:ffi cung cấp API để tải thư viện và khai báo chữ ký
  • Hiệu suất của các cuộc gọi FFI cao hơn 50–100 lần so với Method Channel
  • Kiểu dữ liệu FFI hỗ trợ các kiểu nguyên thủy, cấu trúc và con trỏ C
  • Flutter sử dụng FFI để tích hợp với các thư viện gốc: OpenCV, SQLite, FFmpeg

FFI là gì?

FFI (Foreign Function Interface) là một cơ chế cho phép ngôn ngữ lập trình gọi các hàm được viết bằng ngôn ngữ khác. Trong bối cảnh của Dart và Flutter, FFI có nghĩa là khả năng gọi các hàm từ thư viện C/C++ trực tiếp từ mã Dart, mà không cần viết mã dành riêng cho nền tảng bằng Java (Android) hoặc Swift/Objective-C (iOS).

Gói dart:ffi xuất hiện trong Dart 2.12 (2021) và kể từ đó đã trở thành công cụ quan trọng để tích hợp Flutter với mã gốc. Trước dart:ffi, cách duy nhất để gọi một hàm C từ Dart là thông qua Method Channel — một cơ chế bất đồng bộ truyền thông điệp qua JSON serialization giữa Dart và phía gốc. FFI hoạt động khác: mã Dart truy cập trực tiếp vào bộ nhớ của thư viện C, gọi các hàm thông qua ABI gốc (Application Binary Interface) mà không cần serialization hay chuyển đổi ngữ cảnh.

FFI đặc biệt được ưa chuộng trong các kịch bản mà hiệu suất là rất quan trọng: xử lý ảnh (OpenCV), âm thanh (FFmpeg), mật mã (OpenSSL), học máy (TensorFlow Lite) và cơ sở dữ liệu (SQLite). Trong tất cả các trường hợp này, Method Channel tạo ra độ trễ không thể chấp nhận được, trong khi FFI cung cấp hiệu suất tương đương với mã C/C++ gốc. Thư viện dart:ffi cũng hỗ trợ quản lý bộ nhớ: cấp phát, giải phóng và thao tác con trỏ.

FFI so với Method Channel: sự khác biệt cơ bản

Method Channel hoạt động bất đồng bộ: Dart gửi một thông điệp đến mã gốc, mã gốc xử lý và gửi kết quả trở lại. Mỗi cuộc gọi yêu cầu serialization các đối số vào Map, truyền qua hàng đợi và deserialization. Điều này mất 0,5–5 ms mỗi cuộc gọi. FFI hoạt động đồng bộ và không cần serialization — một cuộc gọi hàm C mất 0,01–0,1 µs. Chênh lệch 50–500 lần, điều này rất quan trọng cho các hoạt động tần suất cao.

dart:ffi hoạt động như thế nào?

Làm việc với dart:ffi bao gồm ba bước: tải thư viện, khai báo chữ ký và gọi hàm. Mỗi bước sử dụng kiểu nghiêm ngặt của Dart, giảm thiểu lỗi thời gian chạy.

Ở bước đầu tiên, thư viện động được tải qua lớp DynamicLibrary. Thư viện có thể được tải theo tên (libxyz.so, libxyz.dylib, xyz.dll) hoặc theo đường dẫn đầy đủ. Dart tự động tìm kiếm thư viện trong các đường dẫn chuẩn của hệ thống. DynamicLibrary cung cấp phương thức lookupFunction, liên kết một hàm Dart với một hàm C theo tên ký hiệu.

Ở bước thứ hai, một hàm Dart được khai báo với các chú thích kiểu tương ứng với chữ ký C. Các kiểu đặc biệt từ dart:ffi được sử dụng: Int32, Float, Double, Pointer, NativeFunction, Handle và các kiểu khác. Chú thích lookupFunction nhận hai tham số generic: kiểu hàm Dart (cách nó sẽ xuất hiện trong Dart) và kiểu hàm C gốc (cách nó được khai báo trong C).

Ở bước thứ ba, hàm Dart được tạo ra được gọi như một hàm thông thường. Các đối số được truyền trực tiếp, kết quả được trả về ngay lập tức. Nếu hàm C sửa đổi bộ nhớ qua con trỏ, Dart có thể đọc những thay đổi này qua lớp Pointer. Quản lý bộ nhớ phía C vẫn là trách nhiệm của nhà phát triển — dart:ffi không quản lý bộ nhớ được cấp phát bởi malloc trong C.

Ví dụ cơ bản: gọi hàm C từ Dart

dart
import 'dart:ffi'
import 'package:ffi/ffi.dart'

// Khai báo hàm C: int add(int a, int b)
typedef AddNative = Int32 Function(Int32, Int32)
typedef AddDart = int Function(int, int)

void main() {
    final lib = DynamicLibrary.open('libcalculator.so')
    final AddDart add = lib
        .lookupFunction<AddNative, AddDart>('add')

    print(add(5, 3)) // 8
}

Trong ví dụ này, add là một hàm C nhận hai int và trả về một int. AddNative typedef mô tả chữ ký C với các kiểu dart:ffi, trong khi AddDart mô tả cách hàm này sẽ xuất hiện trong Dart. lookupFunction liên kết chúng và trả về một hàm Dart có thể được gọi như một hàm thông thường.

Các kiểu dữ liệu trong FFI

dart:ffi cung cấp một tập hợp các kiểu tương ứng với các kiểu C. Mỗi kiểu có kích thước cố định và các quy tắc chuyển đổi giữa Dart và C. Hiểu được sự tương ứng kiểu là rất quan trọng đối với hoạt động chính xác của FFI — một lỗi về kích thước hoặc dấu của kiểu có thể dẫn đến sự cố ứng dụng.

Kiểu CKiểu dart:ffiKiểu DartKích thước (byte)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8 (con trỏ)
void*Pointer<Void>Pointer8 (con trỏ)
structPointer<T> (Struct)Pointerphụ thuộc vào trường

Để làm việc với chuỗi C (char*), dart:ffi sử dụng Pointer<Int8>. Chuyển đổi từ Dart String sang C char* và ngược lại được thực hiện qua toNativeUtf8 (từ gói ffi) và fromUtf8. Điều quan trọng là giải phóng chuỗi C sau khi sử dụng qua calloc.free để tránh rò rỉ bộ nhớ.

Cấu trúc (Struct)

dart:ffi hỗ trợ khai báo cấu trúc C dưới dạng các lớp Dart mở rộng Struct. Các trường của cấu trúc được khai báo với các chú thích @Int32(), @Float(), @Array() và các chú thích khác. Kích thước và độ dịch chuyển của các trường được tính toán tự động theo ABI của nền tảng. Pointer<Point> có thể được lấy từ một hàm C trả về con trỏ đến cấu trúc, hoặc được cấp phát trong Dart qua calloc.

dart
// Struct C: typedef struct { int x; int y; } Point;
final class Point extends Struct {
    @Int32()
    external int x

    @Int32()
    external int y
}

// Gọi hàm C trả về Point*
typedef CreatePointNative = Pointer<Point> Function(Int32, Int32)
typedef CreatePointDart = Pointer<Point> Function(int, int)

final Pointer<Point> p = createPoint(10, 20)
print('x: ${p.ref.x}, y: ${p.ref.y}')
calloc.free(p) // giải phóng bộ nhớ

Lớp Point mở rộng Struct và khai báo các trường x và y với các chú thích @Int32(). Mã C được tạo ra sẽ có bố cục bộ nhớ giống hệt. Pointer.ref cung cấp quyền truy cập vào các trường của cấu trúc thông qua getters và setters.

Ví dụ thực tế về FFI

Hãy xem một ví dụ phức tạp hơn — tích hợp với thư viện C để tính toán băm SHA256. Đây là một nhiệm vụ điển hình mà FFI mang lại lợi ích hiệu suất đáng kể so với Method Channel.

Tích hợp với OpenSSL qua FFI

Thư viện OpenSSL cung cấp hàm SHA256, tính toán băm của một chuỗi. Thông qua dart:ffi, chúng ta có thể gọi nó trực tiếp, mà không cần viết wrapper Java hay Swift. Đây là một ví dụ về cách FFI cho phép tái sử dụng các thư viện C hiện có trong Flutter.

dart
import 'dart:ffi'
import 'package:ffi/ffi.dart'

// Chữ ký: unsigned char* SHA256(
//   const unsigned char *d, size_t n, unsigned char *md)
typedef Sha256Native = Pointer<Uint8> Function(
    Pointer<Uint8>, Size, Pointer<Uint8>)
typedef Sha256Dart = Pointer<Uint8> Function(
    Pointer<Uint8>, int, Pointer<Uint8>)

String sha256(String input) {
    final lib = DynamicLibrary.open('libcrypto.so')
    final Sha256Dart sha256Fn = lib
        .lookupFunction<Sha256Native, Sha256Dart>('SHA256')

    final inputPtr = input.toNativeUtf8()
    final outputPtr = calloc(Uint8)(32) // SHA256 = 32 byte

    sha256Fn(inputPtr, input.length, outputPtr)

    final digest = outputPtr.asTypedList(32)
    final hex = digest.map((b) => b.toRadixString(16)
        .padLeft(2, '0')).join()

    calloc.free(inputPtr)
    calloc.free(outputPtr)

    return hex
}

Trong ví dụ này, hàm sha256 tải thư viện libcrypto.so, tìm ký hiệu SHA256 và gọi nó với các con trỏ đến dữ liệu đầu vào và đầu ra. toNativeUtf8 chuyển đổi Dart String thành chuỗi C (cấp phát bộ nhớ), và asTypedList cho phép đọc mảng byte kết quả. Bộ nhớ được giải phóng sau khi sử dụng — đây là một bước bắt buộc để ngăn ngừa rò rỉ.

Cấp phát và giải phóng bộ nhớ

Gói ffi cung cấp hàm calloc để cấp phát bộ nhớ tương thích với C. Bộ nhớ được cấp phát phải được giải phóng qua calloc.free, nếu không sẽ xảy ra rò rỉ. Để quản lý bộ nhớ tự động, bạn có thể sử dụng lớp Arena từ gói ffi, lớp này giải phóng tất cả bộ nhớ được cấp phát bên trong nó khi arena.release() được gọi. Điều này đặc biệt thuận tiện cho số lượng lớn các cấp phát tạm thời.

Giới hạn và các thực hành tốt nhất

Mặc dù mạnh mẽ, FFI có những giới hạn cần được xem xét khi thiết kế kiến trúc ứng dụng Flutter. Các giới hạn chính liên quan đến an toàn kiểu, quản lý bộ nhớ và tương thích nền tảng.

An toàn

FFI không thực hiện kiểm tra kiểu trong thời gian chạy. Nếu một hàm C mong đợi một con trỏ nhưng nhận được một số, ứng dụng sẽ gặp sự cố với lỗi phân đoạn. Nên sử dụng FFIgen — một công cụ tạo các wrapper Dart an toàn về kiểu dựa trên các tệp tiêu đề C (.h). FFIgen phân tích các khai báo hàm C và tạo mã Dart với các kiểu chính xác, loại bỏ các lỗi ở giai đoạn viết mã.

Tương thích nền tảng

Tên và đường dẫn của các thư viện động khác nhau giữa các nền tảng: libxyz.so trên Android/Linux, libxyz.dylib trên iOS/macOS, xyz.dll trên Windows. Đối với các thư viện đa nền tảng, việc biên dịch có điều kiện được sử dụng thông qua dart:io (Platform.isAndroid, Platform.isIOS) hoặc các trừu tượng như package:ffi. Nên tạo một phương thức factory trả về thư viện chính xác cho nền tảng hiện tại.

Quản lý bộ nhớ

FFI không quản lý bộ nhớ phía C. Nếu một hàm C cấp phát bộ nhớ qua malloc, nó phải được giải phóng qua free, nếu không sẽ xảy ra rò rỉ. Dart không có trình thu gom rác cho bộ nhớ C. Khuyến nghị: luôn giải phóng bộ nhớ trong cùng phương thức nơi nó được cấp phát, hoặc sử dụng Arena để giải phóng theo nhóm.

Hiệu suất và luồng

Các cuộc gọi FFI được thực thi trong cùng luồng với mã Dart. Các hoạt động đồng bộ kéo dài (hơn 10 ms) chặn luồng UI và gây ra mất khung hình. Đối với các hoạt động dài, nên gọi hàm C trong một Isolate hoặc đảm bảo rằng hàm C chạy công việc trong luồng nền và thông báo cho Dart qua Port hoặc callback.

dart
// FFI trong isolate cho các hoạt động dài
import 'dart:isolate'

Future<String> computeHash(String input) async {
    final port = ReceivePort()
    await Isolate.spawn((SendPort sendPort) {
        final result = sha256(input) // Gọi FFI
        sendPort.send(result)
    }, port.sendPort)

    return await port.first as String
}

Di chuyển các cuộc gọi FFI vào một isolate đảm bảo rằng luồng UI không bị chặn. Tuy nhiên, cần lưu ý rằng việc truyền một lượng lớn dữ liệu giữa các isolate yêu cầu sao chép bộ nhớ. Đối với các bộ đệm lớn (>10 MB), tốt hơn nên sử dụng SharedMemory hoặc tệp ánh xạ bộ nhớ.

Câu hỏi thường gặp

FFI khác gì so với Method Channel?

FFI gọi hàm C trực tiếp, đồng bộ và không cần serialization — độ trễ 0,01–0,1 µs. Method Channel hoạt động bất đồng bộ qua JSON serialization với độ trễ 0,5–5 ms. FFI phù hợp cho các hoạt động hiệu suất cao, Method Channel cho các cuộc gọi API nền tảng đơn giản.

Có thể gọi hàm C++ qua FFI không?

Trực tiếp — không, dart:ffi chỉ hỗ trợ các hàm C. Để gọi C++, bạn cần tạo một wrapper C với extern “C” (các điểm đầu vào được xuất dưới dạng ký hiệu C). Các lớp C++ yêu cầu một lớp bổ sung dịch các cuộc gọi phương thức thành các hàm C.

Làm thế nào để xử lý lỗi trong các hàm C?

FFI không hỗ trợ ngoại lệ — nếu một hàm C trả về mã lỗi, nó phải được kiểm tra thủ công. Nên bọc các cuộc gọi FFI trong try-catch trong Dart và kiểm tra mã trả về của các hàm C. Các lỗi nghiêm trọng (segfault) không thể bắt được.

Những thư viện nào không thể sử dụng qua FFI?

FFI không hoạt động với các thư viện yêu cầu khởi tạo phức tạp của Java (JNI) hoặc Objective-C (Message Dispatch). Ví dụ, UIKit và Android Views không thể truy cập qua FFI. Giới hạn liên quan đến việc FFI hoạt động ở cấp độ ABI C, trong khi các API này yêu cầu môi trường thực thi cụ thể.

Có cần biên dịch thư viện C cho mỗi nền tảng không?

Có, các thư viện C được biên dịch riêng cho từng nền tảng mục tiêu. Cho Android, .so được xây dựng cho các ABI khác nhau (armeabi-v7a, arm64-v8a, x86_64). Cho iOS — .dylib phổ quát (arm64). Cho Windows — .dll. Flutter tự động đóng gói phiên bản thư viện chính xác trong quá trình xây dựng.

Tổng kết

  • FFI (Foreign Function Interface) — cơ chế gọi hàm C trực tiếp từ Dart qua dart:ffi
  • Hiệu suất của các cuộc gọi FFI cao hơn 50–500 lần so với Method Channel
  • Kiến trúc bao gồm tải thư viện, khai báo chữ ký và gọi hàm
  • Kiểu dữ liệu dart:ffi hỗ trợ Int32, Float, Double, Pointer, Struct và các kiểu C khác
  • Bộ nhớ phía C được quản lý thủ công qua calloc/free hoặc Arena
  • Giới hạn của FFI: không kiểm tra kiểu runtime, không hỗ trợ C++ trực tiếp, chặn luồng UI
  • Sử dụng FFI để tích hợp hiệu suất cao với các thư viện gốc trong Flutter

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm