Menggunakan FFI di plugin Flutter

1. Pengantar

FFI (foreign function interface) Dart memungkinkan aplikasi Flutter menggunakan library native yang mengekspos C API. Dart mendukung FFI di Android, iOS, Windows, macOS, dan Linux. Untuk web, Dart mendukung interop JavaScript, tetapi subjek tersebut tidak dibahas dalam codelab ini.

Yang akan Anda bangun

Dalam codelab ini, Anda akan membangun plugin seluler dan desktop yang menggunakan library C. Dengan API ini, Anda akan menulis aplikasi contoh sederhana yang memanfaatkan plugin. Plugin dan aplikasi Anda akan:

  • Mengimpor kode sumber library C ke plugin Flutter baru Anda
  • Menyesuaikan plugin agar dapat dibangun di Windows, macOS, Linux, Android, dan iOS
  • Membangun aplikasi yang menggunakan plugin untuk JavaScript REPL (read eval print loop)

76b496eb58ef120a.png

Yang akan Anda pelajari

Dalam codelab ini, Anda akan mempelajari pengetahuan praktis yang diperlukan untuk membangun plugin Flutter berbasis FFI di platform desktop dan seluler, yang meliputi:

  • Membuat template plugin Flutter berbasis FFI Dart
  • Menggunakan paket ffigen guna menghasilkan kode binding untuk library C
  • Menggunakan CMake guna membangun plugin Flutter FFI untuk Android, Windows, dan Linux
  • Menggunakan CocoaPods guna membangun plugin Flutter FFI untuk iOS dan macOS

Yang akan Anda butuhkan

  • Android Studio versi 4.1 atau yang lebih baru untuk pengembangan Android
  • Xcode 13 atau yang lebih baru untuk pengembangan iOS dan macOS
  • Visual Studio 2022 atau Visual Studio Build Tools 2022 dengan pengembangan Desktop dengan beban kerja C++ untuk pengembangan Windows
  • Flutter SDK
  • Semua alat build yang diperlukan untuk platform yang akan Anda kembangkan (misalnya CMake, CocoaPods, dan sebagainya).
  • LLVM untuk platform yang akan Anda kembangkan. Rangkaian alat compiler LLVM digunakan oleh ffigen untuk menguraikan file header C guna membangun binding FFI yang diekspos di Dart.
  • Editor kode, seperti Visual Studio Code.

2. Memulai

Alat ffigen adalah fitur yang baru ditambahkan di Flutter. Anda dapat memastikan bahwa instalasi Flutter Anda menjalankan rilis terbaru yang stabil dengan menjalankan perintah berikut.

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

Pastikan output flutter doctor menyatakan bahwa Anda sedang berada di saluran stabil, dan belum tersedia rilis Flutter stabil yang lebih baru. Jika Anda sedang tidak berada di saluran stabil, atau tersedia rilis yang lebih baru, jalankan dua perintah berikut untuk mempercepat alat Flutter Anda.

$ flutter channel stable
$ flutter upgrade

Anda dapat menjalankan code di codelab ini menggunakan salah satu perangkat berikut:

  • Komputer pengembangan Anda (untuk build desktop plugin Anda dan contoh aplikasi)
  • Perangkat Android atau iOS fisik yang terhubung ke komputer dan disetel ke mode Developer
  • Simulator iOS (perlu menginstal alat Xcode)
  • Android Emulator (memerlukan penyiapan di Android Studio)

3. Membuat template plugin

Memulai pengembangan plugin Flutter

Flutter dilengkapi dengan template untuk plugin yang memudahkan pengguna untuk memulai. Saat membuat template plugin, Anda dapat menentukan bahasa pemrograman apa yang ingin digunakan.

Jalankan perintah berikut di direktori kerja Anda untuk membuat project menggunakan template plugin:

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

Parameter --platforms menentukan platform mana yang akan didukung plugin Anda.

Anda dapat memeriksa tata letak project yang dihasilkan menggunakan perintah tree, atau file eksplorer sistem operasi Anda.

$ 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

Sebaiknya luangkan waktu sejenak melihat struktur direktori untuk membiasakan diri dengan yang dihasilkan dan lokasinya. Template plugin_ffi menempatkan kode Dart untuk plugin di bagian lib, direktori khusus platform bernama android, ios, linux, macos, dan windows, serta yang paling penting, direktori example.

Untuk developer yang terbiasa dengan pengembangan Flutter normal, struktur ini mungkin terasa aneh, karena tidak ada file yang dapat dieksekusi ditentukan di bagian atas. Plugin dimaksudkan untuk disertakan di project Flutter lainnya, tetapi Anda akan memperjelas kode di direktori example untuk memastikan bahwa kode plugin Anda berfungsi.

Saatnya untuk memulai.

4. Membangun dan menjalankan contoh

Untuk memastikan bahwa sistem build dan prasyarat terinstal dengan benar dan berfungsi untuk setiap platform yang didukung, bangun dan jalankan aplikasi contoh yang dihasilkan untuk setiap target.

Windows

Pastikan Anda menggunakan versi Windows yang didukung. Codelab ini diketahui berfungsi pada Windows 10 dan Windows 11.

Anda dapat membangun aplikasi dari editor kode, atau pada command line.

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

Anda akan melihat jendela aplikasi berjalan seperti berikut:

3e0aca5027bf9ee5.png

Linux

Pastikan Anda menggunakan versi Linux yang didukung. Codelab ini menggunakan Ubuntu 22.04.1.

Setelah Anda menginstal semua prasyarat yang tercantum di Langkah 2, jalankan perintah berikut di terminal:

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

Anda akan melihat jendela aplikasi berjalan seperti berikut:

d2298ee958814232.png

Android

Untuk Android, Anda dapat menggunakan Windows, macOS, atau Linux untuk kompilasi. Pertama, pastikan Anda memiliki perangkat Android yang terhubung ke komputer pengembangan Anda atau menjalankan instance Android Emulator (AVD). Pastikan bahwa Flutter dapat terhubung ke perangkat Android atau emulator dengan menjalankan perintah berikut:

$ 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

5616e9d659614460.png

macOS dan iOS

Untuk pengembangan Flutter macOS dan iOS, Anda harus menggunakan komputer macOS.

Mulailah dengan menjalankan aplikasi contoh di macOS. Pastikan lagi perangkat yang dilihat 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

Jalankan aplikasi contoh menggunakan project plugin yang dihasilkan:

$ cd ffigen_app/example
$ flutter run -d macos

Anda akan melihat jendela aplikasi berjalan seperti berikut:

808f738662f4a43.png

Untuk iOS, Anda dapat menggunakan simulator atau perangkat hardware fisik. Jika menggunakan simulator, luncurkan simulator. Perintah flutter devices kini mencantumkan simulator sebagai salah satu perangkat yang tersedia.

$ 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

Setelah simulator dimulai, jalankan: flutter run.

$ cd ffigen_app/example
$ flutter run -d iphone

d39c62d1959718cd.png

Simulator iOS lebih diprioritaskan daripada target macOS, jadi Anda tidak harus menentukan perangkat dengan parameter -d.

Selamat, Anda berhasil membangun dan menjalankan aplikasi pada lima sistem operasi yang berbeda. Langkah selanjutnya adalah membangun plugin native dan menyesuaikan antarmukanya dengan aplikasi ini dari Dart menggunakan FFI.

5. Menggunakan Duktape di Windows, Linux, dan Android

Library C yang akan Anda gunakan di codelab ini adalah Duktape. Duktape adalah mesin JavaScript yang dapat disematkan, yang berfokus pada portabilitas dan jejak yang ringkas. Di langkah ini, Anda akan mengonfigurasi plugin untuk mengompilasi library Duktape, menautkan ke plugin, dan mengaksesnya menggunakan FFI Dart.

Langkah ini mengonfigurasi integrasi untuk berfungsi di Windows, Linux, dan Android. Integrasi iOS dan macOS memerlukan konfigurasi tambahan (lebih dari yang diperinci dalam langkah ini) untuk menyertakan library yang dikompilasi ke file Flutter final yang dapat dieksekusi. Konfigurasi tambahan yang diperlukan dibahas di langkah berikutnya.

Mengambil Duktape

Pertama, dapatkan salinan kode sumber duktape dengan mendownload dari situs duktape.org.

Untuk Windows, Anda dapat menggunakan PowerShell dengan Invoke-WebRequest:

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

Untuk Linux, wget adalah pilihan yang baik.

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

Filenya adalah arsip tar.xz. Di Windows, salah satu opsinya adalah mendownload alat 7Zip, dan menggunakannya seperti berikut.

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

Anda perlu menjalankan 7z dua kali, pertama untuk membatalkan pengarsipan kompresi xz, kedua untuk meluaskan arsip 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

Pada lingkungan linux modern, tar mengekstrak konten dalam satu langkah seperti berikut.

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

Menginstal LLVM

Untuk menggunakan ffigen, Anda perlu menginstal LLVM, yang digunakan ffigen untuk menguraikan header C. Di Windows, jalankan perintah berikut.

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

Konfigurasikan jalur sistem untuk menambahkan C:\Program Files\LLVM\bin ke jalur penelusuran biner Anda untuk menyelesaikan instalasi LLVM pada komputer Windows Anda. Anda dapat menguji apakah LLVM sudah terinstal dengan benar seperti berikut.

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

Untuk Ubuntu, dependensi LLVM dapat diinstal seperti berikut. Distribusi Linux lainnya memiliki dependensi serupa untuk LLVM dan 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) ...

Seperti cara di atas, Anda dapat menguji instalasi LLVM Anda di Linux seperti berikut.

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

Mengonfigurasi ffigen

Template yang menghasilkan pubpsec.yaml tingkat teratas mungkin memiliki versi paket ffigen yang sudah tidak berlaku. Jalankan perintah berikut untuk memperbarui dependensi Dart dalam project plugin:

$ flutter pub upgrade --major-versions

Karena sekarang paket ffigen sudah dalam versi terbaru, selanjutnya konfigurasikan file mana yang akan digunakan ffigen untuk menghasilkan file binding. Ubah konten file ffigen.yaml project Anda agar cocok dengan konfigurasi berikut.

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

Konfigurasi ini mencakup file header C untuk diteruskan ke LLVM, file output yang akan dihasilkan, deskripsi yang akan diletakkan di atas file, dan bagian pembukaan yang digunakan untuk menambahkan peringatan lint. Lihat dokumentasi ffigen untuk mengetahui detail selengkapnya tentang kunci dan nilai.

Anda perlu menyalin file Duktape tertentu dari distribusi Duktape ke lokasi ffigen dikonfigurasi untuk menemukannya.

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

Secara teknis, Anda hanya perlu menyalin seluruh duktape.h untuk ffigen, tetapi Anda akan mengonfigurasi CMake untuk membangun library yang memerlukan ketiganya. Jalankan ffigen untuk menghasilkan binding baru:

$ 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

Anda akan melihat peringatan yang berbeda pada setiap sistem operasi. Untuk saat ini, Anda dapat mengabaikannya karena Duktape 2.7.0 diketahui untuk mengompilasi dengan clang pada Windows, Linux, dan macOS.

Mengonfigurasi CMake

CMake adalah sistem pembuatan sistem build. Plugin ini menggunakan CMake untuk menghasilkan sistem build untuk Android, Windows, dan Linux guna menyertakan Duktape ke biner Flutter yang dihasilkan. Anda perlu memodifikasi file konfigurasi CMake yang dihasilkan template seperti berikut.

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)

Konfigurasi CMake menambahkan file sumber, dan yang lebih penting, mengubah perilaku default file library yang dihasilkan di Windows untuk mengekspor semua simbol C secara default. Hal ini adalah pekerjaan CMake untuk membantu membuat port library gaya Unix, seperti peran Duktape di dunia Windows.

Ganti konten lib/ffigen_app.dart dengan konten berikut.

lib/ffigen_app.dart

import 'dart:ffi';
import 'dart:io';
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;
}

File ini bertanggung jawab untuk memuat file library link dinamis (.so untuk Linux dan Android, .dll untuk Windows) dan menyediakan wrapper yang mengekspos antarmuka idiomatis Dart ke kode C yang mendasarinya.

Ganti konten main.dart contoh dengan konten berikut.

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

Anda kini dapat menjalankan aplikasi contoh kembali menggunakan:

$ cd example
$ flutter run

Anda akan melihat aplikasi berjalan seperti ini:

Kedua screenshot ini menunjukkan sebelum dan sesudah menekan tombol Jalankan JavaScript. Contoh ini mendemonstrasikan proses eksekusi kode JavaScript dari Dart dan menunjukkan hasilnya di layar.

Android

Android adalah OS berbasis kernel Linux dan serupa dengan distribusi Linux desktop. Sistem build CMake dapat menyembunyikan sebagian besar perbedaan antara kedua platform. Untuk membangun dan menjalankan di Android, pastikan Android emulator sedang berjalan (atau perangkat Android terhubung). Jalankan aplikasi. Misalnya:

$ cd example
$ flutter run -d emulator-5554

Sekarang Anda akan melihat aplikasi contoh berjalan di Android:

6. Menggunakan Duktape di macOS dan iOS

Sekarang saatnya untuk membuat plugin Anda bekerja di macOS dan iOS, dua sistem operasi yang terkait erat. Memulai dengan macOS. Meskipun CMake mendukung macOS dan iOS, Anda tidak akan menggunakan kembali pekerjaan yang Anda lakukan untuk Linux & Android, karena Flutter di macOS dan iOS menggunakan CocoaPods untuk mengimpor library.

Membersihkan

Di langkah sebelumnya, Anda membangun aplikasi yang bekerja untuk Android, Windows, dan Linux. Namun, ada beberapa file tersisa dari template asli yang sekarang perlu Anda bersihkan. Hapus file tersebut sekarang seperti berikut.

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

macOS

Flutter pada platform macOS menggunakan CocoaPods untuk mengimpor kode C dan C++. Artinya, paket ini perlu diintegrasikan ke infrastruktur build CocoaPods. Untuk memungkinkan penggunaan kembali kode C yang telah Anda konfigurasikan untuk dibangun dengan CMake di langkah sebelumnya, Anda perlu menambahkan satu file terusan di runner platform macOS.

macos/Classes/duktape.c

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

File ini menggunakan kecanggihan preprocessor C untuk menyertakan kode sumber dari kode sumber native yang Anda siapkan di langkah sebelumnya. Lihat macos/ffigen_app.podspec untuk mengetahui detail tentang cara kerja file ini.

Menjalankan aplikasi ini sekarang mengikuti pola yang sama dengan yang Anda lihat di Windows dan Linux.

$ cd example
$ flutter run -d macos

iOS

Mirip dengan penyiapan macOS, iOS juga perlu menambahkan satu file C terusan.

ios/Classes/duktape.c

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

Dengan satu file ini, plugin Anda kini juga dikonfigurasikan untuk dijalankan di iOS. Jalankan seperti biasa.

$ flutter run -d iPhone

Selamat! Anda telah berhasil mengintegrasikan kode native di lima platform. Hal ini perlu dirayakan. Apalagi, antarmuka pengguna yang lebih fungsional, yang akan Anda bangun di langkah selanjutnya.

7. Mengimplementasikan Read Eval Print Loop

Berinteraksi dengan bahasa pemrograman jauh lebih menyenangkan dalam lingkungan interaktif yang cepat. Implementasi asli lingkungan seperti itu adalah Read Eval Print Loop (REPL) LISP. Anda akan mengimplementasikan hal serupa dengan Duktape di langkah ini.

Membuat semuanya siap diproduksi

Kode saat ini yang berinteraksi dengan library C Duktape mengasumsikan bahwa tidak mungkin ada yang gagal. Selain itu, kode ini tidak memuat library link dinamis Duktape saat sedang diuji. Untuk membuat integrasi ini siap diproduksi, Anda perlu melakukan sedikit perubahan pada lib/ffigen_app.dart.

lib/ffigen_app.dart

import 'dart:ffi';
import 'dart:io';
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;
}

Kode untuk memuat library link dinamis telah diperpanjang untuk menangani kasus saat plugin sedang digunakan dalam runner pengujian. Hal ini memungkinkan pengujian integrasi yang melatih API ini ditulis sebagai pengujian Flutter. Kode untuk mengevaluasi string kode JavaScript telah diperpanjang untuk menangani kondisi error, misalnya, kode yang tidak lengkap atau salah. Kode tambahan ini menunjukkan cara menangani situasi saat string ditampilkan sebagai array byte dan perlu dikonversi menjadi string Dart.

Menambahkan paket

Dalam membuat REPL, Anda akan menampilkan interaksi antara pengguna dan mesin JavaScript Duktape. Pengguna memasukkan baris kode, dan Duktape merespons dengan hasil komputasi atau pengecualian. Anda akan menggunakan freezed untuk mengurangi jumlah kode boilerplate yang perlu Anda tulis. Anda juga akan menggunakan google_fonts untuk membuat konten yang ditampilkan lebih sesuai dengan tema, dan flutter_riverpod untuk pengelolaan status.

Tambahkan dependensi yang diperlukan untuk aplikasi contoh:

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

Selanjutnya, buat file untuk merekam interaksi 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;
}

Kelas ini menggunakan fitur jenis union freezed untuk memungkinkan ekspresi bentuk setiap baris yang ditampilkan dengan mudah di REPL sebagai salah satu dari tiga jenis tersebut. Pada tahap ini, kode Anda mungkin menampilkan beberapa bentuk error pada kode ini karena terdapat kode tambahan yang harus dihasilkan. Lakukan seperti berikut.

$ flutter pub run build_runner build

Tindakan ini menghasilkan file example/lib/duktape_message.freezed.dart, yang diandalkan oleh kode yang baru saja Anda ketik.

Selanjutnya, Anda akan perlu membuat sepasang modifikasi pada file konfigurasi macOS untuk memungkinkan google_fonts membuat permintaan jaringan untuk data font.

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>

Membangun REPL

Setelah memperbarui lapisan integrasi untuk menangani error, dan setelah membangun representasi data untuk interaksi, sekarang saatnya Anda membangun antarmuka pengguna aplikasi contoh.

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

Ada banyak hal yang terjadi pada kode ini, tetapi hal ini di luar cakupan pembahasan codelab ini. Sebaiknya jalankan kode, lalu buat modifikasi pada code setelah meninjau dokumentasi yang sesuai.

$ cd example
$ flutter run

8. Selamat

Selamat! Anda telah berhasil membuat plugin berbasis Flutter FFI untuk Windows, macOS, Linux, Android, dan iOS.

Setelah membuat plugin, Anda mungkin ingin membagikannya secara online sehingga pengguna lain dapat menggunakannya. Anda dapat menemukan dokumentasi lengkap terkait memublikasikan plugin ke pub.dev di Mengembangkan paket plugin.