Thông tin về lớp học lập trình này
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
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
, macos
và windows
, 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:
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:
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.
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:
Đố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.
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:
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:
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
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
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
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ợ.