FFI: พื้นฐาน, Foreign Function Interface และการรวมเข้ากับ C

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-06-05 เวลาอ่าน: 10 นาที

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) — กลไกการเรียกใช้ฟังก์ชัน C โดยตรงจาก Dart
  • dart:ffi จัดเตรียม API สำหรับโหลดไลบรารีและประกาศลายเซ็น
  • ประสิทธิภาพ ของการเรียกใช้ FFI สูงกว่า Method Channel 50–100 เท่า
  • ชนิดข้อมูล FFI รองรับชนิดพื้นฐาน, โครงสร้าง และพอยน์เตอร์ C
  • Flutter ใช้ FFI สำหรับการรวมกับไลบรารีดั้งเดิม: OpenCV, SQLite, FFmpeg

FFI คืออะไร?

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 ยังรองรับการจัดการหน่วยความจำ: การจัดสรร, การปลดปล่อย และการจัดการพอยน์เตอร์

FFI เทียบกับ Method Channel: ความแตกต่างพื้นฐาน

Method Channel ทำงานแบบอะซิงโครนัส: Dart ส่งข้อความไปยังโค้ดดั้งเดิม โค้ดดั้งเดิมประมวลผลและส่งผลลัพธ์กลับมา แต่ละการเรียกใช้ต้องการการทำให้อาร์กิวเมนต์เป็นอนุกรมใน Map, การส่งผ่านคิว และการดีซีเรียลไลซ์ ซึ่งใช้เวลา 0.5–5 ms ต่อการเรียกใช้ FFI ทำงานแบบซิงโครนัสและไม่ต้องทำให้เป็นอนุกรม — การเรียกใช้ฟังก์ชัน C ใช้เวลา 0.01–0.1 µs ความแตกต่าง 50–500 เท่า ซึ่งสำคัญสำหรับการดำเนินงานที่มีความถี่สูง

dart:ffi ทำงานอย่างไร?

การทำงานกับ 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

ตัวอย่างพื้นฐาน: การเรียกใช้ฟังก์ชัน C จาก Dart

dart
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 ที่สามารถเรียกใช้ได้เหมือนปกติ

ชนิดข้อมูลใน FFI

dart:ffi จัดเตรียมชุดชนิดที่สอดคล้องกับชนิด C แต่ละชนิดมีขนาดคงที่และกฎการแปลงระหว่าง Dart และ C การทำความเข้าใจการจับคู่ชนิดมีความสำคัญอย่างยิ่งต่อการทำงานที่ถูกต้องของ FFI — ข้อผิดพลาดในขนาดหรือเครื่องหมายของชนิดอาจทำให้แอปพลิเคชันล่ม

ชนิด Cชนิด dart:ffiชนิด Dartขนาด (ไบต์)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8 (พอยน์เตอร์)
void*Pointer<Void>Pointer8 (พอยน์เตอร์)
structPointer<T> (Struct)Pointerขึ้นอยู่กับฟิลด์

สำหรับการทำงานกับสตริง C (char*) นั้น dart:ffi ใช้ Pointer<Int8> การแปลงจาก Dart String เป็น C char* และกลับกันดำเนินการผ่าน toNativeUtf8 (จากแพ็คเกจ ffi) และ fromUtf8 สิ่งสำคัญคือต้องปลดปล่อยสตริง C หลังการใช้งานผ่าน calloc.free เพื่อป้องกันหน่วยความจำรั่ว

โครงสร้าง (Struct)

dart:ffi รองรับการประกาศโครงสร้าง C เป็นคลาส Dart ที่สืบทอด Struct ฟิลด์ของโครงสร้างถูกประกาศด้วยคำอธิบาย @Int32(), @Float(), @Array() และอื่น ๆ ขนาดและออฟเซ็ตของฟิลด์ถูกคำนวณโดยอัตโนมัติตาม ABI ของแพลตฟอร์ม Pointer<Point> สามารถรับได้จากฟังก์ชัน C ที่คืนค่าพอยน์เตอร์ไปยังโครงสร้าง หรือจัดสรรใน Dart ผ่าน calloc

dart
// โครงสร้าง 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

ตัวอย่างการใช้งาน FFI

ลองดูตัวอย่างที่ซับซ้อนมากขึ้น — การรวมกับไลบรารี C สำหรับการคำนวณแฮช SHA256 นี่เป็นงานทั่วไปที่ FFI ให้ประโยชน์ด้านประสิทธิภาพอย่างมีนัยสำคัญเมื่อเทียบกับ Method Channel

การรวมกับ OpenSSL ผ่าน FFI

ไลบรารี OpenSSL จัดเตรียมฟังก์ชัน SHA256 ซึ่งคำนวณแฮชของสตริง ผ่าน dart:ffi เราสามารถเรียกใช้ได้โดยตรง โดยไม่ต้องเขียน wrapper Java หรือ Swift นี่คือตัวอย่างวิธีที่ FFI อนุญาตให้นำไลบรารี C ที่มีอยู่กลับมาใช้ใหม่ใน Flutter

dart
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

dart
// 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 แตกต่างจาก Method Channel อย่างไร?

FFI เรียกใช้ฟังก์ชัน C โดยตรง แบบซิงโครนัส และไม่ต้องทำให้เป็นอนุกรม — ความหน่วง 0.01–0.1 µs Method Channel ทำงานแบบอะซิงโครนัสผ่านการทำให้เป็น JSON ด้วยความหน่วง 0.5–5 ms FFI เหมาะสำหรับการดำเนินงานที่มีประสิทธิภาพสูง, Method Channel เหมาะสำหรับการเรียกใช้ API แพลตฟอร์มอย่างง่าย

สามารถเรียกใช้ฟังก์ชัน C++ ผ่าน FFI ได้หรือไม่?

โดยตรง — ไม่ได้, dart:ffi รองรับเฉพาะฟังก์ชัน C เท่านั้น ในการเรียกใช้ C++ คุณต้องสร้าง wrapper C ด้วย extern “C” (จุดเริ่มต้นที่ส่งออกเป็นสัญลักษณ์ C) คลาส C++ ต้องการชั้นเพิ่มเติมที่แปลงการเรียกใช้เมธอดเป็นฟังก์ชัน C

วิธีจัดการข้อผิดพลาดในฟังก์ชัน C?

FFI ไม่สนับสนุนข้อยกเว้น — หากฟังก์ชัน C คืนรหัสข้อผิดพลาด จะต้องตรวจสอบด้วยตนเอง ขอแนะนำให้ห่อการเรียกใช้ FFI ใน try-catch ใน Dart และตรวจสอบรหัสส่งคืนของ ฟังก์ชัน C ข้อผิดพลาดร้ายแรง (segfault) ไม่สามารถตรวจจับได้

ไลบรารีใดที่ไม่สามารถใช้ผ่าน FFI ได้?

FFI ไม่ทำงานกับไลบรารีที่ต้องการการเริ่มต้นที่ซับซ้อนของ Java (JNI) หรือ Objective-C (Message Dispatch) ตัวอย่างเช่น UIKit และ Android Views ไม่สามารถเข้าถึงได้ผ่าน FFI ข้อจำกัด เกี่ยวข้องกับความจริงที่ว่า FFI ทำงานที่ระดับ ABI C ในขณะที่ API เหล่านี้ต้องการรันไทม์เฉพาะ

จำเป็นต้องคอมไพล์ไลบรารี C สำหรับแต่ละแพลตฟอร์มหรือไม่?

ใช่ ไลบรารี C จะถูกคอมไพล์แยกกันสำหรับแต่ละแพลตฟอร์มเป้าหมาย สำหรับ Android นั้น .so จะถูกสร้างสำหรับ ABI ที่แตกต่างกัน (armeabi-v7a, arm64-v8a, x86_64) สำหรับ iOS — .dylib สากล (arm64) สำหรับ Windows — .dll Flutter จะบรรจุเวอร์ชันไลบรารีที่ถูกต้องโดยอัตโนมัติระหว่างการสร้าง

สรุป

  • FFI (Foreign Function Interface) — กลไกการเรียกใช้ฟังก์ชัน C โดยตรงจาก Dart ผ่าน dart:ffi
  • ประสิทธิภาพ ของการเรียกใช้ FFI สูงกว่า Method Channel 50–500 เท่า
  • สถาปัตยกรรม รวมถึงการโหลดไลบรารี การประกาศลายเซ็น และการเรียกใช้ฟังก์ชัน
  • ชนิดข้อมูล dart:ffi รองรับ Int32, Float, Double, Pointer, Struct และชนิด C อื่น ๆ
  • หน่วยความจำ ฝั่ง C จัดการด้วยตนเองผ่าน calloc/free หรือ Arena
  • ข้อจำกัด ของ FFI: ไม่มีการตรวจสอบชนิดขณะรันไทม์, ไม่รองรับ C++ โดยตรง, บล็อกเธรด UI
  • ใช้ FFI สำหรับการรวมประสิทธิภาพสูงกับไลบรารีดั้งเดิมใน Flutter

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม