FFI (Foreign Function Interface) เป็นกลไกของภาษา Dart ที่จัดเตรียมโดยแพ็คเกจ dart:ffi ซึ่งอนุญาตให้เรียกใช้ฟังก์ชันจากไลบรารี C ดั้งเดิมได้โดยตรง โดยไม่ต้องมีชั้นกลางใน Kotlin, Swift หรือ Java นักพัฒนาสามารถโหลดไลบรารีแบบไดนามิก (.so บน Android, .dylib บน iOS, .dll บน Windows) ประกาศลายเซ็นของฟังก์ชัน C และเรียกใช้เหมือนฟังก์ชัน Dart ทั่วไป ตาม Dart API Reference (2025), FFI ลดค่าใช้จ่ายในการเรียกใช้ข้ามภาษาเหลือ 0.1 µs ซึ่งเร็วกว่าการใช้ Method Channel หลายสิบเท่า
ประเด็นสำคัญ
FFI (Foreign Function Interface) เป็นกลไกที่ช่วยให้ภาษาโปรแกรมสามารถเรียกใช้ฟังก์ชันที่เขียนด้วยภาษาอื่นได้ ในบริบทของ Dart และ Flutter, FFI หมายถึงความสามารถในการเรียกใช้ฟังก์ชันจากไลบรารี C/C++ โดยตรงจากโค้ด Dart โดยไม่ต้องเขียนโค้ดเฉพาะแพลตฟอร์มใน Java (Android) หรือ Swift/Objective-C (iOS)
แพ็คเกจ dart:ffi ปรากฏใน Dart 2.12 (2021) และนับตั้งแต่นั้นมาก็กลายเป็นเครื่องมือสำคัญในการรวม Flutter เข้ากับโค้ดดั้งเดิม ก่อน dart:ffi วิธีเดียวที่จะเรียกใช้ฟังก์ชัน C จาก Dart คือผ่าน Method Channel — กลไกแบบอะซิงโครนัสที่ส่งข้อความผ่านการทำให้เป็น JSON ระหว่าง Dart และฝั่งดั้งเดิม FFI ทำงานแตกต่าง: โค้ด Dart เข้าถึงหน่วยความจำของไลบรารี C โดยตรง เรียกใช้ฟังก์ชันผ่าน ABI ดั้งเดิม (Application Binary Interface) โดยไม่ต้องทำให้เป็นอนุกรมหรือสลับบริบท
FFI เป็นที่ต้องการโดยเฉพาะในสถานการณ์ที่ประสิทธิภาพมีความสำคัญ: การประมวลผลภาพ (OpenCV), เสียง (FFmpeg), การเข้ารหัส (OpenSSL), การเรียนรู้ของเครื่อง (TensorFlow Lite) และฐานข้อมูล (SQLite) ในทุกกรณีเหล่านี้ Method Channel สร้างความล่าช้าที่ยอมรับไม่ได้ ในขณะที่ FFI ให้ประสิทธิภาพที่เทียบเท่ากับโค้ด C/C++ ดั้งเดิม ไลบรารี dart:ffi ยังรองรับการจัดการหน่วยความจำ: การจัดสรร, การปลดปล่อย และการจัดการพอยน์เตอร์
Method Channel ทำงานแบบอะซิงโครนัส: Dart ส่งข้อความไปยังโค้ดดั้งเดิม โค้ดดั้งเดิมประมวลผลและส่งผลลัพธ์กลับมา แต่ละการเรียกใช้ต้องการการทำให้อาร์กิวเมนต์เป็นอนุกรมใน Map, การส่งผ่านคิว และการดีซีเรียลไลซ์ ซึ่งใช้เวลา 0.5–5 ms ต่อการเรียกใช้ FFI ทำงานแบบซิงโครนัสและไม่ต้องทำให้เป็นอนุกรม — การเรียกใช้ฟังก์ชัน C ใช้เวลา 0.01–0.1 µs ความแตกต่าง 50–500 เท่า ซึ่งสำคัญสำหรับการดำเนินงานที่มีความถี่สูง
การทำงานกับ dart:ffi ประกอบด้วยสามขั้นตอน: การโหลดไลบรารี, การประกาศลายเซ็น และการเรียกใช้ฟังก์ชัน แต่ละขั้นตอนใช้การกำหนดชนิดที่เข้มงวดของ Dart ซึ่งลดข้อผิดพลาดขณะรันไทม์
ในขั้นตอนแรก ไลบรารีแบบไดนามิกจะถูกโหลดผ่านคลาส DynamicLibrary ไลบรารีสามารถโหลดได้ด้วยชื่อ (libxyz.so, libxyz.dylib, xyz.dll) หรือด้วยพาธแบบเต็ม Dart จะค้นหาไลบรารีในพาธมาตรฐานของระบบโดยอัตโนมัติ DynamicLibrary จัดเตรียมเมธอด lookupFunction ซึ่งเชื่อมโยงฟังก์ชัน Dart กับฟังก์ชัน C ด้วยชื่อสัญลักษณ์
ในขั้นตอนที่สอง ฟังก์ชัน Dart จะถูกประกาศพร้อมคำอธิบายชนิดที่ตรงกับลายเซ็น C ชนิดพิเศษจาก dart:ffi ถูกใช้: Int32, Float, Double, Pointer, NativeFunction, Handle และอื่น ๆ คำอธิบาย lookupFunction รับพารามิเตอร์เจเนอริกสองตัว: ชนิดฟังก์ชัน Dart (จะมีลักษณะอย่างไรใน Dart) และชนิดฟังก์ชัน C ดั้งเดิม (ประกาศใน C อย่างไร)
ในขั้นตอนที่สาม ฟังก์ชัน Dart ที่สร้างขึ้นจะถูกเรียกใช้เหมือนฟังก์ชันปกติ อาร์กิวเมนต์ถูกส่งโดยตรง ผลลัพธ์ถูกส่งคืนทันที หากฟังก์ชัน C แก้ไขหน่วยความจำผ่านพอยน์เตอร์ Dart สามารถอ่านการเปลี่ยนแปลงเหล่านี้ผ่านคลาส Pointer การจัดการหน่วยความจำ ฝั่ง C ยังคงเป็นความรับผิดชอบของนักพัฒนา — dart:ffi ไม่จัดการหน่วยความจำที่จัดสรรโดย malloc ใน C
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// การประกาศฟังก์ชัน 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
}
ในตัวอย่างนี้ add คือฟังก์ชัน C ที่รับ int สองตัวและคืนค่า int หนึ่งตัว typedef AddNative อธิบายลายเซ็น C ด้วยชนิด dart:ffi ในขณะที่ AddDart อธิบายว่าฟังก์ชันนี้จะมีลักษณะอย่างไรใน Dart lookupFunction เชื่อมโยงพวกมันและคืนค่า ฟังก์ชัน Dart ที่สามารถเรียกใช้ได้เหมือนปกติ
dart:ffi จัดเตรียมชุดชนิดที่สอดคล้องกับชนิด C แต่ละชนิดมีขนาดคงที่และกฎการแปลงระหว่าง Dart และ C การทำความเข้าใจการจับคู่ชนิดมีความสำคัญอย่างยิ่งต่อการทำงานที่ถูกต้องของ FFI — ข้อผิดพลาดในขนาดหรือเครื่องหมายของชนิดอาจทำให้แอปพลิเคชันล่ม
| ชนิด C | ชนิด dart:ffi | ชนิด Dart | ขนาด (ไบต์) |
|---|---|---|---|
| int | Int32 | int | 4 |
| long | Int64 | int | 8 |
| float | Float | double | 4 |
| double | Double | double | 8 |
| char* | Pointer<Int8> | Pointer | 8 (พอยน์เตอร์) |
| void* | Pointer<Void> | Pointer | 8 (พอยน์เตอร์) |
| struct | Pointer<T> (Struct) | Pointer | ขึ้นอยู่กับฟิลด์ |
สำหรับการทำงานกับสตริง C (char*) นั้น dart:ffi ใช้ Pointer<Int8> การแปลงจาก Dart String เป็น C char* และกลับกันดำเนินการผ่าน toNativeUtf8 (จากแพ็คเกจ ffi) และ fromUtf8 สิ่งสำคัญคือต้องปลดปล่อยสตริง C หลังการใช้งานผ่าน calloc.free เพื่อป้องกันหน่วยความจำรั่ว
dart:ffi รองรับการประกาศโครงสร้าง C เป็นคลาส Dart ที่สืบทอด Struct ฟิลด์ของโครงสร้างถูกประกาศด้วยคำอธิบาย @Int32(), @Float(), @Array() และอื่น ๆ ขนาดและออฟเซ็ตของฟิลด์ถูกคำนวณโดยอัตโนมัติตาม ABI ของแพลตฟอร์ม Pointer<Point> สามารถรับได้จากฟังก์ชัน C ที่คืนค่าพอยน์เตอร์ไปยังโครงสร้าง หรือจัดสรรใน Dart ผ่าน calloc
// โครงสร้าง C: typedef struct { int x; int y; } Point;
final class Point extends Struct {
@Int32()
external int x
@Int32()
external int y
}
// เรียกใช้ฟังก์ชัน C ที่คืนค่า 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) // ปลดปล่อยหน่วยความจำ
คลาส Point สืบทอด Struct และประกาศฟิลด์ x และ y ด้วยคำอธิบาย @Int32() โค้ด C ที่สร้างขึ้นจะมีรูปแบบหน่วยความจำเหมือนกันทุกประการ Pointer.ref ให้การเข้าถึงฟิลด์ของโครงสร้างผ่าน getter และ setter
ลองดูตัวอย่างที่ซับซ้อนมากขึ้น — การรวมกับไลบรารี C สำหรับการคำนวณแฮช SHA256 นี่เป็นงานทั่วไปที่ FFI ให้ประโยชน์ด้านประสิทธิภาพอย่างมีนัยสำคัญเมื่อเทียบกับ Method Channel
ไลบรารี OpenSSL จัดเตรียมฟังก์ชัน SHA256 ซึ่งคำนวณแฮชของสตริง ผ่าน dart:ffi เราสามารถเรียกใช้ได้โดยตรง โดยไม่ต้องเขียน wrapper Java หรือ Swift นี่คือตัวอย่างวิธีที่ FFI อนุญาตให้นำไลบรารี C ที่มีอยู่กลับมาใช้ใหม่ใน Flutter
import 'dart:ffi'
import 'package:ffi/ffi.dart'
// ลายเซ็น: 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 ไบต์
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
}
ในตัวอย่างนี้ ฟังก์ชัน sha256 โหลดไลบรารี libcrypto.so ค้นหาสัญลักษณ์ SHA256 และเรียกใช้ด้วยพอยน์เตอร์ไปยังข้อมูลอินพุตและเอาต์พุต toNativeUtf8 แปลง Dart String เป็นสตริง C (จัดสรรหน่วยความจำ) และ asTypedList อนุญาตให้อ่านอาร์เรย์ไบต์ผลลัพธ์ หน่วยความจำถูกปลดปล่อยหลังการใช้งาน — นี่เป็นขั้นตอนบังคับเพื่อป้องกันการรั่วไหล
แพ็คเกจ ffi จัดเตรียมฟังก์ชัน calloc สำหรับจัดสรรหน่วยความจำที่เข้ากันได้กับ C หน่วยความจำที่จัดสรรแล้วต้องถูกปลดปล่อยผ่าน calloc.free มิฉะนั้นจะเกิดการรั่วไหล สำหรับการจัดการหน่วยความจำอัตโนมัติ คุณสามารถใช้คลาส Arena จากแพ็คเกจ ffi ซึ่งปลดปล่อยหน่วยความจำทั้งหมดที่จัดสรรภายในเมื่อเรียกใช้ arena.release() สิ่งนี้สะดวกเป็นพิเศษสำหรับการจัดสรรชั่วคราวจำนวนมาก
แม้ FFI จะมีพลัง แต่ก็มีข้อจำกัดที่ต้องพิจารณาเมื่อออกแบบสถาปัตยกรรมแอปพลิเคชัน Flutter ข้อจำกัดหลักเกี่ยวข้องกับความปลอดภัยของชนิด การจัดการหน่วยความจำ และความเข้ากันได้ของแพลตฟอร์ม
FFI ไม่ตรวจสอบชนิดในขณะรันไทม์ หากฟังก์ชัน C คาดหวังพอยน์เตอร์แต่ได้รับตัวเลข แอปพลิเคชันจะล่มด้วยข้อผิดพลาด segmentation ขอแนะนำให้ใช้ FFIgen — เครื่องมือที่สร้าง wrapper Dart ที่ปลอดภัยของชนิดตามไฟล์ส่วนหัว C (.h) FFIgen วิเคราะห์การประกาศฟังก์ชัน C และสร้างโค้ด Dart ด้วยชนิดที่ถูกต้อง ซึ่งช่วยขจัดข้อผิดพลาดในขั้นตอนการเขียนโค้ด
ชื่อและพาธของไลบรารีแบบไดนามิกแตกต่างกันไปตามแพลตฟอร์ม: libxyz.so บน Android/Linux, libxyz.dylib บน iOS/macOS, xyz.dll บน Windows สำหรับไลบรารีข้ามแพลตฟอร์ม จะใช้การคอมไพล์แบบมีเงื่อนไขผ่าน dart:io (Platform.isAndroid, Platform.isIOS) หรือสิ่งที่เป็นนามธรรมเช่น package:ffi ขอแนะนำให้สร้างเมธอดโรงงานที่คืนค่าไลบรารีที่ถูกต้องสำหรับแพลตฟอร์มปัจจุบัน
FFI ไม่จัดการหน่วยความจำฝั่ง C หากฟังก์ชัน C จัดสรรหน่วยความจำผ่าน malloc จะต้องปลดปล่อยผ่าน free มิฉะนั้นจะเกิดการรั่วไหล Dart ไม่มีตัวเก็บขยะสำหรับหน่วยความจำ C คำแนะนำ: ปลดปล่อยหน่วยความจำในเมธอดเดียวกันกับที่จัดสรรเสมอ หรือใช้ Arena สำหรับการปลดปล่อยเป็นกลุ่ม
การเรียกใช้ FFI ทำงานในเธรดเดียวกับโค้ด Dart การดำเนินการแบบซิงโครนัสที่ยาวนาน (มากกว่า 10 ms) จะบล็อกเธรด UI และทำให้เฟรมดรอป สำหรับการดำเนินการที่ยาวนาน ควรเรียกใช้ฟังก์ชัน C ใน Isolate หรือตรวจสอบให้แน่ใจว่าฟังก์ชัน C ทำงานในเธรดพื้นหลังและแจ้ง Dart ผ่าน Port หรือ callback
// FFI ใน Isolate สำหรับการดำเนินงานที่ยาวนาน
import 'dart:isolate'
Future<String> computeHash(String input) async {
final port = ReceivePort()
await Isolate.spawn((SendPort sendPort) {
final result = sha256(input) // การเรียกใช้ FFI
sendPort.send(result)
}, port.sendPort)
return await port.first as String
}
การย้ายการเรียกใช้ FFI ไปยัง Isolate ช่วยให้มั่นใจได้ว่าเธรด UI จะไม่ถูกบล็อก อย่างไรก็ตาม ควรทราบว่าการถ่ายโอนข้อมูลจำนวนมากระหว่าง Isolate ต้องการการคัดลอกหน่วยความจำ สำหรับบัฟเฟอร์ขนาดใหญ่ (>10 MB) ควรใช้ SharedMemory หรือไฟล์ที่แมปหน่วยความจำ
คำถามที่พบบ่อย
FFI เรียกใช้ฟังก์ชัน C โดยตรง แบบซิงโครนัส และไม่ต้องทำให้เป็นอนุกรม — ความหน่วง 0.01–0.1 µs Method Channel ทำงานแบบอะซิงโครนัสผ่านการทำให้เป็น JSON ด้วยความหน่วง 0.5–5 ms FFI เหมาะสำหรับการดำเนินงานที่มีประสิทธิภาพสูง, Method Channel เหมาะสำหรับการเรียกใช้ API แพลตฟอร์มอย่างง่าย
โดยตรง — ไม่ได้, dart:ffi รองรับเฉพาะฟังก์ชัน C เท่านั้น ในการเรียกใช้ C++ คุณต้องสร้าง wrapper C ด้วย extern “C” (จุดเริ่มต้นที่ส่งออกเป็นสัญลักษณ์ C) คลาส C++ ต้องการชั้นเพิ่มเติมที่แปลงการเรียกใช้เมธอดเป็นฟังก์ชัน C
FFI ไม่สนับสนุนข้อยกเว้น — หากฟังก์ชัน C คืนรหัสข้อผิดพลาด จะต้องตรวจสอบด้วยตนเอง ขอแนะนำให้ห่อการเรียกใช้ FFI ใน try-catch ใน Dart และตรวจสอบรหัสส่งคืนของ ฟังก์ชัน C ข้อผิดพลาดร้ายแรง (segfault) ไม่สามารถตรวจจับได้
FFI ไม่ทำงานกับไลบรารีที่ต้องการการเริ่มต้นที่ซับซ้อนของ Java (JNI) หรือ Objective-C (Message Dispatch) ตัวอย่างเช่น UIKit และ Android Views ไม่สามารถเข้าถึงได้ผ่าน FFI ข้อจำกัด เกี่ยวข้องกับความจริงที่ว่า FFI ทำงานที่ระดับ ABI C ในขณะที่ API เหล่านี้ต้องการรันไทม์เฉพาะ
ใช่ ไลบรารี C จะถูกคอมไพล์แยกกันสำหรับแต่ละแพลตฟอร์มเป้าหมาย สำหรับ Android นั้น .so จะถูกสร้างสำหรับ ABI ที่แตกต่างกัน (armeabi-v7a, arm64-v8a, x86_64) สำหรับ iOS — .dylib สากล (arm64) สำหรับ Windows — .dll Flutter จะบรรจุเวอร์ชันไลบรารีที่ถูกต้องโดยอัตโนมัติระหว่างการสร้าง
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ