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

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

Thông tin về lớp học lập trình này

subjectLần cập nhật gần đây nhất: thg 6 23, 2025
account_circleTác giả: Brett Morgan and Maksim Lin

1. Giới thiệu

FFI (giao diện hàm ngoại) của Dart cho phép các ứng dụng Flutter sử dụng các thư viện gốc hiện có hiển thị 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 với 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ợ dành cho máy tính và thiết bị di động sử dụng thư viện C. Với API này, bạn sẽ viết một ứng dụng mẫu 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ợ để cho phép tạo trên Windows, macOS, Linux, Android và iOS
  • Tạo một ứng dụng sử dụng trình bổ trợ cho REPL (vòng lặp in hiển thị nội dung đã đọc) của JavaScript

Duktape REPL chạy dưới dạng ứ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 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ẫ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 iOS và macOS
  • Visual Studio 2022 hoặc Công cụ xây dựng Visual Studio 2022 với khối lượng công việc "Phát triển máy tính bằng C++" để phát triển máy tính Windows
  • SDK Flutter
  • Mọi công cụ xây dựng bắt buộc 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.
  • Trình soạn thảo mã, chẳng hạn như Visual Studio Code.

2. Bắt đầu

Bộ công cụ ffigen là một bổ sung gần đây cho Flutter. Bạn có thể xác nhận rằng quá trình cài đặt Flutter đ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.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!

Xác nhận rằng đầu ra flutter doctor cho biết bạn đang sử dụng kênh chính thức và không có bản phát hành Flutter chính thức nào mới hơn. Nếu bạn không 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 để cập nhật công cụ Flutter.

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 phát triển (dành cho các bản dựng máy tính của trình bổ trợ và ứng dụng mẫu)
  • Một thiết bị Android hoặc iOS thực tế được kết nối với máy tính và đặt thành Chế độ nhà phát triển
  • Trình mô phỏng iOS (yêu cầu cài đặt các 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 đi kèm với các mẫu cho trình bổ trợ giúp bạn bắt đầu. Khi tạo mẫu trình bổ trợ, bạn có thể chỉ định ngôn ngữ mà bạn 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 để biết những gì đã được tạo và vị trí của các 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 nhà phát triển đã quen với việc phát triển Flutter thông thường, cấu trúc này có thể sẽ gây khó hiểu vì không có tệp thực thi nào được xác định ở cấp cao nhất. Trình bổ trợ được đưa vào các dự án Flutter khác, nhưng bạn sẽ bổ sung mã trong thư mục example để xác minh rằng mã trình bổ trợ của bạn hoạt động.

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

4. Tạo bản dựng và chạy ví dụ

Để đảm bảo rằng 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 cho 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

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

Bạn có thể tạo ứng dụng từ trong 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 được tạo bằng mẫu chạy dưới dạng ứng dụng Windows

Linux

Xác minh rằng bạn đang sử dụng 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ả cá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 một 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 được tạo bằng mẫu 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.

Bạn cần thay đổi example/android/app/build.gradle.kts để sử dụng phiên bản NDK phù hợp.

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

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

Đảm bảo 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 thực thể 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ị hoặc trình mô phỏng Android bằng cách chạy các 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

Sau khi bạn có một thiết bị Android đang chạy, có thể là thiết bị thực hoặc trình mô phỏng, hãy chạy lệnh sau:

cd ffigen_app/example
flutter run

Flutter sẽ hỏi bạn muốn chạy ứng dụng trên thiết bị nào. Chọn thiết bị thích hợp trong danh sách.

Ứng dụng FFI được tạo bằng mẫu đang chạy trong trình mô phỏng Android

macOS và iOS

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

Bắt đầu bằng cách chạy ứng dụng mẫu trên macOS. Xác nhận lại 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 được tạo bằng mẫu 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 sử dụng trình mô phỏng, trước tiên, hãy khởi 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ị có sẵn.

$ 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 bạn có một thiết bị iOS đang chạy, có thể là thiết bị thực hoặc trình mô phỏng, hãy chạy lệnh sau:

cd ffigen_app/example
flutter run

Flutter sẽ hỏi bạn muốn chạy ứng dụng trên thiết bị nào. Chọn thiết bị thích hợp trong danh sách.

Ứng dụng FFI được tạo bằng mẫu chạy trong trình mô phỏng iOS

Trình mô phỏng iOS được ưu tiên hơn mục tiêu macOS, vì vậy, bạn có thể bỏ qua việc chỉ định thiết bị bằng 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 khác nhau. Tiếp theo, hãy tạo 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 khả năng di chuyển 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 đó bằng FFI của Dart.

Bước này định cấu hình chế độ 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 cấu hình bổ sung (ngoài những gì được nêu chi tiết trong 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 sẽ được đề cập trong bước tiếp theo.

Truy xuất Duktape

Trước tiên, hãy tải bản sao mã nguồn duktape xuống bằng cách tải mã nguồn đó 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]

Tệp này là một 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 để trích xuất tệp nén xz và lần 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 các môi trường Linux hiện đại, tar trích xuất nội dung trong 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 cần cài đặt LLVM. ffigen sử dụng LLVM để phân tích cú pháp 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 để 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 quá trình cài đặt LLVM trên máy Windows. Bạn có thể kiểm tra xem thẻ này đã được cài đặt đúng cách 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ự cho 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ử quá trình 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

Định cấu hình ffigen

Mẫu đã tạo pubpsec.yaml cấp cao nhất có thể có các phiên bản gói ffigen đã lỗi thời. 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

Giờ đây, 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 tệp liên kết. Chỉnh sửa nội dung của tệp ffigen.yaml của dự án cho khớp với nội dung sau.

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ấ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, trình tạo liên kết theo mặc định sẽ không tạo 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 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 chỉ định loại tính chất rỗng trên con trỏ của Duktape. Để hỗ trợ đầy đủ macOS và iOS, Duktape cần thêm các chỉ định loại này vào cơ sở mã Duktape. Trong thời gian chờ đợi, chúng tôi quyết định bỏ qua các 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 xuất bản ứng dụng. Tuy nhiên, việc này 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 chi tiết về các khoá và giá trị khác.

Bạn cần sao chép các tệp Duktape cụ thể từ bản 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 duktape.h cho ffigen, nhưng bạn sắp định cấu hình CMake để tạo thư viện cần cả ba. Chạy ffigen để tạo liên kết mới:

$ 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

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 các lỗi 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 nhằm đư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 được tạo bằng mẫu 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)

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

Cấu hình CMake sẽ 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 được tạo trên Windows để xuất tất cả các biểu tượng C theo mặc định. Đây là một giải pháp CMake giúp chuyển các thư viện kiểu Unix (trong đó có Duktape) sang 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 liên kết động (.so cho Linux và Android, .dll cho Windows) và cung cấp trình bao bọc hiển thị giao diện Dart đặc trưng hơn cho 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 chuyển gói này từ dev_dependencies sang dependencies. Một cách nhanh chó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)}';
                   
});
                 
},
               
),
             
],
           
),
         
),
       
),
     
),
   
);
 
}
}

Giờ đây, 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 chạy như sau:

Hiển thị Duktape được khởi chạy trong một ứng dụng Windows

Hiển thị kết quả JavaScript Duktape trong một ứng dụng Windows

Hai ảnh chụp màn hình này cho thấy trạng thái trước và sau khi nhấn nút Run JavaScript (Chạy JavaScript). Mã này minh hoạ cách 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 Linux và có phần tương tự như các bản phân phối Linux cho máy tính. Hệ thống xây dựng CMake có thể ẩn hầu hết các điểm khác biệt giữa hai nền tảng. Để tạo và chạy trên Android, hãy đảm bảo trình mô phỏng Android đang chạy (hoặc thiết bị Android đã kết nối). 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 đang chạy trên Android:

Hiển thị Duktape được 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

Giờ là lúc bạn phải làm cho trình bổ trợ 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 đã làm cho Linux và Android, vì Flutter trên macOS và iOS sử dụng CocoaPods để nhập thư viện.

Dọn dẹp

Ở bước trước, bạn đã tạo một ứng dụng hoạt động cho Android, Windows và Linux. Tuy nhiên, bạn cần dọn dẹp một vài tệp còn lại từ mẫu ban đầu. Hãy xoá các kênh đó ngay theo cách 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 bản dựng CocoaPods. Để bật tính năng 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 duy nhất trong trình chạy 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 trình xử lý trước C để đưa mã nguồn từ mã nguồn gốc mà bạn đã thiết lập ở bước trước. Hãy xem macos/ffigen_app.podspec để biết thêm thông tin chi tiết về cách hoạt động của công cụ này.

Giờ đây, việc chạy ứng dụng này 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 được khởi chạy trong một ứng dụng macOS

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

iOS

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

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 được khởi chạy trong trình mô phỏng iOS

Hiển thị đầu ra 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 5 nền tảng. Đây là lý do để ăn mừng! Thậm chí có thể là một giao diện người dùng có chức năng hơn mà bạn sẽ xây dựng trong bước tiếp theo.

7. Triển khai vòng lặp Read Eval Print

Việc tương tác với một ngôn ngữ lập trình sẽ thú vị hơn nhiều trong 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 (REPL) của LISP. Bạn sẽ triển khai một cách tương tự với Duktape trong bước này.

Chuẩn bị mọi thứ sẵn sàng cho bản phát hành chính thức

Mã hiện tại tương tác với thư viện Duktape C giả định không có gì sai. Ngoài ra, thư viện này không tải thư viện liên kết động Duktape khi đang kiểm thử. Để tích hợp này sẵn sàng cho bản phát hành chính thức, 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 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;
}

Để làm được điều này, bạn cần thêm gói path.

flutter pub add path

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 trình chạy kiểm thử. Điều này cho phép bạn viết một kiểm thử tích hợp để thực thi 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, chẳng hạn 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 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 phản hồi bằng kết quả của phép tính hoặc một ngoại lệ. Bạn sẽ sử dụng freezed để giảm lượng mã nguyên mẫu cần viết. Bạn cũng sẽ sử dụng google_fonts để làm cho nội dung hiển thị phù hợp hơn với chủ đề 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
flutter pub add flutter_riverpod freezed_annotation google_fonts
flutter 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 loại liên kết của freezed để cho phép biểu thức an toàn về kiểu của hình dạng của mỗi dòng hiển thị trong REPL dưới dạng một trong ba loại. Tại thời điểm này, mã của bạn có thể đang hiển thị một số lỗi trên mã này, vì 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 dựa vào đó.

Tiếp theo, bạn cần thực hiện một số sửa đổi đối với các tệp cấu hình macOS để cho phép google_fonts đưa ra các yêu cầu mạng cho 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>

Tạo REPL

Giờ đây, bạn đã cập nhật lớp tích hợp để xử lý lỗi và đã tạo một bản trình bày dữ liệu cho hoạt động tương tác, đã đến lúc tạo 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) {
                   
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,
             
),
           
),
         
],
       
),
     
),
   
);
 
}
}

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

cd example
flutter run

Duktape REPL chạy trong một ứ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 trình bổ trợ dựa trên FFI của Flutter 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ên mạng để người khác có thể sử dụng. Bạn có thể xem tài liệu đầy đủ về cách phát hành trình bổ trợ lên pub.dev trong phần Phát triển gói trình bổ trợ.