Sử dụng FFI trong trình bổ trợ Flutter

1. Giới thiệu

FFI (giao diện hàm ngoại ngữ) của Dart cho phép các ứng dụng Flutter tận dụng các thư viện gốc hiện có để hiển thị một API C. Dart hỗ trợ FFI trên Android, iOS, Windows, macOS và Linux. Đối với web, Dart hỗ trợ khả năng tương tác JavaScript, nhưng chủ đề đó không được đề cập trong lớp học lập trình này.

Sản phẩm bạn sẽ tạo ra

Trong lớp học lập trình này, bạn sẽ tạo một trình bổ trợ cho thiết bị di động và máy tính sử dụng thư viện C. Với API này, bạn sẽ viết một ứng dụng mẫu đơn giản sử dụng trình bổ trợ. Trình bổ trợ và ứng dụng của bạn sẽ:

  • Nhập mã nguồn thư viện C vào trình bổ trợ Flutter mới
  • Tuỳ chỉnh trình bổ trợ để có thể xây dựng trình bổ trợ trên Windows, macOS, Linux, Android và iOS
  • Xây dựng một ứng dụng sử dụng trình bổ trợ cho RepL (đọc để thấy vòng lặp in) JavaScript của JavaScript

Duktape REPL chạy dưới dạng một ứng dụng macOS

Kiến thức bạn sẽ học được

Trong lớp học lập trình này, bạn sẽ tìm hiểu kiến thức thực tế cần thiết để xây dựng một trình bổ trợ Flutter dựa trên FFI trên cả nền tảng máy tính và thiết bị di động, bao gồm:

  • Tạo một mẫu trình bổ trợ Flutter dựa trên Dart FFI
  • Sử dụng gói ffigen để tạo mã liên kết cho thư viện C
  • Sử dụng CMake để tạo trình bổ trợ Flutter FFI cho Android, Windows và Linux
  • Sử dụng CocoaPods để tạo trình bổ trợ Flutter FFI cho iOS và macOS

Bạn cần có

  • Android Studio 4.1 trở lên để phát triển Android
  • Xcode 13 trở lên để phát triển cho iOS và macOS
  • Visual Studio 2022 hoặc Visual Studio Build Tools 2022 có thông báo "Desktop Development with C++" (Phát triển máy tính để bàn bằng C++) khối lượng công việc để phát triển máy tính chạy Windows
  • SDK Flutter
  • Mọi công cụ bản dựng cần thiết cho các nền tảng mà bạn sẽ phát triển (ví dụ: CMake, CocoaPods, v.v.).
  • LLVM cho các nền tảng mà bạn sẽ phát triển. Bộ công cụ trình biên dịch LLVM được ffigen sử dụng để phân tích cú pháp tệp tiêu đề C nhằm tạo liên kết FFI hiển thị trong Dart.
  • Một trình soạn thảo mã, chẳng hạn như Visual Studio Code.

2. Bắt đầu

Công cụ ffigen là một tính năng mới bổ sung gần đây cho Flutter. Bạn có thể xác nhận rằng quá trình cài đặt Flutter của mình đang chạy bản phát hành ổn định hiện tại bằng cách chạy lệnh sau.

$ 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!

Xác nhận rằng kết quả đầu ra flutter doctor cho biết bạn đang ở trên kênh chính thức, đồng thời không có bản phát hành Flutter ổn định nào mới đây. Nếu bạn chưa sử dụng phiên bản ổn định hoặc có các bản phát hành mới hơn, hãy chạy hai lệnh sau để tăng tốc độ cho công cụ Flutter của bạn.

$ flutter channel stable
$ flutter upgrade

Bạn có thể chạy mã trong lớp học lập trình này bằng bất kỳ thiết bị nào sau đây:

  • Máy tính dùng để phát triển của bạn (dành cho phiên bản trình bổ trợ và ứng dụng mẫu dành cho máy tính)
  • Một thiết bị Android hoặc iOS thực đã kết nối với máy tính của bạn và đặt ở Chế độ nhà phát triển
  • Trình mô phỏng iOS (yêu cầu cài đặt công cụ Xcode)
  • Trình mô phỏng Android (yêu cầu thiết lập trong Android Studio)

3. Tạo mẫu trình bổ trợ

Bắt đầu phát triển trình bổ trợ Flutter

Flutter cung cấp các mẫu trình bổ trợ giúp bạn dễ dàng bắt đầu. Khi tạo mẫu trình bổ trợ, bạn có thể chỉ định ngôn ngữ mình muốn sử dụng.

Chạy lệnh sau trong thư mục đang hoạt động để tạo dự án bằng mẫu trình bổ trợ:

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

Tham số --platforms chỉ định những nền tảng mà trình bổ trợ của bạn sẽ hỗ trợ.

Bạn có thể kiểm tra bố cục của dự án đã tạo bằng lệnh tree hoặc trình khám phá tệp của hệ điều hành.

$ 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

Bạn nên dành chút thời gian xem cấu trúc thư mục để cảm nhận nội dung đã được tạo và vị trí của thư mục đó. Mẫu plugin_ffi đặt mã Dart cho trình bổ trợ trong lib, các thư mục dành riêng cho nền tảng có tên android, ios, linux, macoswindows và quan trọng nhất là thư mục example.

Đối với một nhà phát triển đã từng phát triển Flutter bình thường, cấu trúc này có thể khác lạ vì không có tệp thực thi nào được xác định ở cấp cao nhất. Một trình bổ trợ sẽ được đưa vào các dự án Flutter khác, nhưng bạn sẽ bổ sung mã trong thư mục example để đảm bảo mã trình bổ trợ của bạn hoạt động được.

Đã đến lúc bắt đầu!

4. Tạo và chạy ví dụ

Để đảm bảo hệ thống xây dựng và các điều kiện tiên quyết được cài đặt chính xác và hoạt động trên từng nền tảng được hỗ trợ, hãy tạo và chạy ứng dụng mẫu đã tạo cho từng mục tiêu.

Windows

Đảm bảo rằng bạn đang sử dụng một phiên bản Windows được hỗ trợ. Lớp học lập trình này được biết là hoạt động trên Windows 10 và Windows 11.

Bạn có thể tạo ứng dụng qua trình soạn thảo mã hoặc trên dòng lệnh.

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=/

Bạn sẽ thấy một cửa sổ ứng dụng đang chạy như sau:

Ứng dụng FFI do mẫu tạo chạy dưới dạng ứng dụng Windows

Linux

Đảm bảo rằng bạn đang sử dụng một phiên bản Linux được hỗ trợ. Lớp học lập trình này sử dụng Ubuntu 22.04.1.

Sau khi bạn đã cài đặt tất cả điều kiện tiên quyết được liệt kê trong Bước 2, hãy chạy các lệnh sau trong thiết bị đầu cuối:

$ 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=/

Bạn sẽ thấy một cửa sổ ứng dụng đang chạy như sau:

Ứng dụng FFI do mẫu tạo chạy dưới dạng ứng dụng Linux

Android

Đối với Android, bạn có thể sử dụng Windows, macOS hoặc Linux để biên dịch. Trước tiên, hãy đảm bảo rằng bạn có một thiết bị Android được kết nối với máy tính phát triển hoặc đang chạy một phiên bản Trình mô phỏng Android (AVD). Xác nhận rằng Flutter có thể kết nối với thiết bị Android hoặc trình mô phỏng bằng cách chạy lệnh sau:

$ 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

Ứng dụng FFI do mẫu tạo chạy trong trình mô phỏng Android

macOS và iOS

Để phát triển Flutter cho macOS và iOS, bạn phải sử dụng máy tính macOS.

Bắt đầu bằng việc chạy ứng dụng mẫu trên macOS. Một lần nữa xác nhận các thiết bị mà Flutter nhìn thấy:

$ 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

Chạy ứng dụng mẫu bằng dự án trình bổ trợ đã tạo:

$ cd ffigen_app/example
$ flutter run -d macos

Bạn sẽ thấy một cửa sổ ứng dụng đang chạy như sau:

Ứng dụng FFI do mẫu tạo chạy dưới dạng ứng dụng Linux

Đối với iOS, bạn có thể sử dụng trình mô phỏng hoặc thiết bị phần cứng thực. Nếu bạn sử dụng trình mô phỏng, trước tiên hãy chạy trình mô phỏng. Lệnh flutter devices hiện liệt kê trình mô phỏng là một trong các thiết bị hiện có.

$ 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

Sau khi trình mô phỏng khởi động, hãy chạy: flutter run.

$ cd ffigen_app/example
$ flutter run -d iphone

Ứng dụng FFI do mẫu tạo chạy trong trình mô phỏng iOS

Trình mô phỏng dành cho iOS được ưu tiên hơn so với mục tiêu của macOS, vì vậy, bạn có thể bỏ qua việc chỉ định một thiết bị có tham số -d.

Xin chúc mừng! Bạn đã tạo và chạy thành công một ứng dụng trên 5 hệ điều hành. Tiếp theo, hãy xây dựng trình bổ trợ gốc và giao tiếp với trình bổ trợ đó từ Dart bằng FFI.

5. Sử dụng Duktape trên Windows, Linux và Android

Thư viện C mà bạn sẽ sử dụng trong lớp học lập trình này là Duktape. Duktape là một công cụ JavaScript có thể nhúng, tập trung vào tính di động và kích thước nhỏ gọn. Trong bước này, bạn sẽ định cấu hình trình bổ trợ để biên dịch thư viện Duktape, liên kết thư viện này với trình bổ trợ của bạn, sau đó truy cập vào thư viện này bằng FFI của Dart.

Bước này sẽ định cấu hình chức năng tích hợp hoạt động trên Windows, Linux và Android. Việc tích hợp iOS và macOS yêu cầu phải có cấu hình bổ sung (ngoài những gì được nêu chi tiết ở bước này) để đưa thư viện đã biên dịch vào tệp thực thi Flutter cuối cùng. Cấu hình bắt buộc bổ sung được đề cập trong bước tiếp theo.

Truy xuất Duktape

Trước tiên, hãy lấy bản sao của mã nguồn duktape bằng cách tải mã này xuống từ trang web duktape.org.

Đối với Windows, bạn có thể sử dụng PowerShell với Invoke-WebRequest:

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

Đối với Linux, wget là một lựa chọn phù hợp.

$ 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]

Đây là tệp lưu trữ tar.xz. Trên Windows, bạn có thể tải các công cụ 7Zip xuống rồi sử dụng như sau.

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

Bạn cần chạy 7z hai lần, lần đầu tiên để huỷ lưu trữ tệp nén xz, thứ hai để mở rộng tệp lưu trữ 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

Trên môi trường linux hiện đại, tar trích xuất nội dung qua một bước như sau.

$ 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]

Cài đặt LLVM

Để sử dụng ffigen, bạn phải cài đặt LLVMffigen dùng để phân tích cú pháp các tiêu đề C. Trên Windows, hãy chạy lệnh sau.

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

Định cấu hình đường dẫn hệ thống của bạn để thêm C:\Program Files\LLVM\bin vào đường dẫn tìm kiếm nhị phân nhằm hoàn tất việc cài đặt LLVM trên máy chạy Windows của bạn. Bạn có thể kiểm tra xem thiết bị đã được cài đặt chính xác hay chưa như sau.

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

Đối với Ubuntu, bạn có thể cài đặt phần phụ thuộc LLVM như sau. Các bản phân phối Linux khác có các phần phụ thuộc tương tự đối với LLVM và 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) ...

Như ở trên, bạn có thể kiểm thử việc cài đặt LLVM trên Linux như sau.

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

Đang cấu hình ffigen

Mẫu được tạo pubpsec.yaml cấp cao nhất có thể có các phiên bản đã lỗi thời của gói ffigen. Chạy lệnh sau để cập nhật các phần phụ thuộc Dart trong dự án trình bổ trợ:

$ flutter pub upgrade --major-versions

Gói ffigen đã được cập nhật, tiếp theo, hãy định cấu hình những tệp mà ffigen sẽ sử dụng để tạo các tệp liên kết. Sửa đổi nội dung của tệp ffigen.yaml của dự án cho phù hợp với nội dung sau.

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ấu hình này bao gồm tệp tiêu đề C để truyền đến LLVM, tệp đầu ra cần tạo, nội dung mô tả để đặt ở đầu tệp và phần mở đầu dùng để thêm cảnh báo tìm lỗi mã nguồn.

Có một mục cấu hình ở cuối tệp cần được giải thích thêm. Kể từ phiên bản 11.0.0 của ffigen, theo mặc định, trình tạo liên kết sẽ không tạo các liên kết nếu có cảnh báo hoặc lỗi do clang tạo ra khi phân tích cú pháp các tệp tiêu đề.

Các tệp tiêu đề Duktape (như đã viết) sẽ kích hoạt clang trên macOS để tạo cảnh báo do thiếu thông số kỹ thuật loại thuộc tính rỗng trên con trỏ của Duktape. Để hỗ trợ đầy đủ cho macOS và iOS Duktape, bạn cần thêm các thông số loại này vào cơ sở mã Duktape. Trong thời gian chờ đợi, chúng tôi sẽ đưa ra quyết định bỏ qua những cảnh báo này bằng cách đặt cờ ignore-source-errors thành true.

Trong ứng dụng chính thức, bạn nên loại bỏ tất cả cảnh báo của trình biên dịch trước khi gửi ứng dụng của mình. Tuy nhiên, việc thực hiện việc này cho Duktape lại nằm ngoài phạm vi của lớp học lập trình này.

Hãy xem tài liệu về ffigen để biết thêm thông tin về các khoá và giá trị khác.

Bạn cần sao chép các tệp Duktape cụ thể từ tệp phân phối Duktape vào vị trí mà ffigen được định cấu hình để tìm các tệp đó.

$ 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/

Về mặt kỹ thuật, bạn chỉ cần sao chép trên duktape.h cho ffigen, nhưng bạn sẽ định cấu hình CMake để xây dựng thư viện cần cả ba thư viện này. Chạy ffigen để tạo liên kết mới:

$ 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

Bạn sẽ thấy các cảnh báo khác nhau trên từng hệ điều hành. Hiện tại, bạn có thể bỏ qua những thông báo này, vì Duktape 2.7.0 được biết là biên dịch với clang trên Windows, Linux và macOS.

Định cấu hình CMake

CMake là một hệ thống tạo hệ thống xây dựng. Trình bổ trợ này sử dụng CMake để tạo hệ thống xây dựng cho Android, Windows và Linux để đưa Duktape vào tệp nhị phân Flutter đã tạo. Bạn cần sửa đổi tệp cấu hình CMake do mẫu tạo như sau.

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)

Cấu hình CMake thêm các tệp nguồn và quan trọng hơn là sửa đổi hành vi mặc định của tệp thư viện đã tạo trên Windows để xuất tất cả biểu tượng C theo mặc định. Đây là một giải pháp của CMake nhằm giúp chuyển các thư viện kiểu Unix, còn gọi là Duktape, sang thế giới Windows.

Thay thế nội dung của lib/ffigen_app.dart bằng nội dung sau.

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;
}

Tệp này chịu trách nhiệm tải tệp thư viện đường liên kết động (.so cho Linux và Android, .dll cho Windows) và cung cấp một trình bao bọc để hiển thị giao diện đặc trưng hơn của Dart với mã C cơ bản.

Vì tệp này nhập trực tiếp gói ffi, nên bạn cần di chuyển gói này từ dev_dependencies sang dependencies. Một cách dễ dàng để thực hiện việc này là chạy lệnh sau:

$ dart pub add ffi

Thay thế nội dung của main.dart trong ví dụ bằng nội dung sau.

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)}';
                    });
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Bây giờ, bạn có thể chạy lại ứng dụng mẫu bằng cách sử dụng:

$ cd example
$ flutter run

Bạn sẽ thấy ứng dụng đang chạy như sau:

Hiển thị Duktape đã khởi tạo trong ứng dụng Windows

Hiển thị đầu ra JavaScript Duktape trong ứng dụng Windows

Hai ảnh chụp màn hình này cho thấy trước và sau khi nhấn nút Run JavaScript (Chạy JavaScript). Điều này minh hoạ việc thực thi mã JavaScript từ Dart và hiển thị kết quả trên màn hình.

Android

Android là một hệ điều hành dựa trên nhân hệ điều hành Linux và có phần tương tự như các bản phân phối Linux dành cho máy tính. Hệ thống xây dựng CMake có thể ẩn hầu hết những khác biệt giữa 2 nền tảng này. Để tạo và chạy trên Android, hãy đảm bảo trình mô phỏng Android đang chạy (hoặc đã kết nối thiết bị Android). Chạy ứng dụng. Ví dụ:

$ cd example
$ flutter run -d emulator-5554

Bây giờ, bạn sẽ thấy ứng dụng mẫu chạy trên Android:

Hiển thị Duktape đã khởi chạy trong trình mô phỏng Android

Hiển thị đầu ra JavaScript Duktape trong trình mô phỏng Android

6. Sử dụng Duktape trên macOS và iOS

Đã đến lúc giúp trình bổ trợ của bạn hoạt động trên macOS và iOS, hai hệ điều hành có liên quan chặt chẽ với nhau. Bắt đầu với macOS. Mặc dù CMake hỗ trợ macOS và iOS, nhưng bạn sẽ không sử dụng lại công việc bạn đã thực hiện cho Linux và Android, vì Flutter trên macOS và iOS sử dụng CocoaPods để nhập thư viện.

Dọn dẹp

Trong bước trước, bạn đã tạo một ứng dụng hoạt động dành cho Android, Windows và Linux. Tuy nhiên, hiện bạn cần dọn dẹp một số tệp còn lại của mẫu gốc. Hãy xoá các từ khoá đó ngay bây giờ như sau.

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

macOS

Flutter trên nền tảng macOS sử dụng CocoaPods để nhập mã C và C++. Điều này có nghĩa là gói này cần được tích hợp vào cơ sở hạ tầng xây dựng của CocoaPods. Để cho phép sử dụng lại mã C mà bạn đã định cấu hình để tạo bằng CMake ở bước trước, bạn cần thêm một tệp chuyển tiếp trong trình chạy của nền tảng macOS.

macos/Classes/duktape.c

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

Tệp này sử dụng sức mạnh của bộ tiền xử lý C để bao gồm mã nguồn từ mã nguồn gốc mà bạn đã thiết lập ở bước trước. Vui lòng xem macos/ffigen_app.podspec để biết thêm thông tin chi tiết về cách hoạt động của chế độ này.

Bây giờ, việc chạy ứng dụng này sẽ tuân theo cùng một mẫu mà bạn đã thấy trên Windows và Linux.

$ cd example
$ flutter run -d macos

Hiển thị Duktape đã khởi chạy trong ứng dụng macOS

Hiện đầu ra JavaScript Duktape trong ứng dụng macOS

iOS

Tương tự như cách thiết lập cho macOS, iOS cũng yêu cầu thêm một tệp chuyển tiếp C.

ios/Classes/duktape.c

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

Với tệp duy nhất này, trình bổ trợ của bạn hiện cũng được định cấu hình để chạy trên iOS. Chạy như bình thường.

$ flutter run -d iPhone

Hiển thị Duktape đã khởi chạy trong trình mô phỏng iOS

Đang hiển thị kết quả JavaScript Duktape trong trình mô phỏng iOS

Xin chúc mừng! Bạn đã tích hợp thành công mã gốc trên năm nền tảng. Đây là không gian ăn mừng! Thậm chí có thể là giao diện người dùng nhiều chức năng hơn mà bạn sẽ tạo trong bước tiếp theo.

7. Triển khai Vòng lặp in Read Eval

Tương tác với ngôn ngữ lập trình sẽ thú vị hơn rất nhiều trong một môi trường tương tác nhanh. Cách triển khai ban đầu của môi trường như vậy là Vòng lặp Read Eval Print (PRCL) của LISP. Bạn sẽ triển khai công cụ tương tự với Duktape trong bước này.

Chuẩn bị sẵn sàng cho khâu sản xuất

Mã hiện tại tương tác với thư viện Duktape C giả định không có sự cố nào có thể xảy ra. Ồ, hệ thống không tải các thư viện đường liên kết động Duktape khi đang kiểm thử. Để chuẩn bị sẵn sàng cho việc tích hợp này, bạn cần thực hiện một số thay đổi đối với 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;
}

Mã để tải thư viện đường liên kết động đã được mở rộng để xử lý trường hợp trình bổ trợ đang được sử dụng trong một trình chạy kiểm thử. Điều này cho phép viết chương trình kiểm thử tích hợp để chạy API này dưới dạng kiểm thử Flutter. Mã để đánh giá một chuỗi mã JavaScript đã được mở rộng để xử lý chính xác các điều kiện lỗi, ví dụ như mã không hoàn chỉnh hoặc không chính xác. Mã bổ sung này cho biết cách xử lý các tình huống trong đó chuỗi được trả về dưới dạng mảng byte và cần được chuyển đổi thành chuỗi Dart.

Thêm gói

Khi tạo REPL, bạn sẽ hiển thị hoạt động tương tác giữa người dùng và công cụ JavaScript Duktape. Người dùng nhập các dòng mã và Duktape sẽ phản hồi bằng kết quả tính toán hoặc ngoại lệ. Bạn sẽ dùng freezed để giảm số lượng mã nguyên mẫu cần phải viết. Bạn cũng sẽ dùng google_fonts để làm cho nội dung hiển thị chú trọng hơn về giao diện và flutter_riverpod để quản lý trạng thái.

Thêm các phần phụ thuộc bắt buộc vào ứng dụng mẫu:

$ cd example
$ dart pub add flutter_riverpod freezed_annotation google_fonts
$ dart pub add -d build_runner freezed

Tiếp theo, hãy tạo một tệp để ghi lại hoạt động tương tác 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;
}

Lớp này sử dụng tính năng kiểu hợp nhất của freezed để cho phép dễ dàng biểu thức hình dạng của từng đường hiển thị trong REPL dưới dạng một trong 3 kiểu. Tại thời điểm này, mã của bạn có thể đang hiển thị một số dạng lỗi trên mã này, vì vẫn cần tạo thêm mã. Hãy thực hiện ngay như sau.

$ flutter pub run build_runner build

Thao tác này sẽ tạo tệp example/lib/duktape_message.freezed.dart mà mã bạn vừa nhập sẽ dựa vào.

Tiếp theo, bạn cần sửa đổi tệp cấu hình macOS để cho phép google_fonts thực hiện yêu cầu mạng về dữ liệu phông chữ.

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>

Xây dựng REPL

Bây giờ, bạn đã cập nhật lớp tích hợp để xử lý lỗi và đã tạo bản trình bày dữ liệu cho tương tác, đã đến lúc xây dựng giao diện người dùng của ứng dụng mẫu.

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,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Có rất nhiều vấn đề trong mã này, nhưng chúng tôi không thể giải thích tất cả mọi thứ trong phạm vi của lớp học lập trình này. Bạn nên chạy mã đó rồi sửa đổi mã sau khi xem lại tài liệu thích hợp.

$ cd example
$ flutter run

Duktape REPL chạy trong ứng dụng Linux

Duktape REPL chạy trong ứng dụng Windows

Duktape REPL chạy trong trình mô phỏng iOS

Duktape REPL chạy trong trình mô phỏng Android

8. Xin chúc mừng

Xin chúc mừng! Bạn đã tạo thành công một trình bổ trợ dựa trên Flutter FFI cho Windows, macOS, Linux, Android và iOS!

Sau khi tạo trình bổ trợ, bạn có thể chia sẻ trình bổ trợ trực tuyến để những người khác có thể sử dụng. Bạn có thể xem tài liệu đầy đủ về cách xuất bản trình bổ trợ lên pub.dev trong phần Phát triển gói trình bổ trợ.