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
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
, macos
và windows
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:
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:
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
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:
Đố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
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 LLVM mà ffigen
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:
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:
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
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
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
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ợ.