Korzystanie z FFI we wtyczce Flutter

Korzystanie z FFI w pluginie Flutter

Informacje o tym ćwiczeniu (w Codelabs)

subjectOstatnia aktualizacja: cze 23, 2025
account_circleAutorzy: Brett Morgan and Maksim Lin

1. Wprowadzenie

Interfejs FFI (foreign function interface) w języku Dart umożliwia aplikacjom Flutter korzystanie z dotychczasowych natywnych bibliotek, które udostępniają interfejs C API. Dart obsługuje FFI na Androidzie, iOS, Windows, macOS i Linuxie. W przypadku internetu Dart obsługuje interoperacyjność z JavaScriptem, ale ten temat nie jest omawiany w tym CodeLab.

Co utworzysz

W tym ćwiczeniu tworzysz wtyczkę na urządzenia mobilne i komputery, która korzysta z biblioteki C. Za pomocą tego interfejsu API napiszesz przykładową aplikację, która korzysta z wtyczki. Wtyczka i aplikacja:

  • Importowanie kodu źródłowego biblioteki C do nowego wtyczki Flutter
  • Spersonalizuj wtyczkę, aby umożliwić jej kompilację w systemach Windows, macOS, Linux, Android i iOS
  • Utwórz aplikację, która używa wtyczki do pętli REPL (read reveal print) w JavaScript.

REPL Duktape działający jako aplikacja na macOS

Czego się nauczysz

W tym ćwiczeniu z programowania poznasz praktyczne informacje potrzebne do tworzenia wtyczki Flutter na podstawie FFI na platformach komputerowych i mobilnych, w tym:

  • Generowanie szablonu wtyczki Flutter na podstawie Dart FFI
  • Generowanie kodu wiązania dla biblioteki C za pomocą pakietu ffigen
  • Kompilowanie wtyczki FFI Flutter na Androida, Windowsa i Linuxa za pomocą CMake
  • Tworzenie wtyczki FFI Flutter na iOS i macOS za pomocą CocoaPods

Czego potrzebujesz

  • Android Studio w wersji 4.1 lub nowszej do tworzenia aplikacji na Androida
  • Xcode 13 lub nowsza wersja do tworzenia aplikacji na iOS i macOS
  • Visual Studio 2022 lub Visual Studio Build Tools 2022 z pakietem „Programowanie na komputery z systemem Windows w C++” do tworzenia aplikacji na komputery z systemem Windows
  • Pakiet SDK Flutter
  • Wszystkie wymagane narzędzia do kompilacji na platformy, na których będziesz prowadzić prace programistyczne (np. CMake, CocoaPods itp.).
  • LLVM na platformy, na których będziesz tworzyć. Zestaw narzędzi kompilatora LLVM jest używany przez ffigen do analizowania pliku nagłówka C w celu utworzenia powiązania FFI udostępnianego w Dart.
  • Edytor kodu, np. Visual Studio Code.

2. Pierwsze kroki

Narzędzie ffigen zostało niedawno dodane do Fluttera. Aby sprawdzić, czy zainstalowana wersja Fluttera działa w ramach bieżącej stabilnej wersji, uruchom to polecenie.

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

Sprawdź, czy w wyniku flutter doctor widnieje informacja, że używasz kanału stabilnego i że nie ma nowszych stabilnych wersji Fluttera. Jeśli nie używasz wersji stabilnej lub są dostępne nowsze wersje, uruchom te 2 polecenia, aby zaktualizować narzędzia Flutter.

flutter channel stable
flutter upgrade

Kod w tym CodeLab możesz uruchomić na dowolnym z tych urządzeń:

  • Twój komputer programistyczny (do tworzenia wersji wtyczki i przykładowej aplikacji na komputer)
  • fizyczne urządzenie z Androidem lub iOS połączone z komputerem i ustawione w trybie dewelopera.
  • symulatora iOS (wymaga zainstalowania narzędzi Xcode);
  • emulator Androida (wymaga konfiguracji w Android Studio);

3. Generowanie szablonu wtyczki

Pierwsze kroki z rozwojem wtyczki Flutter

Flutter zawiera szablony wtyczek, które ułatwiają rozpoczęcie pracy. Podczas generowania szablonu wtyczki możesz określić, którego języka chcesz użyć.

Aby utworzyć projekt za pomocą szablonu wtyczki, uruchom to polecenie w katalogu roboczym:

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

Parametr --platforms określa, na których platformach będzie obsługiwany Twój wtyczka.

Układ wygenerowanego projektu możesz sprawdzić za pomocą polecenia tree lub Eksploratora plików w systemie operacyjnym.

$ 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

Warto poświęcić chwilę na zapoznanie się ze strukturą katalogów, aby dowiedzieć się, co zostało utworzone i gdzie się znajduje. Szablon plugin_ffi umieszcza kod Dart w pluginie w katalogach lib, android, ios, linux, macoswindows, a co najważniejsze, w katalogu example.

Dla dewelopera przyzwyczajonego do zwykłego tworzenia aplikacji w Flutterze ta struktura może wydawać się dziwna, ponieważ na najwyższym poziomie nie ma pliku wykonywalnego. Wtyczka ma być dołączona do innych projektów Fluttera, ale kod będziesz uzupełniać w katalogu example, aby sprawdzić, czy działa.

Czas zacząć!

4. Kompilowanie i uruchamianie przykładu

Aby mieć pewność, że system kompilacji i wymagania wstępne są prawidłowo zainstalowane i działają na każdej obsługiwanej platformie, skompiluj i uruchom wygenerowaną przykładową aplikację dla każdego celu.

Windows

Sprawdź, czy używasz obsługiwanej wersji systemu Windows. To ćwiczenie z programowania w języku Java działa w systemach Windows 10 i Windows 11.

Aplikację możesz skompilować w edytorze kodu lub z wiersza poleceń.

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

Powinien wyświetlić się ekran uruchomionej aplikacji podobny do tego:

Aplikacja FFI utworzona na podstawie szablonu, uruchomiona jako aplikacja dla systemu Windows

Linux

Sprawdź, czy używasz obsługiwanej wersji systemu Linux. To ćwiczenie z programowania korzysta z Ubuntu 22.04.1.

Gdy zainstalujesz wszystkie wymagania wstępne wymienione w kroku 2, uruchom w terminalu te polecenia:

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

Powinien wyświetlić się ekran uruchomionej aplikacji podobny do tego:

Aplikacja FFI utworzona na podstawie szablonu działająca jako aplikacja Linuksa

Android

W przypadku Androida możesz użyć systemu Windows, macOS lub Linux do skompilowania aplikacji.

Aby używać odpowiedniej wersji NDK, musisz wprowadzić zmianę w example/android/app/build.gradle.kts.

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

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

Upewnij się, że masz urządzenie z Androidem połączone z komputerem programisty lub że uruchomiono instancję emulatora Androida (AVD). Aby sprawdzić, czy Flutter może połączyć się z urządzeniem z Androidem lub emulatorem, uruchom te polecenia:

$ 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

Po uruchomieniu urządzenia z Androidem (fizycznego lub w emulatorze) uruchom to polecenie:

cd ffigen_app/example
flutter run

Flutter zapyta Cię, na jakim urządzeniu chcesz uruchomić aplikację. Wybierz odpowiednie urządzenie z listy.

Aplikacja FFI utworzona na podstawie szablonu, działająca w emulatorze Androida

macOS i iOS

Do tworzenia aplikacji Flutter na macOS i iOS musisz używać komputera z systemem macOS.

Zacznij od uruchomienia przykładowej aplikacji na macOS. Ponownie sprawdź urządzenia, które widzi Flutter:

$ flutter devices
2 connected devices:

macOS (desktop) • macos  • darwin-arm64   • macOS 13.1 22C65 darwin-arm
Chrome (web)    • chrome • web-javascript • Google Chrome 108.0.5359.98

Uruchom przykładową aplikację za pomocą wygenerowanego projektu wtyczki:

cd ffigen_app/example
flutter run -d macos

Powinien wyświetlić się ekran uruchomionej aplikacji podobny do tego:

Aplikacja FFI utworzona na podstawie szablonu działająca jako aplikacja Linuksa

W przypadku iOS możesz użyć symulatora lub prawdziwego urządzenia. Jeśli korzystasz z symulatora, najpierw go uruchom. Polecenie flutter devices wyświetla teraz symulator jako jedno z dostępnych urządzeń.

$ 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

Po uruchomieniu urządzenia z iOS (fizycznego lub na symulatorze) uruchom to polecenie:

cd ffigen_app/example
flutter run

Flutter zapyta Cię, na jakim urządzeniu chcesz uruchomić aplikację. Wybierz odpowiednie urządzenie z listy.

Aplikacja FFI utworzona na podstawie szablonu, uruchomiona w symulatorze iOS

Symulator iOS ma wyższy priorytet niż docel macOS, więc możesz pominąć określenie urządzenia za pomocą parametru -d.

Gratulacje, udało Ci się utworzyć i uruchomić aplikację na 5 różnych systemach operacyjnych. Następnie utwórz natywny wtyczkę i zaimplementuj interfejs za pomocą FFI.

5. Korzystanie z Duktape w systemach Windows, Linux i Android

Biblioteka C, której użyjesz w tym laboratorium kodu, to Duktape. Duktape to mechanizm JavaScript, który można umieszczać w innych aplikacjach. Jego zalety to przenośność i mała wielkość. W tym kroku skonfigurujesz wtyczkę, aby skompilować bibliotekę Duktape, powiązać ją z wtyczką, a potem uzyskać do niej dostęp za pomocą interfejsu FFI Darta.

Ten krok konfiguruje integrację do działania w systemach Windows, Linux i Android. Integracja z iOS i macOS wymaga dodatkowej konfiguracji (oprócz tej opisanej w tym kroku), aby uwzględnić skompilowaną bibliotekę w końcowym pliku wykonywalnym Fluttera. W następnym kroku omówimy wymaganą dodatkową konfigurację.

Odzyskiwanie Duktape

Najpierw pobierz kopię kodu źródłowego duktape, pobierając go z witryny duktape.org.

W systemie Windows możesz użyć PowerShell z Invoke-WebRequest:

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

W przypadku systemu Linux dobrym wyborem jest wget.

$ wget https://duktape.org/duktape-2.7.0.tar.xz
--2022-12-22 16:21:39--  https://duktape.org/duktape-2.7.0.tar.xz
Resolving duktape.org (duktape.org)... 104.198.14.52
Connecting to duktape.org (duktape.org)|104.198.14.52|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1026524 (1002K) [application/x-xz]
Saving to: 'duktape-2.7.0.tar.xz'

duktape-2.7.0.tar.x 100%[===================>]   1002K  1.01MB/s    in 1.0s

2022-12-22 16:21:41 (1.01 MB/s) - 'duktape-2.7.0.tar.xz' saved [1026524/1026524]

Plik jest archiwum tar.xz. W systemie Windows możesz pobrać narzędzia 7-Zip i użyć ich w ten sposób.

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

Musisz uruchomić 7z dwa razy: pierwszy raz, aby rozpakować kompresję xz, a drugi, aby rozpakować archiwum 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

W nowoczesnych środowiskach Linuxa tar wyodrębnia zawartość w jednym kroku w ten sposób:

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

Instalowanie LLVM

Aby używać ffigen, musisz zainstalować LLVM, którego ffigen używa do analizowania nagłówków C. W systemie Windows uruchom to polecenie:

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

Aby zakończyć instalację LLVM na komputerze z systemem Windows, skonfiguruj ścieżki systemowe, dodając C:\Program Files\LLVM\bin do ścieżki wyszukiwania binarnego. Aby sprawdzić, czy został prawidłowo zainstalowany, wykonaj te czynności.

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

W Ubuntu zależność LLVM można zainstalować w ten sposób: Inne dystrybucje Linuksa mają podobne zależności w przypadku LLVM i 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) ...

Jak wspomniano powyżej, możesz przetestować instalację LLVM w systemie Linux w ten sposób:

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

Skonfiguruj ffigen

Szablon wygenerowany na najwyższym poziomie pubpsec.yaml może zawierać nieaktualne wersje pakietu ffigen. Aby zaktualizować zależności Dart w projekcie wtyczki, uruchom to polecenie:

flutter pub upgrade --major-versions

Teraz, gdy pakiet ffigen jest aktualny, skonfiguruj pliki, których ffigen będzie używać do generowania plików wiązania. Zmodyfikuj zawartość pliku ffigen.yaml projektu, aby odpowiadała poniższemu.

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

Ta konfiguracja zawiera plik nagłówka C, który należy przekazać do LLVM, plik wyjściowy do wygenerowania, opis do umieszczenia na początku pliku oraz sekcję wstępną, która służy do dodawania ostrzeżeń lint.

Na końcu pliku znajduje się jeden element konfiguracji, który wymaga wyjaśnienia. Od wersji 11.0.0 generatora powiązań domyślnie nie generuje powiązań, jeśli podczas analizowania plików nagłówków wystąpiły ostrzeżenia lub błędy generowane przez clang.ffigen

Pliki nagłówka Duktape, w sposób, w jaki zostały napisane, powodują, że clang w systemie macOS generuje ostrzeżenia z powodu braku specyfikatorów typu „null” w wskaźnikach Duktape. Aby w pełni obsługiwać systemy macOS i iOS, Duktape musi dodać te specyfikatory typów do kodu źródłowego Duktape. Tymczasem postanowiliśmy zignorować te ostrzeżenia, ustawiając flagę ignore-source-errors na true.

W przypadku aplikacji produkcyjnej przed jej udostępnieniem musisz wyeliminować wszystkie ostrzeżenia kompilatora. Jednak w przypadku Duktape jest to poza zakresem tego ćwiczenia.

Więcej informacji o innych kluczach i wartościach znajdziesz w dokumentacji ffigen.

Musisz skopiować określone pliki Duktape z dystrybucji Duktape do lokalizacji, w której ffigen ma je znaleźć.

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/

Teoretycznie wystarczy skopiować duktape.h do ffigen, ale skonfigurujesz CMake, aby skompilować bibliotekę, która potrzebuje wszystkich 3 wersji. Aby wygenerować nowe powiązanie, uruchom ffigen:

$ 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

W każdym systemie operacyjnym zobaczysz inne ostrzeżenia. Na razie możesz je zignorować, ponieważ Duktape 2.7.0 kompiluje się z clang w systemach Windows, Linux i macOS.

Konfigurowanie CMake

CMake to system generowania systemu kompilacji. Ta wtyczka używa CMake do generowania systemu kompilacji dla Androida, Windowsa i Linuxa, aby uwzględnić Duktape w wygenerowanym binarnym pliku Fluttera. Musisz zmodyfikować wygenerowany przez szablon plik konfiguracji CMake w ten sposób:

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()

Konfiguracja CMake dodaje pliki źródłowe, a co ważniejsze, modyfikuje domyślne zachowanie wygenerowanego pliku biblioteki w Windows, aby domyślnie eksportować wszystkie symbole C. Jest to obejście CMake, które ułatwia przenoszenie bibliotek w stylu Unix, do których należy Duktape, do świata Windows.

Zamień zawartość pliku lib/ffigen_app.dart na tę:

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

Ten plik odpowiada za wczytywanie pliku biblioteki dynamicznych (.so w przypadku systemu Linux i Androida, .dll w przypadku systemu Windows) oraz za udostępnianie owijki, która udostępnia bardziej idiomatyczny interfejs Dart do kodu C.

Ponieważ ten plik importuje bezpośrednio pakiet ffi, musisz przenieść go z poziomu dev_dependencies do dependencies. Najszybszym sposobem jest uruchomienie tego polecenia:

dart pub add ffi

Zamień zawartość przykładu main.dart na tę:

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

Teraz możesz ponownie uruchomić przykładową aplikację, korzystając z jednego z tych sposobów:

cd example
flutter run

Aplikacja powinna działać w ten sposób:

Wyświetlanie zainicjowanego w aplikacji Windows Duktape

Wyświetlanie danych wyjściowych kodu JavaScript w Duktape w aplikacji Windows

Te 2 zrzuty ekranu przedstawiają sytuację przed i po naciśnięciu przycisku Uruchom JavaScript. Ten przykład pokazuje wykonywanie kodu JavaScript z Dartu i wyświetlanie wyniku na ekranie.

Android

Android to system operacyjny oparty na jądrze Linuksa i jest nieco podobny do dystrybucji Linuksa na komputery. System kompilacji CMake może ukryć większość różnic między tymi platformami. Aby skompilować i uruchomić aplikację na Androidzie, upewnij się, że emulowany Android jest uruchomiony (lub że urządzenie z Androidem jest podłączone). Uruchom aplikację. Na przykład:

cd example
flutter run -d emulator-5554

Powinna się teraz uruchomić przykładowa aplikacja na Androida:

Wyświetlanie inicjowania Duktape w emulatorze Androida

Wyświetlanie danych wyjściowych kodu JavaScript w Duktape w emulatorze Androida

6. Korzystanie z Duktape w systemie macOS i iOS

Teraz czas uruchomić wtyczkę na macOS i iOS, czyli dwóch powiązanych ze sobą systemach operacyjnych. Zacznij od macOS. CMake obsługuje systemy macOS i iOS, ale nie można ponownie użyć kodu napisanego na potrzeby Linuxa i Androida, ponieważ Flutter w systemach macOS i iOS używa CocoaPods do importowania bibliotek.

Czyszczenie

W poprzednim kroku utworzyliśmy działającą aplikację na Androida, Windowsa i Linuxa. Pozostało jednak kilka plików z pierwotnego szablonu, które musisz teraz usunąć. Usuń je teraz w ten sposób:

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

macOS

Flutter na platformie macOS używa CocoaPods do importowania kodu C i C++. Oznacza to, że ten pakiet musi zostać zintegrowany z infrastrukturą kompilacji CocoaPods. Aby umożliwić ponowne użycie kodu C skompilowanego w CMake w poprzednim kroku, musisz dodać jeden plik przekierowujący w Runnerze platformy macOS.

macos/Classes/duktape.c

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

Ten plik korzysta z kompilatora C, aby uwzględnić kod źródłowy z natywnego kodu źródłowego skonfigurowanego w poprzednim kroku. Więcej informacji o tym, jak to działa, znajdziesz w pliku macos/ffigen_app.podspec.

Uruchomienie tej aplikacji odbywa się teraz w taki sam sposób jak w Windows i Linuxie.

cd example
flutter run -d macos

Wyświetlanie zainicjowania Duktape w aplikacji na macOS

Wyświetlanie danych wyjściowych kodu JavaScript w aplikacji macOS

iOS

Podobnie jak w przypadku konfiguracji systemu macOS, iOS wymaga dodania pojedynczego pliku C do przekierowania.

ios/Classes/duktape.c

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

Dzięki temu jednemu plikowi wtyczka jest teraz skonfigurowana tak, aby działać na iOS. Uruchom go jak zwykle.

flutter run -d iPhone

Ilustracja pokazująca inicjowanie Duktape w symulatorze iOS

Wyświetlanie danych wyjściowych JavaScriptu Duktape w symulatorze iOS

Gratulacje! Udało Ci się zintegrować kod natywny na 5 platformach. To powód do świętowania! Może nawet bardziej funkcjonalny interfejs użytkownika, który utworzysz w następnym kroku.

7. Wdrażanie pętli odczytu i wydruku

Interakcje z językiem programowania są dużo przyjemniejsze w szybkim, interaktywnym środowisku. Pierwotna implementacja takiego środowiska to pętla odczytu, oceny i wydruku (REPL) w języku LISP. Na tym etapie zaimplementujesz coś podobnego za pomocą Duktape.

Przygotowanie do wdrożenia

Obecny kod, który współpracuje z biblioteką C Duktape, zakłada, że nic nie może pójść nie tak. Podczas testowania nie wczytuje bibliotek linków dynamicznych Duktape. Aby przygotować tę integrację do wdrożenia, musisz wprowadzić kilka zmian w 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;
}

Wymaga to dodania pakietu path.

flutter pub add path

Kod służący do wczytywania biblioteki linków dynamicznych został rozszerzony, aby obsługiwać przypadki, w których wtyczka jest używana w ramach testów. Umożliwia to napisanie testu integracji, który testuje ten interfejs API jako test Fluttera. Kod służący do oceny ciągu kodu JavaScript został rozszerzony, aby prawidłowo obsługiwać stany błędów, np. niekompletny lub niepoprawny kod. Ten dodatkowy kod pokazuje, jak radzić sobie z sytuacjami, w których ciągi znaków są zwracane jako tablice bajtów i muszą zostać przekonwertowane na ciągi znaków w języku Dart.

Dodawanie pakietów

Podczas tworzenia REPL wyświetlasz interakcję między użytkownikiem a silnikiem JavaScript Duktape. Użytkownik podaje wiersze kodu, a Duktape odpowiada wynikiem obliczeń lub wyjątkiem. Aby ograniczyć ilość kodu stałego, który musisz napisać, użyjesz freezed. Użyjesz też atrybutu google_fonts, aby wyświetlane treści były bardziej związane z tematem, oraz atrybutu flutter_riverpod do zarządzania stanami.

Dodaj do przykładowej aplikacji wymagane zależności:

cd example
flutter pub add flutter_riverpod freezed_annotation google_fonts
flutter pub add -d build_runner freezed

Następnie utwórz plik, w którym zapiszesz interakcję z 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;
}

Ta klasa korzysta z funkcji freezed typu zjednoczenia, aby umożliwić bezpieczne wyrażanie kształtu każdej linii wyświetlanej w repl jako jeden z 3 typów. W tym momencie Twój kod prawdopodobnie wyświetla błąd, ponieważ musi zostać wygenerowany dodatkowy kod. Zrób to teraz w ten sposób.

flutter pub run build_runner build

Spowoduje to wygenerowanie pliku example/lib/duktape_message.freezed.dart, na którym opiera się kod, który właśnie wpisujesz.

Następnie musisz wprowadzić kilka zmian w plikach konfiguracji macOS, aby umożliwić google_fonts wysyłanie żądań sieciowych dotyczących danych czcionek.

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>

Tworzenie REPL

Po zaktualizowaniu warstwy integracji w celu obsługi błędów i utworzeniu reprezentacji danych dla interakcji nadszedł czas na stworzenie interfejsu przykładowej aplikacji.

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

Ten kod jest bardzo rozbudowany, ale nie da się go w pełni omówić w ramach tego Codelab. Zalecamy uruchomienie kodu, a potem wprowadzenie w nim zmian po zapoznaniu się z odpowiednią dokumentacją.

cd example
flutter run

REPL Duktape działający w aplikacji Linuksa

REPL Duktape działający w aplikacji Windows

REPL Duktape działający w symulatorze iOS

REPL Duktape działający w narzędziu Android Emulator

8. Gratulacje

Gratulacje! Udało Ci się utworzyć wtyczkę Flutter FFI na potrzeby systemów Windows, macOS, Linux, Android i iOS.

Po utworzeniu wtyczki możesz udostępnić ją online, aby inni mogli z niej korzystać. Pełną dokumentację dotyczącą publikowania wtyczki na pub.dev znajdziesz w artykule Tworzenie pakietów wtyczek.