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

1. บทนำ

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

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

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

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

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

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

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

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

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

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

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

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

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.3.9, on macOS 13.1 22C65 darwin-arm, locale en)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 14.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2021.2)
[✓] IntelliJ IDEA Community Edition (version 2022.2.2)
[✓] VS Code (version 1.74.0)
[✓] Connected device (2 available)
[✓] HTTP Host Availability

• No issues found!

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

$ flutter channel stable
$ flutter upgrade

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

  • คอมพิวเตอร์สำหรับการพัฒนาซอฟต์แวร์ (ปลั๊กอินและแอปตัวอย่างในเวอร์ชันเดสก์ท็อป)
  • อุปกรณ์ Android หรือ iOS จริงที่เชื่อมต่อกับคอมพิวเตอร์และตั้งค่าเป็นโหมดนักพัฒนาซอฟต์แวร์
  • iOS Simulator (ต้องติดตั้งเครื่องมือ 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 เวอร์ชันที่รองรับ Codelab นี้เป็นที่ทราบกันว่าใช้งานได้บน 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 ในการคอมไพล์ได้ ก่อนอื่นให้ตรวจสอบว่าคุณมีอุปกรณ์ 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

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

macOS และ iOS

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

เริ่มด้วยการเรียกใช้แอปตัวอย่างใน 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

เมื่อเครื่องจำลองเริ่มทำงานแล้ว ให้เรียกใช้: flutter run

$ cd ffigen_app/example
$ flutter run -d iphone

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

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

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

5. การใช้ Duktape บน Windows, Linux และ Android

ไลบรารี C ที่คุณจะใช้ใน Codelab นี้คือ 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 สามารถติดตั้ง Dependency 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 `flutter pub run ffigen --config ffigen.yaml`.
name: DuktapeBindings
description: |
  Bindings for `src/duktape.h`.

  Regenerate bindings with `flutter pub 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 เพื่อสร้างคำเตือนเนื่องจากไม่มีตัวระบุประเภทความสามารถในการเว้นว่างในเคอร์เซอร์ของ Duktape Duktape ต้องเพิ่มตัวระบุประเภทเหล่านี้ลงในฐานของโค้ด Duktape เพื่อให้รองรับ Duktape สำหรับ macOS และ iOS อย่างเต็มรูปแบบ ในระหว่างนี้ เราจะตัดสินใจที่จะเพิกเฉยต่อคำเตือนเหล่านี้โดยตั้งค่าสถานะ ignore-source-errors เป็น true

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

ดูรายละเอียดเพิ่มเติมเกี่ยวกับคีย์และค่าอื่นๆ ได้ในเอกสารประกอบเกี่ยวกับ 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 เพื่อสร้างการเชื่อมโยงใหม่

$ flutter pub run ffigen --config ffigen.yaml
Running in Directory: '/home/brett/GitHub/codelabs/ffigen_codelab/step_05'
Input Headers: [./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 '__va_list_tag' start's with '_' and therefore will be private.
Finished, Bindings generated in /home/brett/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)

การกำหนดค่า 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) และให้ Wrapper ที่แสดงอินเทอร์เฟซที่ไม่รู้จักของ 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

กำลังแสดงเอาต์พุต JavaScript ของ Duktape ในแอปพลิเคชัน 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

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

iOS

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

ios/Classes/duktape.c

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

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

$ flutter run -d iPhone

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

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

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

7. ใช้ลูปการพิมพ์ของ Read Eval

การโต้ตอบกับภาษาโปรแกรมจะสนุกมากขึ้นในสภาพแวดล้อมแบบอินเทอร์แอกทีฟที่รวดเร็ว การใช้งานเดิมในสภาพแวดล้อมดังกล่าวคือ 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 DynamicLibrary.open(p.canonicalize(
          p.join(r'build\windows\runner\Debug', '$_libName.dll')));
    }
    // ...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;
}

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

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

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

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

$ cd example
$ dart pub add flutter_riverpod freezed_annotation google_fonts
$ dart 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" เป็น 1 ใน 3 ประเภทได้อย่างง่ายดาย ณ จุดนี้ โค้ดของคุณอาจแสดงข้อผิดพลาดบางอย่างในโค้ดนี้ เนื่องจากมีโค้ดเพิ่มเติมที่ต้องสร้าง โดยทำตามขั้นตอนต่อไปนี้

$ flutter pub run build_runner build

การดำเนินการนี้จะสร้างไฟล์ example/lib/duktape_message.freezed.dart ซึ่งเป็นโค้ดที่คุณเพิ่งพิมพ์

ถัดไป คุณจะต้องแก้ไขไฟล์การกำหนดค่า 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>

การสร้างการตอบกลับ

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

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) => messages[idx].when(
                    evaluate: (str) => Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Text(
                        '> $str',
                        style: GoogleFonts.firaCode(
                          textStyle: Theme.of(context).textTheme.titleMedium,
                        ),
                      ),
                    ),
                    response: (str) => Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Text(
                        '= $str',
                        style: GoogleFonts.firaCode(
                          textStyle: Theme.of(context).textTheme.titleMedium,
                          color: Colors.blue[800],
                        ),
                      ),
                    ),
                    error: (str) => Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Text(
                        str,
                        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,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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

$ cd example
$ flutter run

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

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

Duktape REPL กำลังทำงานในเครื่องมือจำลองของ iOS

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

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

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

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