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) 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ỏ.
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.
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.
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.
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 C | Kiểu dart:ffi | Kiểu Dart | Kích thước (byte) |
|---|---|---|---|
| int | Int32 | int | 4 |
| long | Int64 | int | 8 |
| float | Float | double | 4 |
| double | Double | double | 8 |
| char* | Pointer<Int8> | Pointer | 8 (con trỏ) |
| void* | Pointer<Void> | Pointer | 8 (con trỏ) |
| struct | Pointer<T> (Struct) | Pointer | phụ 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ớ.
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.
// 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.
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.
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.
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ỉ.
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.
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.
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ê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.
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.
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.
// 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 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.
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.
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.
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á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
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.