การใช้ FFI ในปลั๊กอิน Flutter

การใช้ FFI ในปลั๊กอิน Flutter

เกี่ยวกับ Codelab นี้

subjectอัปเดตล่าสุดเมื่อ มิ.ย. 23, 2025
account_circleเขียนโดย Brett Morgan and Maksim Lin

1 บทนำ

FFI (Foreign Function Interface) ของ Dart ช่วยให้แอป Flutter ใช้ประโยชน์จากไลบรารีเนทีฟที่มีอยู่ซึ่งแสดง C API ได้ Dart รองรับ FFI ใน Android, iOS, Windows, macOS และ Linux สําหรับเว็บ Dart รองรับการทำงานร่วมกันของ JavaScript แต่หัวข้อนี้ไม่อยู่ใน Codelab นี้

สิ่งที่คุณจะสร้าง

ในโค้ดแล็บนี้ คุณจะได้สร้างปลั๊กอินสำหรับอุปกรณ์เคลื่อนที่และเดสก์ท็อปที่ใช้ไลบรารี C คุณจะใช้ API นี้เพื่อเขียนแอปตัวอย่างที่ใช้ปลั๊กอิน ปลั๊กอินและแอปจะทําสิ่งต่อไปนี้

  • นําเข้าซอร์สโค้ดของไลบรารี C ไปยังปลั๊กอิน Flutter ใหม่
  • ปรับแต่งปลั๊กอินเพื่อให้สร้างได้ใน Windows, macOS, Linux, Android และ iOS
  • สร้างแอปพลิเคชันที่ใช้ปลั๊กอินสําหรับ REPL (read reveal print loop) ของ JavaScript

Duktape REPL ที่ทำงานเป็นแอปพลิเคชัน macOS

สิ่งที่คุณจะได้เรียนรู้

ในโค้ดแล็บนี้ คุณจะได้เรียนรู้ความรู้เชิงปฏิบัติที่จำเป็นต่อการสร้างปลั๊กอิน Flutter ที่ใช้ FFI ทั้งบนแพลตฟอร์มเดสก์ท็อปและอุปกรณ์เคลื่อนที่ ซึ่งรวมถึงสิ่งต่อไปนี้

  • การสร้างเทมเพลตปลั๊กอิน Flutter ที่ใช้ Dart FFI
  • การใช้แพ็กเกจ ffigen เพื่อสร้างโค้ดการเชื่อมโยงสำหรับไลบรารี C
  • การใช้ CMake เพื่อสร้างปลั๊กอิน FFI ของ Flutter สำหรับ Android, Windows และ Linux
  • การใช้ CocoaPods เพื่อสร้างปลั๊กอิน Flutter FFI สำหรับ iOS และ macOS

สิ่งที่คุณต้องมี

  • Android Studio 4.1 ขึ้นไปสำหรับการพัฒนาแอป Android
  • Xcode 13 ขึ้นไปสำหรับการพัฒนา iOS และ macOS
  • Visual Studio 2022 หรือ Visual Studio Build Tools 2022 ที่มีเวิร์กโหลด "การพัฒนาเดสก์ท็อปด้วย C++" สําหรับการพัฒนาเดสก์ท็อป Windows
  • Flutter SDK
  • เครื่องมือสร้างที่จำเป็นสำหรับแพลตฟอร์มที่คุณจะใช้พัฒนา (เช่น CMake, CocoaPods และอื่นๆ)
  • LLVM สําหรับแพลตฟอร์มที่คุณจะพัฒนา ffigen ใช้ชุดเครื่องมือคอมไพเลอร์ LLVM เพื่อแยกวิเคราะห์ไฟล์ส่วนหัว C เพื่อสร้างการเชื่อมโยง FFI ที่แสดงใน Dart
  • ตัวแก้ไขโค้ด เช่น Visual Studio Code

2 เริ่มต้นใช้งาน

เครื่องมือ ffigen เป็นเครื่องมือที่เพิ่งเพิ่มเข้ามาใน Flutter คุณสามารถยืนยันว่าการติดตั้ง Flutter ของคุณใช้รุ่นเสถียรปัจจุบันอยู่โดยเรียกใช้คำสั่งต่อไปนี้

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.32.4, on macOS 15.5 24F74 darwin-arm64, locale en-AU)
[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 16.4)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2024.2)
[✓] IntelliJ IDEA Community Edition (version 2024.3.1.1)
[✓] VS Code (version 1.101.0)
[✓] Connected device (3 available)
[✓] Network resources

• No issues found!

ตรวจสอบว่าเอาต์พุต flutter doctor ระบุว่าคุณใช้เวอร์ชันเสถียร และไม่มี Flutter เวอร์ชันเสถียรที่ใหม่กว่าพร้อมใช้งาน หากคุณไม่ได้ใช้เวอร์ชันเสถียรหรือมีรุ่นที่ใหม่กว่า ให้เรียกใช้ 2 คำสั่งต่อไปนี้เพื่ออัปเดตเครื่องมือ Flutter

flutter channel stable
flutter upgrade

คุณเรียกใช้โค้ดในโค้ดแล็บนี้ได้โดยใช้อุปกรณ์ต่อไปนี้

  • คอมพิวเตอร์สำหรับการพัฒนา (สำหรับบิลด์เดสก์ท็อปของปลั๊กอินและแอปตัวอย่าง)
  • อุปกรณ์ Android หรือ iOS จริงที่เชื่อมต่อกับคอมพิวเตอร์และตั้งค่าเป็นโหมดนักพัฒนาแอป
  • เครื่องจำลอง iOS (ต้องติดตั้งเครื่องมือ Xcode)
  • โปรแกรมจำลอง Android (ต้องมีการตั้งค่าใน Android Studio)

3 สร้างเทมเพลตปลั๊กอิน

เริ่มต้นใช้งานการพัฒนาปลั๊กอิน Flutter

Flutter มาพร้อมกับเทมเพลตสำหรับปลั๊กอินที่จะช่วยให้คุณเริ่มต้นใช้งานได้ เมื่อสร้างเทมเพลตปลั๊กอิน คุณจะระบุภาษาที่ต้องการใช้ได้

เรียกใช้คําสั่งต่อไปนี้ในไดเรกทอรีทํางานเพื่อสร้างโปรเจ็กต์โดยใช้เทมเพลตปลั๊กอิน

flutter create --template=plugin_ffi --platforms=android,ios,linux,macos,windows ffigen_app

พารามิเตอร์ --platforms จะระบุแพลตฟอร์มที่พลิแกนจะรองรับ

คุณสามารถตรวจสอบเลย์เอาต์ของโปรเจ็กต์ที่สร้างขึ้นได้โดยใช้คำสั่ง tree หรือโปรแกรมสำรวจไฟล์ของระบบปฏิบัติการ

$ tree -L 2 ffigen_app
ffigen_app
├── CHANGELOG.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── android
│   ├── build.gradle
│   ├── ffigen_app_android.iml
│   ├── local.properties
│   ├── settings.gradle
│   └── src
├── example
│   ├── README.md
│   ├── analysis_options.yaml
│   ├── android
│   ├── ffigen_app_example.iml
│   ├── ios
│   ├── lib
│   ├── linux
│   ├── macos
│   ├── pubspec.lock
│   ├── pubspec.yaml
│   └── windows
├── ffigen.yaml
├── ffigen_app.iml
├── ios
│   ├── Classes
│   └── ffigen_app.podspec
├── lib
│   ├── ffigen_app.dart
│   └── ffigen_app_bindings_generated.dart
├── linux
│   └── CMakeLists.txt
├── macos
│   ├── Classes
│   └── ffigen_app.podspec
├── pubspec.lock
├── pubspec.yaml
├── src
│   ├── CMakeLists.txt
│   ├── ffigen_app.c
│   └── ffigen_app.h
└── windows
    └── CMakeLists.txt

17 directories, 26 files

คุณควรใช้เวลาสักครู่เพื่อดูโครงสร้างไดเรกทอรีเพื่อให้ทราบสิ่งที่สร้างขึ้นและตำแหน่งของสิ่งนั้น เทมเพลต plugin_ffi จะวางโค้ด Dart สําหรับปลั๊กอินไว้ในส่วน lib ซึ่งเป็นไดเรกทอรีเฉพาะแพลตฟอร์มที่มีชื่อว่า android, ios, linux, macos และ windows และที่สำคัญที่สุดคือไดเรกทอรี example

สําหรับนักพัฒนาซอฟต์แวร์ที่คุ้นเคยกับการพัฒนา Flutter แบบปกติ โครงสร้างนี้อาจดูแปลกๆ เนื่องจากไม่มีไฟล์ปฏิบัติการที่กําหนดไว้ที่ระดับบนสุด ปลั๊กอินมีไว้เพื่อรวมไว้ในโปรเจ็กต์ Flutter อื่นๆ แต่คุณจะเขียนโค้ดในไดเรกทอรี example เพื่อยืนยันว่าโค้ดปลั๊กอินใช้งานได้

ได้เวลาเริ่มต้นใช้งานแล้ว

4 สร้างและเรียกใช้ตัวอย่าง

โปรดตรวจสอบว่าได้ติดตั้งระบบบิลด์และข้อกําหนดเบื้องต้นอย่างถูกต้องและใช้งานได้กับแพลตฟอร์มที่รองรับแต่ละแพลตฟอร์มแล้ว โดยให้สร้างและเรียกใช้แอปตัวอย่างที่สร้างขึ้นสําหรับแต่ละเป้าหมาย

Windows

ตรวจสอบว่าคุณใช้ Windows เวอร์ชันที่รองรับ เป็นที่ทราบกันดีว่าโค้ดแล็บนี้ใช้งานได้ใน Windows 10 และ Windows 11

คุณสามารถสร้างแอปพลิเคชันจากภายในเครื่องมือแก้ไขโค้ดหรือในบรรทัดคำสั่งก็ได้

PS C:\Users\brett\Documents> cd .\ffigen_app\example\
PS C:\Users\brett\Documents\ffigen_app\example> flutter run -d windows
Launching lib\main.dart on Windows in debug mode...Building Windows application...
Syncing files to device Windows...                                 160ms

Flutter run key commands.
r Hot reload.
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

 Running with sound null safety

An Observatory debugger and profiler on Windows is available at: http://127.0.0.1:53317/OiKWpyHXxHI=/
The Flutter DevTools debugger and profiler on Windows is available at:
http://127.0.0.1:9100?uri=http://127.0.0.1:53317/OiKWpyHXxHI=/

คุณควรเห็นหน้าต่างแอปที่ทำงานอยู่ดังต่อไปนี้

แอป FFI ที่สร้างขึ้นจากเทมเพลตซึ่งทำงานเป็นแอป Windows

Linux

ตรวจสอบว่าคุณใช้ Linux เวอร์ชันที่รองรับ Codelab นี้ใช้ Ubuntu 22.04.1

เมื่อติดตั้งข้อกําหนดเบื้องต้นทั้งหมดที่ระบุไว้ในขั้นตอนที่ 2 แล้ว ให้เรียกใช้คําสั่งต่อไปนี้ในเทอร์มินัล

$ cd ffigen_app/example
$ flutter run -d linux
Launching lib/main.dart on Linux in debug mode...
Building Linux application...
Syncing files to device Linux...                                   504ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on Linux is available at: http://127.0.0.1:36653/Wgek1JGag48=/
The Flutter DevTools debugger and profiler on Linux is available at:
http://127.0.0.1:9103?uri=http://127.0.0.1:36653/Wgek1JGag48=/

คุณควรเห็นหน้าต่างแอปที่ทำงานอยู่ดังต่อไปนี้

แอป FFI ที่สร้างขึ้นจากเทมเพลตซึ่งทํางานเป็นแอปพลิเคชัน Linux

Android

สำหรับ Android คุณใช้ Windows, macOS หรือ Linux ในการคอมไพล์ได้

คุณต้องทําการเปลี่ยนแปลงใน example/android/app/build.gradle.kts เพื่อใช้ NDK เวอร์ชันที่เหมาะสม

example/android/app/build.gradle.kts)

android {
   
// Modify the next line from `flutter.ndkVersion` to the following:
    ndkVersion
= "27.0.12077973"
   
// ...
}

ตรวจสอบว่าคุณมีอุปกรณ์ Android ที่เชื่อมต่อกับคอมพิวเตอร์สำหรับการพัฒนาหรือกำลังเรียกใช้อินสแตนซ์ Android Emulator (AVD) ตรวจสอบว่า Flutter เชื่อมต่อกับอุปกรณ์ Android หรือโปรแกรมจำลองได้โดยการเรียกใช้คำสั่งต่อไปนี้

$ flutter devices
3 connected devices:

sdk gphone64 arm64 (mobile) • emulator-5554 • android-arm64  • Android 12 (API 32) (emulator)
macOS (desktop)             • macos         • darwin-arm64   • macOS 13.1 22C65 darwin-arm
Chrome (web)                • chrome        • web-javascript • Google Chrome 108.0.5359.98

เมื่อคุณมีอุปกรณ์ Android ที่ทำงานอยู่ ไม่ว่าจะเป็นอุปกรณ์จริงหรือโปรแกรมจำลอง ให้เรียกใช้คำสั่งต่อไปนี้

cd ffigen_app/example
flutter run

Flutter จะถามคุณว่าต้องการเรียกใช้บนอุปกรณ์ใด เลือกอุปกรณ์ที่เหมาะสมจากรายการ

แอป FFI ที่สร้างขึ้นจากเทมเพลตซึ่งทำงานในโปรแกรมจำลอง Android

macOS และ iOS

สำหรับการพัฒนา Flutter ใน macOS และ iOS คุณต้องใช้คอมพิวเตอร์ macOS

เริ่มต้นด้วยการเปิดแอปตัวอย่างใน macOS ตรวจสอบอีกครั้งว่า Flutter เห็นอุปกรณ์ใดบ้าง

$ flutter devices
2 connected devices:

macOS (desktop) • macos  • darwin-arm64   • macOS 13.1 22C65 darwin-arm
Chrome (web)    • chrome • web-javascript • Google Chrome 108.0.5359.98

เรียกใช้แอปตัวอย่างโดยใช้โปรเจ็กต์ปลั๊กอินที่สร้างขึ้น

cd ffigen_app/example
flutter run -d macos

คุณควรเห็นหน้าต่างแอปที่ทำงานอยู่ดังต่อไปนี้

แอป FFI ที่สร้างขึ้นจากเทมเพลตซึ่งทํางานเป็นแอปพลิเคชัน Linux

สำหรับ iOS คุณสามารถใช้โปรแกรมจำลองหรืออุปกรณ์ฮาร์ดแวร์จริง หากใช้เครื่องจำลอง ให้เปิดเครื่องจำลองก่อน ตอนนี้คำสั่ง flutter devices จะแสดงเครื่องจำลองเป็นหนึ่งในอุปกรณ์ที่ใช้ได้

$ flutter devices
3 connected devices:

iPhone SE (3rd generation) (mobile) • 1BCBE334-7EC4-433A-90FD-1BC14F3BA41F • ios            • com.apple.CoreSimulator.SimRuntime.iOS-16-1 (simulator)
macOS (desktop)                     • macos                                • darwin-arm64   • macOS 13.1 22C65 darwin-arm
Chrome (web)                        • chrome                               • web-javascript • Google Chrome 108.0.5359.98

เมื่อคุณมีอุปกรณ์ iOS ที่ทำงานอยู่ ไม่ว่าจะเป็นอุปกรณ์จริงหรือเครื่องจำลอง ให้เรียกใช้คำสั่งต่อไปนี้

cd ffigen_app/example
flutter run

Flutter จะถามคุณว่าต้องการเรียกใช้บนอุปกรณ์ใด เลือกอุปกรณ์ที่เหมาะสมจากรายการ

แอป FFI ที่สร้างขึ้นจากเทมเพลตซึ่งทำงานในโปรแกรมจำลอง iOS

เครื่องจำลอง iOS จะมีความสําคัญเหนือกว่าเป้าหมาย macOS คุณจึงข้ามการระบุอุปกรณ์ด้วยพารามิเตอร์ -d ได้

ยินดีด้วย คุณสร้างและเรียกใช้แอปพลิเคชันในระบบปฏิบัติการ 5 ระบบที่แตกต่างกันเรียบร้อยแล้ว ขั้นตอนถัดไปคือการสร้างปลั๊กอินแบบเนทีฟและการติดต่อกับปลั๊กอินจาก Dart โดยใช้ FFI

5 ใช้ Duktape ใน Windows, Linux และ Android

ไลบรารี C ที่คุณจะใช้ในโค้ดแล็บนี้คือ Duktape Duktape เป็นเครื่องมือ JavaScript ที่ฝังได้ โดยเน้นที่ความสามารถในการเคลื่อนย้ายได้ง่ายและขนาดที่กะทัดรัด ในขั้นตอนนี้ คุณจะต้องกำหนดค่าปลั๊กอินให้คอมไพล์ไลบรารี Duktape, ลิงก์กับปลั๊กอิน แล้วเข้าถึงโดยใช้ FFI ของ Dart

ขั้นตอนนี้จะกำหนดค่าการผสานรวมให้ทำงานใน Windows, Linux และ Android การผสานรวม iOS และ macOS ต้องมีการกําหนดค่าเพิ่มเติม (นอกเหนือจากที่ระบุไว้ในขั้นตอนนี้) เพื่อรวมไลบรารีที่คอมไพล์แล้วไว้ในไฟล์ปฏิบัติการ Flutter ขั้นสุดท้าย การกำหนดค่าเพิ่มเติมที่จำเป็นจะอยู่ในขั้นตอนถัดไป

ดึงข้อมูล Duktape

ก่อนอื่น ให้ขอสำเนาซอร์สโค้ด duktape โดยดาวน์โหลดจากเว็บไซต์ duktape.org

สำหรับ Windows คุณสามารถใช้ PowerShell กับ Invoke-WebRequest ดังนี้

PS> Invoke-WebRequest -Uri https://duktape.org/duktape-2.7.0.tar.xz -OutFile duktape-2.7.0.tar.xz

สําหรับ Linux wget เป็นตัวเลือกที่ดี

$ wget https://duktape.org/duktape-2.7.0.tar.xz
--2022-12-22 16:21:39--  https://duktape.org/duktape-2.7.0.tar.xz
Resolving duktape.org (duktape.org)... 104.198.14.52
Connecting to duktape.org (duktape.org)|104.198.14.52|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1026524 (1002K) [application/x-xz]
Saving to: 'duktape-2.7.0.tar.xz'

duktape-2.7.0.tar.x 100%[===================>]   1002K  1.01MB/s    in 1.0s

2022-12-22 16:21:41 (1.01 MB/s) - 'duktape-2.7.0.tar.xz' saved [1026524/1026524]

ไฟล์เป็นไฟล์ tar.xz ใน Windows ตัวเลือกหนึ่งคือดาวน์โหลดเครื่องมือ 7Zip และใช้ดังนี้

PS> 7z x .\duktape-2.7.0.tar.xz

7-Zip 22.01 (x64) : Copyright (c) 1999-2022 Igor Pavlov : 2022-07-15

Scanning the drive for archives:
1 file, 1026524 bytes (1003 KiB)

Extracting archive: .\duktape-2.7.0.tar.xz
--
Path = .\duktape-2.7.0.tar.xz
Type = xz
Physical Size = 1026524
Method = LZMA2:26 CRC64
Streams = 1
Blocks = 1

Everything is Ok

Size:       19087360
Compressed: 1026524

คุณต้องเรียกใช้ 7z 2 ครั้ง โดยครั้งแรกเพื่อแตกไฟล์ที่บีบอัดด้วย xz และครั้งที่ 2 เพื่อขยายไฟล์เก็บถาวร tar

PS> 7z x .\duktape-2.7.0.tar

7-Zip 22.01 (x64) : Copyright (c) 1999-2022 Igor Pavlov : 2022-07-15

Scanning the drive for archives:
1 file, 19087360 bytes (19 MiB)

Extracting archive: .\duktape-2.7.0.tar
--
Path = .\duktape-2.7.0.tar
Type = tar
Physical Size = 19087360
Headers Size = 543232
Code Page = UTF-8
Characteristics = GNU ASCII

Everything is Ok

Folders: 46
Files: 1004
Size:       18281564
Compressed: 19087360

ในสภาพแวดล้อม Linux สมัยใหม่ tar จะดึงข้อมูลเนื้อหาในขั้นตอนเดียว ดังนี้

$ tar xvf duktape-2.7.0.tar.xz
x duktape-2.7.0/
x duktape-2.7.0/README.rst
x duktape-2.7.0/Makefile.sharedlibrary
x duktape-2.7.0/Makefile.coffee
x duktape-2.7.0/extras/
x duktape-2.7.0/extras/README.rst
x duktape-2.7.0/extras/module-node/
x duktape-2.7.0/extras/module-node/README.rst
x duktape-2.7.0/extras/module-node/duk_module_node.h
x duktape-2.7.0/extras/module-node/Makefile
[... and many more files]

ติดตั้ง LLVM

หากต้องการใช้ ffigen คุณต้องติดตั้ง LLVM ซึ่ง ffigen ใช้เพื่อแยกวิเคราะห์ส่วนหัว C ใน Windows ให้เรียกใช้คำสั่งต่อไปนี้

PS> winget install -e --id LLVM.LLVM
Found LLVM [LLVM.LLVM] Version 15.0.5
This application is licensed to you by its owner.
Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.
Downloading https://github.com/llvm/llvm-project/releases/download/llvmorg-15.0.5/LLVM-15.0.5-win64.exe
  ██████████████████████████████   277 MB /  277 MB
Successfully verified installer hash
Starting package install...
Successfully installed

กำหนดค่าเส้นทางของระบบเพื่อเพิ่ม C:\Program Files\LLVM\bin ลงในเส้นทางการค้นหาไบนารีเพื่อติดตั้ง LLVM ในเครื่อง Windows ให้เสร็จสมบูรณ์ คุณทดสอบได้ว่าติดตั้งอย่างถูกต้องหรือไม่โดยทำดังนี้

PS> clang --version
clang version 15.0.5
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: C:\Program Files\LLVM\bin

สำหรับ Ubuntu คุณสามารถติดตั้งข้อกำหนดของ LLVM ได้โดยทำดังนี้ ส่วนระบบปฏิบัติการ Linux อื่นๆ จะมี Dependency ที่คล้ายกันสำหรับ LLVM และ Clang

$ sudo apt install libclang-dev
[sudo] password for brett:
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  libclang-15-dev
The following NEW packages will be installed:
  libclang-15-dev libclang-dev
0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded.
Need to get 26.1 MB of archives.
After this operation, 260 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://archive.ubuntu.com/ubuntu kinetic/universe amd64 libclang-15-dev amd64 1:15.0.2-1 [26.1 MB]
Get:2 http://archive.ubuntu.com/ubuntu kinetic/universe amd64 libclang-dev amd64 1:15.0-55.1ubuntu1 [2962 B]
Fetched 26.1 MB in 7s (3748 kB/s)
Selecting previously unselected package libclang-15-dev.
(Reading database ... 85898 files and directories currently installed.)
Preparing to unpack .../libclang-15-dev_1%3a15.0.2-1_amd64.deb ...
Unpacking libclang-15-dev (1:15.0.2-1) ...
Selecting previously unselected package libclang-dev.
Preparing to unpack .../libclang-dev_1%3a15.0-55.1ubuntu1_amd64.deb ...
Unpacking libclang-dev (1:15.0-55.1ubuntu1) ...
Setting up libclang-15-dev (1:15.0.2-1) ...
Setting up libclang-dev (1:15.0-55.1ubuntu1) ...

ดังที่กล่าวไว้ข้างต้น คุณสามารถทดสอบการติดตั้ง LLVM ใน Linux ได้โดยทำดังนี้

$ clang --version
Ubuntu clang version 15.0.2-1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

กำหนดค่า ffigen

pubpsec.yaml ระดับบนสุดที่เทมเพลตสร้างขึ้นอาจมีแพ็กเกจ ffigen เวอร์ชันเก่า เรียกใช้คำสั่งต่อไปนี้เพื่ออัปเดต Dependency ของ Dart ในโปรเจ็กต์ปลั๊กอิน

flutter pub upgrade --major-versions

เมื่อแพ็กเกจ ffigen เป็นเวอร์ชันล่าสุดแล้ว ให้กำหนดค่าไฟล์ที่ ffigen จะใช้เพื่อสร้างไฟล์การเชื่อมโยง แก้ไขเนื้อหาของไฟล์ ffigen.yaml ของโปรเจ็กต์ให้ตรงกับข้อมูลต่อไปนี้

ffigen.yaml

# Run with `dart run ffigen --config ffigen.yaml`.
name: DuktapeBindings
description: |
  Bindings for `src/duktape.h`.

  Regenerate bindings with `dart run ffigen --config ffigen.yaml`.
output: 'lib/duktape_bindings_generated.dart'
headers:
  entry-points:
    - 'src/duktape.h'
  include-directives:
    - 'src/duktape.h'
preamble: |
  // ignore_for_file: always_specify_types
  // ignore_for_file: camel_case_types
  // ignore_for_file: non_constant_identifier_names
comments:
  style: any
  length: full
ignore-source-errors: true

การกําหนดค่านี้ประกอบด้วยไฟล์ส่วนหัว C เพื่อส่งไปยัง LLVM, ไฟล์เอาต์พุตที่จะสร้าง, คําอธิบายที่จะวางไว้ที่ด้านบนของไฟล์ และส่วนนําหน้าที่ใช้เพื่อเพิ่มคําเตือน Lint

มีรายการการกําหนดค่า 1 รายการที่ท้ายไฟล์ซึ่งควรได้รับการอธิบายเพิ่มเติม ตั้งแต่เวอร์ชัน 11.0.0 ของ ffigen เป็นต้นไป เครื่องมือสร้างการเชื่อมโยงจะไม่สร้างการเชื่อมโยงโดยค่าเริ่มต้นหากมีคำเตือนหรือข้อผิดพลาดที่เกิดจาก clang เมื่อแยกวิเคราะห์ไฟล์ส่วนหัว

ไฟล์ส่วนหัวของ Duktape ตามที่เขียนไว้จะทริกเกอร์ clang ใน macOS ให้แสดงคำเตือนเนื่องจากไม่มีตัวระบุประเภทที่อนุญาตค่า Null ในพอยน์เตอร์ของ Duktape หากต้องการรองรับ macOS และ iOS อย่างเต็มรูปแบบ Duktape จะต้องเพิ่มตัวระบุประเภทเหล่านี้ลงในโค้ดเบสของ Duktape ในระหว่างนี้ เราจะตัดสินใจที่จะไม่สนใจคำเตือนเหล่านี้โดยตั้งค่า Flag ignore-source-errors เป็น true

ในแอปพลิเคชันเวอร์ชันที่ใช้งานจริง คุณควรแก้ไขคำเตือนทั้งหมดจากคอมไพเลอร์ก่อนส่งแอปพลิเคชัน อย่างไรก็ตาม การทำเช่นนี้สําหรับ Duktape อยู่นอกเหนือขอบเขตของโค้ดแล็บนี้

ดูรายละเอียดเพิ่มเติมเกี่ยวกับคีย์และค่าอื่นๆ ได้ในเอกสารประกอบffigen

คุณต้องคัดลอกไฟล์ Duktape ที่เฉพาะเจาะจงจากชุดการแจกจ่าย Duktape ไปยังตำแหน่งที่กําหนดค่า ffigen ให้ค้นหาไฟล์เหล่านั้น

cp duktape-2.7.0/src/duktape.c src/
cp duktape-2.7.0/src/duktape.h src/
cp duktape-2.7.0/src/duk_config.h src/

ในทางเทคนิคแล้ว คุณจะต้องคัดลอก duktape.h สำหรับ ffigen เท่านั้น แต่คุณกำลังจะกำหนดค่า CMake เพื่อสร้างไลบรารีที่ต้องใช้ทั้ง 3 รายการ เรียกใช้ ffigen เพื่อสร้างการเชื่อมโยงใหม่

$ dart run ffigen --config ffigen.yaml
Building package executable... (1.5s)
Built ffigen:ffigen.
[INFO]   : Running in Directory: '/Users/brett/Documents/GitHub/codelabs/ffigen_codelab/step_05'
[INFO]   : Input Headers: [file:///Users/brett/Documents/GitHub/codelabs/ffigen_codelab/step_05/src/duktape.h]
[WARNING]: No definition found for declaration -(Cursor) spelling: duk_hthread, kind: 2, kindSpelling: StructDecl, type: 105, typeSpelling: struct duk_hthread, usr: c:@S@duk_hthread
[WARNING]: No definition found for declaration -(Cursor) spelling: duk_hthread, kind: 2, kindSpelling: StructDecl, type: 105, typeSpelling: struct duk_hthread, usr: c:@S@duk_hthread
[WARNING]: Generated declaration '__builtin_va_list' starts with '_' and therefore will be private.
[INFO]   : Finished, Bindings generated in /Users/brett/Documents/GitHub/codelabs/ffigen_codelab/step_05/lib/duktape_bindings_generated.dart

คุณจะเห็นคำเตือนที่แตกต่างกันในแต่ละระบบปฏิบัติการ คุณไม่ต้องสนใจข้อผิดพลาดเหล่านี้ในตอนนี้ เนื่องจาก Duktape 2.7.0 ทราบว่าคอมไพล์ด้วย clang ใน Windows, Linux และ macOS

กำหนดค่า CMake

CMake เป็นระบบการสร้างระบบการต่อยอด ปลั๊กอินนี้ใช้ CMake เพื่อสร้างระบบการบิลด์สำหรับ Android, Windows และ Linux เพื่อรวม Duktape ไว้ในไบนารี Flutter ที่สร้างขึ้น คุณต้องแก้ไขไฟล์การกำหนดค่า CMake ที่เทมเพลตสร้างขึ้น ดังนี้

src/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)

project(ffigen_app_library VERSION 0.0.1 LANGUAGES C)

add_library(ffigen_app SHARED
  duktape.c                     # Modify
)

set_target_properties(ffigen_app PROPERTIES
  PUBLIC_HEADER duktape.h       # Modify
  PRIVATE_HEADER duk_config.h   # Add
  OUTPUT_NAME "ffigen_app"      # Add
)

# Add from here...
if (WIN32)
set_target_properties(ffigen_app PROPERTIES
  WINDOWS_EXPORT_ALL_SYMBOLS ON
)
endif (WIN32)
# ... to here.

target_compile_definitions(ffigen_app PUBLIC DART_SHARED_LIB)

if (ANDROID)
  # Support Android 15 16k page size
  target_link_options(ffigen_app PRIVATE "-Wl,-z,max-page-size=16384")
endif()

การกําหนดค่า CMake จะเพิ่มไฟล์ซอร์สโค้ด และที่สำคัญกว่านั้นคือแก้ไขลักษณะการทํางานเริ่มต้นของไฟล์ไลบรารีที่สร้างขึ้นบน Windows เพื่อส่งออกสัญลักษณ์ C ทั้งหมดโดยค่าเริ่มต้น นี่เป็นวิธีแก้ปัญหาของ CMake เพื่อช่วยพอร์ตไลบรารีสไตล์ Unix ซึ่งก็คือ Duktape ไปยังโลกของ Windows

แทนที่เนื้อหาของ lib/ffigen_app.dart ด้วยข้อมูลต่อไปนี้

lib/ffigen_app.dart

import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart' as ffi;

import 'duktape_bindings_generated.dart';

const String _libName = 'ffigen_app';

final DynamicLibrary _dylib = () {
 
if (Platform.isMacOS || Platform.isIOS) {
   
return DynamicLibrary.open('$_libName.framework/$_libName');
 
}
 
if (Platform.isAndroid || Platform.isLinux) {
   
return DynamicLibrary.open('lib$_libName.so');
 
}
 
if (Platform.isWindows) {
   
return DynamicLibrary.open('$_libName.dll');
 
}
 
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();

final DuktapeBindings _bindings = DuktapeBindings(_dylib);

class Duktape {
 
Duktape() {
   
ctx = _bindings.duk_create_heap(
     
nullptr,
     
nullptr,
     
nullptr,
     
nullptr,
     
nullptr,
   
);
 
}

 
void evalString(String jsCode) {
   
var nativeUtf8 = jsCode.toNativeUtf8();
   
_bindings.duk_eval_raw(
     
ctx,
     
nativeUtf8.cast<Char>(),
     
0,
     
0 |
         
DUK_COMPILE_EVAL |
         
DUK_COMPILE_SAFE |
         
DUK_COMPILE_NOSOURCE |
         
DUK_COMPILE_STRLEN |
         
DUK_COMPILE_NOFILENAME,
   
);
   
ffi.malloc.free(nativeUtf8);
 
}

 
int getInt(int index) {
   
return _bindings.duk_get_int(ctx, index);
 
}

 
void dispose() {
   
_bindings.duk_destroy_heap(ctx);
   
ctx = nullptr;
 
}

 
late Pointer<duk_hthread> ctx;
}

ไฟล์นี้มีหน้าที่โหลดไฟล์ไลบรารีลิงก์แบบไดนามิก (.so สำหรับ Linux และ Android, .dll สำหรับ Windows) และจัดเตรียมตัวแฝงซึ่งแสดงอินเทอร์เฟซแบบ Dart มากขึ้นต่อโค้ด C ที่อยู่เบื้องหลัง

เนื่องจากไฟล์นี้จะนำเข้าแพ็กเกจ ffi โดยตรง คุณจึงต้องย้ายแพ็กเกจจาก dev_dependencies ไปยัง dependencies วิธีลัดในการดำเนินการนี้คือให้เรียกใช้คําสั่งต่อไปนี้

dart pub add ffi

แทนที่เนื้อหาของ main.dart ของตัวอย่างด้วยข้อมูลต่อไปนี้

example/lib/main.dart

import 'package:ffigen_app/ffigen_app.dart';
import 'package:flutter/material.dart';

const String jsCode = '1+2';

void main() {
 
runApp(const MyApp());
}

class MyApp extends StatefulWidget {
 
const MyApp({super.key});

 
@override
 
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
 
late Duktape duktape;
 
String output = '';

 
@override
 
void initState() {
   
super.initState();
   
duktape = Duktape();
   
setState(() {
     
output = 'Initialized Duktape';
   
});
 
}

 
@override
 
void dispose() {
   
duktape.dispose();
   
super.dispose();
 
}

 
@override
 
Widget build(BuildContext context) {
   
const textStyle = TextStyle(fontSize: 25);
   
const spacerSmall = SizedBox(height: 10);
   
return MaterialApp(
     
home: Scaffold(
       
appBar: AppBar(title: const Text('Duktape Test')),
       
body: Center(
         
child: Container(
           
padding: const EdgeInsets.all(10),
           
child: Column(
             
mainAxisSize: MainAxisSize.min,
             
children: [
               
Text(output, style: textStyle, textAlign: TextAlign.center),
               
spacerSmall,
               
ElevatedButton(
                 
child: const Text('Run JavaScript'),
                 
onPressed: () {
                   
duktape.evalString(jsCode);
                   
setState(() {
                     
output = '$jsCode => ${duktape.getInt(-1)}';
                   
});
                 
},
               
),
             
],
           
),
         
),
       
),
     
),
   
);
 
}
}

ตอนนี้คุณเรียกใช้แอปตัวอย่างอีกครั้งได้โดยใช้สิ่งต่อไปนี้

cd example
flutter run

คุณควรเห็นแอปทํางานดังต่อไปนี้

การแสดง Duktape ที่เริ่มต้นในแอปพลิเคชัน Windows

การแสดงเอาต์พุต Duktape JavaScript ในแอปพลิเคชัน Windows

ภาพหน้าจอ 2 ภาพนี้แสดงภาพก่อนและหลังการกดปุ่มเรียกใช้ JavaScript ตัวอย่างนี้แสดงการเรียกใช้โค้ด JavaScript จาก Dart และแสดงผลลัพธ์บนหน้าจอ

Android

Android เป็นระบบปฏิบัติการที่ใช้เคอร์เนล Linux และคล้ายกับระบบปฏิบัติการ Linux บนเดสก์ท็อป ระบบการคอมไพล์ CMake สามารถซ่อนความแตกต่างส่วนใหญ่ระหว่าง 2 แพลตฟอร์มนี้ได้ หากต้องการสร้างและเรียกใช้บน Android ให้ตรวจสอบว่าโปรแกรมจำลอง Android ทำงานอยู่ (หรืออุปกรณ์ Android เชื่อมต่ออยู่) เรียกใช้แอป เช่น

cd example
flutter run -d emulator-5554

ตอนนี้คุณควรเห็นแอปตัวอย่างที่ทำงานบน Android

แสดง Duktape ที่เริ่มต้นในโปรแกรมจำลอง Android

การแสดงเอาต์พุต JavaScript ของ Duktape ในโปรแกรมจำลอง Android

6 ใช้ Duktape ใน macOS และ iOS

ตอนนี้ถึงเวลาทำให้ปลั๊กอินทำงานใน macOS และ iOS ซึ่งเป็นระบบปฏิบัติการ 2 ระบบที่เกี่ยวข้องกัน เริ่มต้นด้วย macOS แม้ว่า CMake จะรองรับ macOS และ iOS แต่คุณจะใช้งานที่ทําสําหรับ Linux และ Android ซ้ำไม่ได้ เนื่องจาก Flutter ใน macOS และ iOS ใช้ CocoaPods เพื่อนําเข้าไลบรารี

ล้าง

ในขั้นตอนก่อนหน้า คุณได้สร้างแอปพลิเคชันที่ใช้งานได้สำหรับ Android, Windows และ Linux อย่างไรก็ตาม ยังมีไฟล์ 2-3 ไฟล์ที่เหลืออยู่จากเทมเพลตเดิมที่คุณต้องล้างออก นำออกเลยโดยทำดังนี้

rm src/ffigen_app.c
rm src/ffigen_app.h
rm ios/Classes/ffigen_app.c
rm macos/Classes/ffigen_app.c

macOS

Flutter ในแพลตฟอร์ม macOS ใช้ CocoaPods เพื่อนำเข้าโค้ด C และ C++ ซึ่งหมายความว่าต้องผสานรวมแพ็กเกจนี้เข้ากับโครงสร้างพื้นฐานการสร้างของ CocoaPods หากต้องการใช้โค้ด C ที่คุณกำหนดค่าไว้ให้สร้างด้วย CMake ในขั้นตอนก่อนหน้าซ้ำ คุณจะต้องเพิ่มไฟล์การส่งต่อไฟล์เดียวในโปรแกรมรันไทม์แพลตฟอร์ม macOS

macos/Classes/duktape.c

#include "../../src/duktape.c"

ไฟล์นี้ใช้ความสามารถของ C preprocessor เพื่อรวมซอร์สโค้ดจากซอร์สโค้ดเนทีฟที่คุณตั้งค่าไว้ในขั้นตอนก่อนหน้า ดูรายละเอียดเพิ่มเติมเกี่ยวกับวิธีการทำงานของแอปได้ที่ macos/ffigen_app.podspec

ตอนนี้การเรียกใช้แอปพลิเคชันนี้เป็นไปตามรูปแบบเดียวกับที่คุณเห็นใน Windows และ Linux

cd example
flutter run -d macos

การแสดง Duktape ที่เริ่มต้นในแอปพลิเคชัน macOS

การแสดงเอาต์พุต JavaScript ของ Duktape ในแอปพลิเคชัน macOS

iOS

iOS ต้องใช้ไฟล์ C ที่ส่งต่อไฟล์เดียวด้วย ซึ่งคล้ายกับการตั้งค่า macOS

ios/Classes/duktape.c

#include "../../src/duktape.c"

ไฟล์เดียวนี้จะช่วยให้ปลั๊กอินของคุณกําหนดค่าให้ทํางานบน iOS ได้ด้วย เรียกใช้ตามปกติ

flutter run -d iPhone

แสดง Duktape ที่เริ่มต้นในโปรแกรมจำลอง iOS

การแสดงเอาต์พุต JavaScript ของ Duktape ในโปรแกรมจำลอง iOS

ยินดีด้วย คุณได้ผสานรวมโค้ดที่มาพร้อมเครื่องใน 5 แพลตฟอร์มเรียบร้อยแล้ว นี่เป็นเหตุผลที่ควรฉลอง หรืออาจเป็นอินเทอร์เฟซผู้ใช้ที่ใช้งานได้จริงมากขึ้น ซึ่งคุณจะสร้างในขั้นตอนถัดไป

7 ใช้ Read Eval Print Loop

การโต้ตอบกับภาษาโปรแกรมจะสนุกขึ้นมากในสภาพแวดล้อมแบบอินเทอร์แอกทีฟที่รวดเร็ว การใช้งานเดิมของสภาพแวดล้อมดังกล่าวคือ Read Eval Print Loop (REPL) ของ LISP คุณจะใช้สิ่งที่คล้ายกับ Duktape ในขั้นตอนนี้

เตรียมสิ่งต่างๆ ให้พร้อมใช้งานจริง

โค้ดปัจจุบันที่โต้ตอบกับไลบรารี Duktape C ถือว่าไม่มีปัญหาใดๆ และจะไม่โหลดไลบรารีลิงก์แบบไดนามิกของ Duktape เมื่ออยู่ระหว่างการทดสอบ คุณต้องทําการเปลี่ยนแปลงบางอย่างใน lib/ffigen_app.dart เพื่อให้การผสานรวมนี้พร้อมใช้งานจริง

lib/ffigen_app.dart

import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:ffi/ffi.dart' as ffi;
import 'package:path/path.dart' as p;                               // Add this import

import 'duktape_bindings_generated.dart';

const String _libName = 'ffigen_app';

final DynamicLibrary _dylib = () {
 
if (Platform.isMacOS || Platform.isIOS) {
   
// Add from here...
   
if (Platform.environment.containsKey('FLUTTER_TEST')) {
     
return DynamicLibrary.open(
       
'build/macos/Build/Products/Debug/$_libName/$_libName.framework/$_libName',
     
);
   
}
   
// To here.
   
return DynamicLibrary.open('$_libName.framework/$_libName');
 
}
 
if (Platform.isAndroid || Platform.isLinux) {
   
// Add from here...
   
if (Platform.environment.containsKey('FLUTTER_TEST')) {
     
return DynamicLibrary.open(
       
'build/linux/x64/debug/bundle/lib/lib$_libName.so',
     
);
   
}
   
// To here.
   
return DynamicLibrary.open('lib$_libName.so');
 
}
 
if (Platform.isWindows) {
   
// Add from here...
   
if (Platform.environment.containsKey('FLUTTER_TEST')) {
     
return switch (Abi.current()) {
       
Abi.windowsArm64 => DynamicLibrary.open(
         
p.canonicalize(
           
p.join(r'build\windows\arm64\runner\Debug', '$_libName.dll'),
         
),
       
),
       
Abi.windowsX64 => DynamicLibrary.open(
         
p.canonicalize(
           
p.join(r'build\windows\x64\runner\Debug', '$_libName.dll'),
         
),
       
),
       
_ => throw 'Unsupported platform',
     
};
   
}
   
// To here.
   
return DynamicLibrary.open('$_libName.dll');
 
}
 
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();

final DuktapeBindings _bindings = DuktapeBindings(_dylib);

class Duktape {
 
Duktape() {
   
ctx = _bindings.duk_create_heap(
     
nullptr,
     
nullptr,
     
nullptr,
     
nullptr,
     
nullptr,
   
);
 
}

 
// Modify this function
 
String evalString(String jsCode) {
   
var nativeUtf8 = jsCode.toNativeUtf8();
   
final evalResult = _bindings.duk_eval_raw(
     
ctx,
     
nativeUtf8.cast<Char>(),
     
0,
     
0 |
         
DUK_COMPILE_EVAL |
         
DUK_COMPILE_SAFE |
         
DUK_COMPILE_NOSOURCE |
         
DUK_COMPILE_STRLEN |
         
DUK_COMPILE_NOFILENAME,
   
);
   
ffi.malloc.free(nativeUtf8);

   
if (evalResult != 0) {
     
throw _retrieveTopOfStackAsString();
   
}

   
return _retrieveTopOfStackAsString();
 
}

 
// Add this function
 
String _retrieveTopOfStackAsString() {
   
Pointer<Size> outLengthPtr = ffi.calloc<Size>();
   
final errorStrPtr = _bindings.duk_safe_to_lstring(ctx, -1, outLengthPtr);
   
final returnVal = errorStrPtr.cast<ffi.Utf8>().toDartString(
     
length: outLengthPtr.value,
   
);
   
ffi.calloc.free(outLengthPtr);
   
return returnVal;
 
}

 
void dispose() {
   
_bindings.duk_destroy_heap(ctx);
   
ctx = nullptr;
 
}

 
late Pointer<duk_hthread> ctx;
}

ซึ่งต้องใช้การเพิ่มแพ็กเกจ path

flutter pub add path

มีการขยายโค้ดเพื่อโหลดไลบรารีลิงก์แบบไดนามิกเพื่อจัดการกรณีที่มีการใช้ปลั๊กอินในโปรแกรมรันทดสอบ ซึ่งจะช่วยให้เขียนการทดสอบการผสานรวมที่ใช้ API นี้เป็นการทดสอบ Flutter ได้ โค้ดสำหรับประเมินสตริงโค้ด JavaScript ได้รับการขยายให้จัดการกับเงื่อนไขข้อผิดพลาดได้อย่างถูกต้อง เช่น โค้ดที่ไม่สมบูรณ์หรือไม่ถูกต้อง โค้ดเพิ่มเติมนี้แสดงวิธีจัดการสถานการณ์ที่ระบบแสดงผลสตริงเป็นอาร์เรย์ไบต์และต้องแปลงเป็นสตริง Dart

เพิ่มแพ็กเกจ

ในการสร้าง REPL คุณจะแสดงการโต้ตอบระหว่างผู้ใช้กับเครื่องมือรันไทม์ JavaScript ของ Duktape ผู้ใช้ป้อนบรรทัดโค้ด แล้ว Duktape จะตอบกลับด้วยผลลัพธ์ของการคํานวณหรือข้อยกเว้น คุณจะใช้ freezed เพื่อลดจำนวนโค้ดที่ซ้ำกันซึ่งต้องเขียน นอกจากนี้ คุณจะใช้ google_fonts เพื่อทำให้เนื้อหาที่แสดงมีความสอดคล้องกับธีมมากขึ้น และ flutter_riverpod เพื่อจัดการสถานะได้ด้วย

เพิ่มการอ้างอิงที่จำเป็นลงในแอปตัวอย่าง

cd example
flutter pub add flutter_riverpod freezed_annotation google_fonts
flutter pub add -d build_runner freezed

ถัดไป ให้สร้างไฟล์เพื่อบันทึกการโต้ตอบ REPL โดยทำดังนี้

example/lib/duktape_message.dart

import 'package:freezed_annotation/freezed_annotation.dart';

part 'duktape_message.freezed.dart';

@freezed
class DuktapeMessage with _$DuktapeMessage {
 
factory DuktapeMessage.evaluate(String code) = DuktapeMessageCode;
 
factory DuktapeMessage.response(String result) = DuktapeMessageResponse;
 
factory DuktapeMessage.error(String log) = DuktapeMessageError;
}

คลาสนี้ใช้ฟีเจอร์ประเภทยูเนียนของ freezed เพื่อเปิดใช้นิพจน์ที่ปลอดภัยตามประเภทของรูปร่างของแต่ละบรรทัดที่แสดงใน REPL เป็นหนึ่งใน 3 ประเภท เมื่อถึงขั้นตอนนี้ โค้ดของคุณอาจแสดงข้อผิดพลาดรูปแบบใดรูปแบบหนึ่ง เนื่องจากมีโค้ดเพิ่มเติมที่ต้องสร้างขึ้น โปรดดำเนินการดังต่อไปนี้

flutter pub run build_runner build

ซึ่งจะสร้างไฟล์ example/lib/duktape_message.freezed.dart ที่รหัสที่คุณเพิ่งพิมพ์ไว้ใช้

ถัดไป คุณจะต้องทำการแก้ไข 2 รายการในไฟล์การกําหนดค่า macOS เพื่อเปิดใช้ google_fonts ในการส่งคําขอข้อมูลแบบอักษรผ่านเครือข่าย

example/macos/Runner/DebugProfile.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <key>com.apple.security.cs.allow-jit</key>
        <true/>
        <key>com.apple.security.network.server</key>
        <true/>
        <!-- Add from here... -->
        <key>com.apple.security.network.client</key>
        <true/>
        <!-- ...to here -->
</dict>
</plist>

example/macos/Runner/Release.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>com.apple.security.app-sandbox</key>
        <true/>
        <!-- Add from here... -->
        <key>com.apple.security.network.client</key>
        <true/>
        <!-- ...to here -->
</dict>
</plist>

สร้าง REPL

เมื่ออัปเดตเลเยอร์การผสานรวมเพื่อจัดการข้อผิดพลาดและสร้างการนําเสนอข้อมูลสําหรับการโต้ตอบแล้ว ก็ถึงเวลาสร้างอินเทอร์เฟซผู้ใช้ของแอปตัวอย่าง

example/lib/main.dart

import 'package:ffigen_app/ffigen_app.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';

import 'duktape_message.dart';

void main() {
 
runApp(const ProviderScope(child: DuktapeApp()));
}

final duktapeMessagesProvider =
   
StateNotifierProvider<DuktapeMessageNotifier, List<DuktapeMessage>>((ref) {
     
return DuktapeMessageNotifier(messages: <DuktapeMessage>[]);
   
});

class DuktapeMessageNotifier extends StateNotifier<List<DuktapeMessage>> {
 
DuktapeMessageNotifier({required List<DuktapeMessage> messages})
   
: duktape = Duktape(),
     
super(messages);
 
final Duktape duktape;

 
void eval(String code) {
   
state = [DuktapeMessage.evaluate(code), ...state];
   
try {
     
final response = duktape.evalString(code);
     
state = [DuktapeMessage.response(response), ...state];
   
} catch (e) {
     
state = [DuktapeMessage.error('$e'), ...state];
   
}
 
}
}

class DuktapeApp extends StatelessWidget {
 
const DuktapeApp({super.key});

 
@override
 
Widget build(BuildContext context) {
   
return const MaterialApp(title: 'Duktape App', home: DuktapeRepl());
 
}
}

class DuktapeRepl extends ConsumerStatefulWidget {
 
const DuktapeRepl({super.key});

 
@override
 
ConsumerState<DuktapeRepl> createState() => _DuktapeReplState();
}

class _DuktapeReplState extends ConsumerState<DuktapeRepl> {
 
final _controller = TextEditingController();
 
final _focusNode = FocusNode();
 
var _isComposing = false;

 
void _handleSubmitted(String text) {
   
_controller.clear();
   
setState(() {
     
_isComposing = false;
   
});
   
setState(() {
     
ref.read(duktapeMessagesProvider.notifier).eval(text);
   
});
   
_focusNode.requestFocus();
 
}

 
@override
 
Widget build(BuildContext context) {
   
final messages = ref.watch(duktapeMessagesProvider);
   
return Scaffold(
     
backgroundColor: Colors.white,
     
appBar: AppBar(
       
title: const Text('Duktape REPL'),
       
elevation: Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0,
     
),
     
body: Column(
       
children: [
         
Flexible(
           
child: Ink(
             
color: Theme.of(context).scaffoldBackgroundColor,
             
child: SafeArea(
               
bottom: false,
               
child: ListView.builder(
                 
padding: const EdgeInsets.all(8.0),
                 
reverse: true,
                 
itemBuilder: (context, idx) {
                   
return switch (messages[idx]) {
                     
DuktapeMessageCode code => Padding(
                       
padding: const EdgeInsets.symmetric(vertical: 2),
                       
child: Text(
                         
'> ${code.code}',
                         
style: GoogleFonts.firaCode(
                           
textStyle: Theme.of(context).textTheme.titleMedium,
                         
),
                       
),
                     
),
                     
DuktapeMessageResponse response => Padding(
                       
padding: const EdgeInsets.symmetric(vertical: 2),
                       
child: Text(
                         
'= ${response.result}',
                         
style: GoogleFonts.firaCode(
                           
textStyle: Theme.of(context).textTheme.titleMedium,
                           
color: Colors.blue[800],
                         
),
                       
),
                     
),
                     
DuktapeMessageError error => Padding(
                       
padding: const EdgeInsets.symmetric(vertical: 2),
                       
child: Text(
                         
error.log,
                         
style: GoogleFonts.firaCode(
                           
textStyle: Theme.of(context).textTheme.titleSmall,
                           
color: Colors.red[800],
                           
fontWeight: FontWeight.bold,
                         
),
                       
),
                     
),
                     
DuktapeMessage message => Padding(
                       
padding: const EdgeInsets.symmetric(vertical: 2),
                       
child: Text(
                         
'Unhandled message $message',
                         
style: GoogleFonts.firaCode(
                           
textStyle: Theme.of(context).textTheme.titleSmall,
                           
color: Colors.red[800],
                           
fontWeight: FontWeight.bold,
                         
),
                       
),
                     
),
                   
};
                 
},
                 
itemCount: messages.length,
               
),
             
),
           
),
         
),
         
const Divider(height: 1.0),
         
SafeArea(
           
top: false,
           
child: Container(
             
decoration: BoxDecoration(color: Theme.of(context).cardColor),
             
child: _buildTextComposer(),
           
),
         
),
       
],
     
),
   
);
 
}

 
Widget _buildTextComposer() {
   
return IconTheme(
     
data: IconThemeData(color: Theme.of(context).colorScheme.secondary),
     
child: Container(
       
margin: const EdgeInsets.symmetric(horizontal: 8.0),
       
child: Row(
         
children: [
           
Text('>', style: Theme.of(context).textTheme.titleMedium),
           
const SizedBox(width: 4),
           
Flexible(
             
child: TextField(
               
controller: _controller,
               
decoration: const InputDecoration(border: InputBorder.none),
               
onChanged: (text) {
                 
setState(() {
                   
_isComposing = text.isNotEmpty;
                 
});
               
},
               
onSubmitted: _isComposing ? _handleSubmitted : null,
               
focusNode: _focusNode,
             
),
           
),
           
Container(
             
margin: const EdgeInsets.symmetric(horizontal: 4.0),
             
child: IconButton(
               
icon: const Icon(Icons.send),
               
onPressed: _isComposing
                   
? () => _handleSubmitted(_controller.text)
                   
: null,
             
),
           
),
         
],
       
),
     
),
   
);
 
}
}

โค้ดนี้มีหลายอย่างเกิดขึ้น แต่อธิบายทั้งหมดนั้นอยู่นอกเหนือขอบเขตของโค้ดแล็บนี้ เราขอแนะนำให้คุณเรียกใช้โค้ด แล้วทำการแก้ไขโค้ดหลังจากอ่านเอกสารประกอบที่เหมาะสม

cd example
flutter run

Duktape REPL ที่ทำงานในแอปพลิเคชัน Linux

Duktape REPL ที่ทำงานในแอปพลิเคชัน Windows

Duktape REPL ที่ทำงานในโปรแกรมจำลอง iOS

Duktape REPL ที่ทำงานในโปรแกรมจำลอง Android

8 ขอแสดงความยินดี

ยินดีด้วย คุณสร้างปลั๊กอิน Flutter ที่ใช้ FFI สำหรับ Windows, macOS, Linux, Android และ iOS เรียบร้อยแล้ว

หลังจากสร้างปลั๊กอินแล้ว คุณอาจต้องการแชร์ปลั๊กอินทางออนไลน์เพื่อให้ผู้อื่นนำไปใช้ได้ ดูเอกสารประกอบฉบับเต็มเกี่ยวกับการเผยแพร่ปลั๊กอินไปยัง pub.dev ได้ในการพัฒนาแพ็กเกจปลั๊กอิน