Cómo usar la FFI en un complemento de Flutter

1. Introducción

La FFI (interfaz de función externa) de Dart permite que las apps de Flutter usen las bibliotecas nativas existentes que exponen una API de C. Dart admite la FFI en Android, iOS, Windows, macOS y Linux. Para el caso de la Web, Dart admite la interoperabilidad de JavaScript, pero este codelab no incluye ese tema.

Qué compilarás

En este codelab, compilarás un complemento para dispositivos móviles y de escritorio que usa una biblioteca de C. Con esta API, escribirás una app de ejemplo simple que use ese complemento. Con el complemento y la app, harás lo siguiente:

  • Importarás el código fuente de la biblioteca de C a tu nuevo complemento de Flutter.
  • Personalizarás el complemento de modo que pueda compilarse en Windows, macOS, Linux, Android y iOS.
  • Compilarás una aplicación que use el complemento para un REPL (bucle de lectura, evaluación e impresión) de JavaScript.

76b496eb58ef120a.png

Qué aprenderás

En este codelab, adquirirás los conocimientos prácticos necesarios para compilar un complemento de Flutter basado en una FFI para plataformas móviles y de escritorio, y también harás lo siguiente:

  • Generarás una plantilla del complemento de Flutter basado en una FFI de Dart.
  • Usarás el paquete ffigen para generar código de vinculación para una biblioteca de C.
  • Usarás CMake para compilar un complemento de Flutter basado en la FFI para Android, Windows y Linux.
  • Usarás CocoaPods para compilar un complemento de Flutter basado en la FFI para iOS y macOS.

Requisitos

  • Android Studio 4.1 o una versión posterior para el desarrollo de Android
  • Xcode 13 o una versión posterior para el desarrollo de iOS y macOS
  • Visual Studio 2022 o Visual Studio Build Tools 2022 con la carga de trabajo de desarrollo para dispositivos de escritorio con C++ para el desarrollo de Windows
  • El SDK de Flutter
  • Cualquier herramienta de compilación necesaria para las plataformas para las que estarás desarrollando (por ejemplo, CMake y CocoaPods, entre otras)
  • LLVM para las plataformas para las que estarás desarrollando (ffigen usa el conjunto de herramientas de compilación LLVM para analizar el archivo de encabezado C con el fin de compilar la vinculación de la FFI expuesta en Dart)
  • Un editor de código, como Visual Studio Code

2. Primeros pasos

Las herramientas de ffigen se agregaron recientemente a Flutter. Ejecuta el siguiente comando para confirmar que tu instalación de Flutter está ejecutando la versión estable actual.

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

Confirma que el resultado que se muestra con flutter doctor indica que estás en el canal estable y que no existen versiones estables de Flutter más recientes. Si no estás en el canal estable o si existen versiones más recientes, ejecuta los siguientes comandos para actualizar tus herramientas de Flutter.

$ flutter channel stable
$ flutter upgrade

Puedes ejecutar el código de este codelab usando cualquiera de los siguientes dispositivos:

  • Tu computadora de desarrollo (para compilaciones de escritorio de tu complemento y app de ejemplo)
  • Un dispositivo físico Android o iOS conectado a tu computadora y configurado en el modo de desarrollador
  • El simulador de iOS (requiere la instalación de herramientas de Xcode)
  • Android Emulator (requiere configuración en Android Studio)

3. Cómo generar la plantilla del complemento

Primeros pasos del desarrollo del complemento de Flutter

Flutter se envía con plantillas para complementos que facilitan los primeros pasos. Cuando generas la plantilla del complemento, puedes especificar el lenguaje que desees usar.

Ejecuta el siguiente comando en tu directorio de trabajo para crear el proyecto usando la plantilla del complemento:

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

El parámetro --platforms especifica las plataformas que tu complemento admitirá.

Puedes inspeccionar el diseño del proyecto generado usando el comando tree o el explorador de archivos de tu sistema operativo.

$ 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

Te recomendamos que te tomes un momento para revisar la estructura de directorios y comprender qué se creó y dónde se ubica. La plantilla de plugin_ffi ubica el código de Dart para el complemento en lib, en directorios específicos de la plataforma llamados android, ios, linux, macos y windows, y, principalmente, en un directorio example.

Para un desarrollador acostumbrado al desarrollo normal de Flutter, esta estructura podría parecer extraña, ya que no hay ejecutables definidos en el nivel superior. Un complemento está diseñado para incluirse en otros proyectos de Flutter, pero completarás el código en el directorio example para asegurarte de que el código del complemento funcione.

Es hora de empezar.

4. Cómo compilar y ejecutar el ejemplo

Para asegurarte de que el sistema de compilación y los requisitos previos estén correctamente instalados y funcionen en cada plataforma compatible, compila y ejecuta la app de ejemplo generada para cada segmento.

Windows

Asegúrate de estar usando una versión compatible de Windows. Este codelab funciona en Windows 10 y Windows 11.

Puedes compilar la aplicación desde tu editor de código o desde la línea de comandos.

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

Deberías ver una ventana de la app en ejecución como la que se muestra a continuación:

3e0aca5027bf9ee5.png

Linux

Asegúrate de estar usando una versión compatible de Linux. Este codelab usa Ubuntu 22.04.1.

Una vez que hayas instalado todos los requisitos previos que se indican en el paso 2, ejecuta los siguientes comandos en una 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=/

Deberías ver una ventana de la app en ejecución como la que se muestra a continuación:

d2298ee958814232.png

Android

Para la compilación de Android, puedes usar Windows, macOS o Linux. Primero, asegúrate de que tengas un dispositivo Android conectado a tu computadora de desarrollo o de que estés ejecutando una instancia de Android Emulator (AVD). Ejecuta el siguiente comando para confirmar que Flutter puede conectarse al dispositivo Android o al emulador:

$ 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 y iOS

Para el desarrollo de Flutter en macOS y iOS, debes usar una computadora con macOS.

Comienza ejecutando la app de ejemplo en macOS. Una vez más, confirma los dispositivos detectados por 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

Ejecuta la app de ejemplo usando el proyecto del complemento generado:

$ cd ffigen_app/example
$ flutter run -d macos

Deberías ver una ventana de la app en ejecución como la que se muestra a continuación:

808f738662f4a43.png

Para iOS, puedes usar el simulador o un dispositivo de hardware real. Si usas un simulador, primero, actívalo. El comando flutter devices ahora muestra el simulador como uno de los dispositivos disponibles.

$ 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

Una vez que se inicie el simulador, ejecuta flutter run.

$ cd ffigen_app/example
$ flutter run -d iphone

d39c62d1959718cd.png

El simulador de iOS tiene prioridad sobre el segmento de macOS, por lo que puedes omitir especificar un dispositivo con el parámetro -d.

Felicitaciones, compilaste y ejecutaste con éxito una aplicación en cinco sistemas operativos diferentes. A continuación, aprenderás a compilar el complemento nativo y a crear una interfaz con él a partir de Dart usando la FFI.

5. Cómo usar Duktape en Windows, Linux y Android

La biblioteca de C que usarás en este codelab es Duktape. Duktape es un motor de JavaScript que se puede incorporar, con un enfoque en la portabilidad y en una huella compacta. En este paso, configurarás el complemento para compilar la biblioteca de Duktape, vincularla a tu complemento y acceder a ella mediante la FFI de Dart.

En este paso, se configurará la integración de modo que funcione en Windows, Linux y Android. La integración de iOS y macOS requiere configuración adicional (más allá de lo que se detalla en este paso) para incluir la biblioteca compilada en el ejecutable final de Flutter. Esta configuración adicional se aborda en el siguiente paso.

Cómo recuperar Duktape

En primer lugar, obtén una copia del código fuente de duktape descargándola desde el sitio web duktape.org.

Para Windows, puedes usar PowerShell con Invoke-WebRequest:

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

Para Linux, wget es una buena opción.

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

El archivo es uno del tipo tar.xz. En Windows, una opción es descargar las herramientas 7Zip y usarlas de la forma que se indica a continuación.

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

Debes ejecutar 7z dos veces: la primera, para desarchivar la compresión xz, y la segunda, para expandir el archivo 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

En entornos modernos de Linux, tar extrae el contenido en un paso, como se indica a continuación.

$ 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ómo instalar LLVM

Para usar ffigen, debes instalar LLVM, que ffigen usa para analizar los encabezados de C. En Windows, ejecuta el siguiente comando.

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

Configura las rutas de acceso de tu sistema para agregar C:\Program Files\LLVM\bin a tu ruta de búsqueda de objeto binario a fin de completar la instalación de LLVM en tu máquina Windows. Puedes probar si se instaló correctamente de la siguiente manera.

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

Para Ubuntu, la dependencia de LLVM puede instalarse como se indica a continuación. Otras distribuciones de Linux tienen dependencias similares para LLVM y 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) ...

Como se indicó antes, puedes probar la instalación de LLVM en Linux de la siguiente manera.

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

Cómo configurar ffigen

La plantilla generada de nivel superior pubpsec.yaml podría tener versiones desactualizadas del paquete ffigen. Ejecuta el siguiente comando para actualizar las dependencias de Dart en el proyecto del complemento:

$ flutter pub upgrade --major-versions

Ahora que el paquete ffigen está actualizado, configura qué archivos consumirá ffigen para generar los archivos de vinculación. Modifica el contenido del archivo ffigen.yaml de tu proyecto de modo que coincida con lo siguiente.

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

Esta configuración incluye el encabezado C que se pasará a LLVM, el archivo resultante que se generará, la descripción que se colocará en la parte superior del archivo y una sección introductoria usada para agregar una advertencia de lint. Consulta la documentación de ffigen para obtener más detalles sobre las claves y los valores.

Debes copiar los archivos específicos de Duktape de la distribución de Duktape en la ubicación configurada para que ffigen los busque.

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

Técnicamente, solo necesitas copiar duktape.h para ffigen, pero estás a punto de configurar CMake para compilar la biblioteca que necesita los tres. Ejecuta ffigen para generar la nueva vinculación:

$ 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

Verás distintas advertencias en cada sistema operativo. Puedes ignorarlas por el momento, ya que Duktape 2.7.0 compila con clang en Windows, Linux y macOS.

Cómo configurar CMake

CMake es un sistema de generación de sistemas de compilación. Este complemento usa CMake para generar el sistema de compilación para Android, Windows y Linux de modo que se incluya Duktape en el objeto binario de Flutter generado. Debes modificar el archivo de configuración de CMake generado por la plantilla como se indica a continuación.

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)

La configuración de CMake agrega los archivos fuente y, fundamentalmente, modifica el comportamiento predeterminado del archivo de biblioteca generado en Windows para exportar todos los símbolos C de forma predeterminada. Este es un camino alternativo de CMake para ayudar a portar las bibliotecas de estilo de Unix, como Duktape, al mundo de Windows.

Reemplaza el contenido de lib/ffigen_app.dart con lo siguiente.

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

Este archivo es el responsable de cargar el archivo de biblioteca de vínculos dinámicos (.so para Linux y Android, .dll para Windows) y de brindar una wrapper que exponga una interfaz más idiomática de Dart con el código C subyacente.

Reemplaza el contenido del archivo main.dart de ejemplo con lo siguiente.

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

Ahora puedes volver a ejecutar la app de ejemplo usando lo siguiente:

$ cd example
$ flutter run

Deberías ver la app en ejecución como se indica a continuación:

Estas dos capturas de pantalla muestran el antes y el después de presionar el botón Run JavaScript. Esto demuestra cómo se ejecuta el código de JavaScript desde Dart y cómo se muestra el resultado en la pantalla.

Android

Android es un sistema operativo basado en el kernel de Linux, y, de alguna manera, es similar a las distribuciones Linux de escritorio. El sistema de compilación de CMake puede ocultar la mayoría de las diferencias entre las dos plataformas. Para compilar y ejecutar en Android, asegúrate de que se esté ejecutando el emulador de Android (o de que el dispositivo Android esté conectado). Ejecuta la app. Por ejemplo:

$ cd example
$ flutter run -d emulator-5554

Deberías ver la app de ejemplo ejecutándose en Android:

6. Cómo usar Duktape en macOS y iOS

Es hora de que trabajemos sobre el complemento de modo que funcione en macOS y iOS, dos sistemas operativos estrechamente relacionados. Comienza con macOS. Si bien CMake es compatible con macOS y iOS, no usarás lo que ya hiciste para Linux y Android, ya que Flutter usa CocoaPods para importar bibliotecas en macOS y iOS.

Limpieza

En el paso anterior, compilaste una aplicación que funciona en Android, Windows y Linux. Sin embargo, hay algunos archivos que quedaron de la plantilla original que debes limpiar. Quítalos como se indica a continuación.

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

macOS

Flutter en la plataforma macOS usa CocoaPods para importar código C y C++. Esto significa que este paquete debe integrarse en la infraestructura de compilación de CocoaPods. Para habilitar la reutilización del código C que ya configuraste en el paso anterior para compilar con CMake, deberás agregar un único archivo de reenvío en el ejecutor de la plataforma macOS.

macos/Classes/duktape.c

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

Este archivo usa el poder del preprocesador de C para incluir el código fuente del código fuente nativo que configuraste en el paso anterior. Consulta macos/ffigen_app.podspec para obtener más detalles acerca de este funcionamiento.

La ejecución de esta aplicación ahora sigue el mismo patrón que viste en Windows y Linux.

$ cd example
$ flutter run -d macos

iOS

De forma similar a la configuración de macOS, iOS también requiere que se agregue un único archivo C de reenvío.

ios/Classes/duktape.c

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

Con este archivo único, tu complemento ahora también estará configurado para ejecutarse en iOS. Ejecútalo de la forma habitual.

$ flutter run -d iPhone

¡Felicitaciones! Lograste integrar código nativo en cinco plataformas. ¡Esto merece una celebración! Quizás puedas hacerlo con una interfaz de usuario aún más funcional, que compilarás en el próximo paso.

7. Cómo implementar el bucle de lectura, evaluación e impresión

Interactuar con un lenguaje de programación es mucho más divertido en un entorno interactivo rápido. La implementación original de un entorno de este tipo fue el bucle de lectura, evaluación e impresión (REPL) de LISP. Implementarás algo similar con Duktape en este paso.

Cómo preparar las cosas para producción

En el código actual que interactúa con la biblioteca C de Duktape, se asume que nada puede salir mal. El código no carga las bibliotecas de vínculos dinámicos de Duktape cuando está en fase de prueba. A fin de hacer que esta integración esté lista para producción, debes hacer algunos cambios en 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;
}

El código que cargará la biblioteca de vínculos dinámicos se extendió de modo que pueda controlar el caso en el que el complemento se use en un ejecutor de pruebas. Esto habilita que se escriba una prueba de integración que utilice esta API como prueba de Flutter. El código para evaluar una cadena de código JavaScript se extendió de modo que controle correctamente las condiciones de error, por ejemplo, código incompleto o incorrecto. Este código adicional muestra cómo controlar situaciones en las que las cadenas se muestran como arrays de bytes y necesitan convertirse en cadenas de Dart.

Cómo agregar paquetes

Cuando crees un REPL, mostrarás la interacción entre el usuario y el motor de JavaScript de Duktape. El usuario ingresará líneas de código, y Duktape responderá con el resultado del procesamiento o con una excepción. Usarás freezed para reducir la cantidad de código estándar que debes escribir. También usarás google_fonts para hacer que el contenido que se muestra se adecue más al tema, y flutter_riverpod para la gestión del estado.

Agrega las dependencias a la app de ejemplo:

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

A continuación, crea un archivo para registrar la interacción del 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;
}

Esta clase usa la función de tipo de unión de freezed para permitir una expresión simple de la forma de cada línea que se muestra en el REPL como una de tres tipos. En este punto, probablemente se muestre algún error en este código, ya que se debe generar código adicional. Hazlo ahora como se indica a continuación.

$ flutter pub run build_runner build

Esto genera el archivo example/lib/duktape_message.freezed.dart, y el código que acabas de escribir depende de él.

A continuación, debes hacer un par de modificaciones en los archivos de configuración de macOS a fin de habilitar a google_fonts para que haga solicitudes de red para datos de fuente.

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>

Cómo compilar el REPL

Ahora que actualizaste la capa de integración de modo que controle los errores y compilaste una representación de datos para esa interacción, es hora de que compiles la interfaz de usuario de esa app de ejemplo.

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

Hay mucho que explorar en este código, pero la explicación está fuera del alcance de este codelab. Te sugerimos que ejecutes el código y lo modifiques luego de revisar la documentación adecuada.

$ cd example
$ flutter run

8. Felicitaciones

¡Felicitaciones! Creaste con éxito un complemento de Flutter basado en una FFI para Windows, macOS, Linux, Android y iOS.

Después de crear un complemento, te recomendamos que lo compartas en línea para que otros puedan usarlo. Puedes encontrar la documentación completa para publicar tu complemento en pub.dev, en Cómo desarrollar paquetes de complementos.