Como usar a FFI em um plugin do Flutter

1. Introdução

A FFI (interface de função externa) do Dart permite que apps do Flutter usem bibliotecas nativas que expõem uma API C (links em inglês). O Dart oferece suporte à FFI no Android, iOS, Windows, macOS e Linux. Para a Web, ele oferece suporte à interoperabilidade de JavaScript, mas esse assunto não será abordado neste codelab.

O que você vai criar

Neste codelab, você criará um plug-in para dispositivos móveis e computadores que usa uma biblioteca C. Com essa API, você vai programar um app de exemplo simples que usa o plug-in. O plug-in e o app vão:

  • Importar o código-fonte da biblioteca C para seu novo plug-in do Flutter.
  • Personalizar o plug-in para permitir a criação no Windows, macOS, Linux, Android e iOS.
  • Criar um aplicativo que use o plug-in para um REPL (read reveal print loop) (link em inglês) JavaScript.

76b496eb58ef120a.png

O que você vai aprender

Neste codelab, você vai adquirir o conhecimento prático necessário para criar um plug-in do Flutter baseado em FFI em plataformas para dispositivos móveis e computadores, incluindo:

  • Gerar um modelo de plug-in do Flutter baseado em FFI do Dart (link em inglês).
  • Usar o pacote ffigen para gerar o código de vinculação de uma biblioteca C.
  • Usar o CMake para criar um plugin FFI do Flutter para Android (link em inglês), Windows e Linux.
  • Usar CocoaPods para criar um plugin FFI do Flutter para iOS e macOS.

O que é necessário

  • Android Studio 4.1 ou mais recente para desenvolvimento no Android.
  • Xcode 13 ou mais recente para desenvolvimento no iOS e macOS.
  • O Visual Studio 2022 ou Ferramentas de build do Visual Studio 2022 com a carga de trabalho "Desktop development with C++" para desenvolvimento no Windows.
  • SDK do Flutter (link em inglês).
  • As ferramentas de build necessárias para as plataformas de desenvolvimento, por exemplo, CMake, CocoaPods e assim por diante.
  • LLVM para as plataformas de desenvolvimento (link em inglês). O pacote de ferramentas de compilação do LLVM é usado pelo ffigen para analisar o arquivo C principal e criar a vinculação FFI exposta no Dart.
  • Um editor de código, como o Visual Studio Code (link em inglês).

2. Para começar

O pacote de ferramentas ffigen é uma adição recente ao Flutter. Com o comando abaixo, é possível confirmar se a instalação do Flutter está executando a versão estável atual.

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

Confirme se a saída de flutter doctor indica que você está no canal estável e se não há versões estáveis mais recentes do Flutter disponíveis. Se você não estiver no canal estável ou se houver versões mais recentes disponíveis, execute os dois comandos abaixo para atualizar as ferramentas do Flutter.

$ flutter channel stable
$ flutter upgrade

Para executar o código deste codelab, use qualquer um destes dispositivos:

  • Seu computador de desenvolvimento, para builds de computador do plug-in e aplicativo de exemplo.
  • Um dispositivo físico Android ou iOS conectado ao seu computador e configurado para o modo de desenvolvedor.
  • O simulador para iOS, que exige a instalação de ferramentas do Xcode.
  • O Android Emulator, que requer a configuração no Android Studio.

3. Gerar o modelo do plug-in

Começar o desenvolvimento do plug-in do Flutter

O Flutter vem com modelos de plug-ins que tornam mais fácil começar. Ao gerar o modelo de plug-in, você pode especificar a linguagem que quer usar.

Execute o comando abaixo no diretório de trabalho para criar seu projeto usando o modelo de plug-in:

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

O parâmetro --platforms especifica a quais plataformas o plug-in oferece suporte.

Para inspecionar o layout do projeto gerado, use o comando tree ou o explorador de arquivos do sistema operacional.

$ 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

Vale a pena dar uma conferida na estrutura de diretórios para ter uma ideia do que foi criado e onde cada elemento está localizado. O modelo plugin_ffi insere o código Dart para o plug-in em lib, diretórios específicos da plataforma denominados android, ios, linux, macos e windows e, o mais importante, um diretório example.

Para desenvolvedores acostumados ao desenvolvimento normal com Flutter, essa estrutura pode parecer estranha, porque não há um executável definido no nível superior. Um plug-in precisa ser incluído em outros projetos do Flutter, mas você detalhará o código do plug-in no diretório example para garantir que funcione.

É hora de começar.

4. Criar e executar o exemplo

Para garantir que o sistema de build e os pré-requisitos estejam instalados de forma correta e funcionem em cada plataforma com suporte, crie e execute o app de exemplo gerado para cada destino.

Windows

Verifique se você está usando uma versão do Windows com suporte. Este codelab funciona no Windows 10 e 11.

É possível criar o aplicativo dentro do editor de código ou na linha de comando.

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

Vai aparecer uma janela de app em execução como esta:

3e0aca5027bf9ee5.png

Linux

Use uma versão do Linux com suporte. Este codelab usa Ubuntu 22.04.1.

Depois de instalar todos os pré-requisitos listados na Etapa 2, execute os comandos abaixo em um 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=/

Vai aparecer uma janela de app em execução como esta:

d2298ee958814232.png

Android

Para Android, você pode usar Windows, macOS ou Linux para compilação. Primeiro, conecte um dispositivo Android ao seu computador de desenvolvimento ou execute uma instância do Android Emulator (AVD). Execute o código abaixo para confirmar se o Flutter pode se conectar ao dispositivo Android ou ao 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 e iOS

Para desenvolvimento com Flutter no macOS e iOS, use um computador macOS.

Primeiro, execute o app de exemplo no macOS. Confirme de novo os dispositivos que o Flutter pode acessar:

$ 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

Execute o app de exemplo usando o projeto de plug-in gerado:

$ cd ffigen_app/example
$ flutter run -d macos

Vai aparecer uma janela de app em execução como esta:

808f738662f4a43.png

No iOS, é possível usar o simulador ou um dispositivo de hardware real. Se estiver usando o simulador, abra-o primeiro. O comando flutter devices agora lista o simulador como um dos dispositivos disponíveis.

$ 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

Uma vez iniciado o simulador, execute: flutter run.

$ cd ffigen_app/example
$ flutter run -d iphone

d39c62d1959718cd.png

O simulador para iOS tem precedência sobre o destino do macOS. Você pode pular a especificação de um dispositivo com o parâmetro -d.

Parabéns, você criou e executou um aplicativo em cinco sistemas operacionais diferentes. A seguir, crie o plug-in nativo e faça a interface com ele no Dart usando a FFI.

5. Como usar a Duktape no Windows, Linux e Android

Neste codelab, a biblioteca C usada será a Duktape (link em inglês), que é um mecanismo JavaScript incorporável, com foco em portabilidade e tamanho compacto. Nesta etapa, você vai configurar o plug-in para compilar a biblioteca Duktape, vinculá-la ao plug-in e acessá-la usando a FFI do Dart.

Esta etapa configura a integração para funcionar no Windows, Linux e Android. A integração com iOS e macOS requer outras configurações, além das está detalhadas nesta etapa, para incluir a biblioteca compilada no executável final do Flutter. A configuração extra necessária será abordada na próxima etapa.

Como extrair a Duktape

Primeiro, faça o download de uma cópia do código-fonte da duktape no site duktape.org (em inglês).

Para Windows, use o PowerShell com 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 é uma boa opção.

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

O arquivo é um tar.xz. No Windows, uma opção é fazer o download das ferramentas 7Zip e usá-las desta forma.

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

Execute o 7z duas vezes, primeiro para desarquivar a compactação xz, depois para expandir o arquivo 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

Em ambientes Linux modernos, o tar extrai o conteúdo em uma etapa, desta forma:

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

Como instalar o LLVM

Para usar o ffigen, é preciso instalar o LLVM (link em inglês), que o ffigen usa para analisar cabeçalhos C. No Windows, execute o comando abaixo.

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

Configure os caminhos do sistema para adicionar C:\Program Files\LLVM\bin ao caminho de pesquisa binária e concluir a instalação do LLVM na máquina Windows. Teste se a instalação foi feita corretamente com o comando abaixo.

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, a dependência do LLVM pode ser instalada desta forma: Outras distribuições do Linux têm dependências semelhantes para LLVM e 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) ...

Assim como mostrado acima, teste a instalação do LLVM no Linux com este comando.

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

Como configurar o ffigen

O pubpsec.yaml de nível superior gerado pelo modelo pode ter versões desatualizadas do pacote ffigen. Execute o comando abaixo para atualizar as dependências do Dart no projeto do plug-in:

$ flutter pub upgrade --major-versions

Agora que o pacote ffigen está atualizado, configure quais arquivos o ffigen vai consumir para gerar os arquivos de vinculação. Modifique o conteúdo do arquivo ffigen.yaml do projeto para corresponder ao seguinte.

ffigen.yaml (link em inglês)

# 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

Essa configuração inclui o arquivo de cabeçalho C a ser transmitido para o LLVM, o arquivo de saída a ser gerado, a descrição a ser colocada na parte de cima do arquivo e uma seção de preâmbulo usada para adicionar um aviso de lint. Consulte a documentação do ffigen (link em inglês) para mais detalhes sobre as chaves e os valores.

Copie arquivos específicos da distribuição da Duktape para o local onde o ffigen está configurado para encontrá-los.

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

Tecnicamente, você só precisa copiar duktape.h para o ffigen, mas vamos configurar o CMake para criar a biblioteca que precisa dos três. Execute o ffigen para gerar a nova vinculação:

$ 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

Serão mostrados avisos diferentes em cada sistema operacional. Você pode ignorá-los por enquanto, já que a Duktape 2.7.0 é conhecida por compilar com clang no Windows, Linux e macOS.

Como configurar o CMake

O CMake é usado para gerar sistemas de build. O plug-in usa o CMake para gerar o sistema de build para Android, Windows e Linux e incluir a Duktape no binário do Flutter gerado. Modifique o arquivo de configuração do CMake gerado pelo modelo desta forma.

src/CMakeLists.txt (link em inglês)

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)

A configuração do CMake adiciona os arquivos de origem e, o mais importante, modifica o comportamento padrão do arquivo de biblioteca gerado no Windows para exportar todos os símbolos C por padrão. Essa é uma alternativa do CMake para ajudar na portabilidade de bibliotecas no estilo Unix, como a Duktape, para o ambiente do Windows.

Substitua o conteúdo do arquivo lib/ffigen_app.dart pelo seguinte:

lib/ffigen_app.dart (link em inglês)

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

Esse arquivo é responsável por carregar o arquivo da biblioteca de links dinâmicos (.so para Linux e Android, .dll para Windows) e por fornecer um wrapper que expõe uma interface idiomática mais voltada ao Dart ao código C.

Substitua o conteúdo do arquivo main.dart de exemplo pelo mostrado abaixo.

example/lib/main.dart (link em inglês)

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

Agora, execute o app de exemplo de novo usando:

$ cd example
$ flutter run

Você vai notar que o app aparece desta forma:

Essas duas capturas de tela mostram o que acontece antes e depois de pressionar o botão Run JavaScript. Isso demonstra a execução do código JavaScript no Dart e a exibição do resultado na tela.

Android

O Android é um SO Linux baseado em kernel e é um pouco semelhante às distribuições Linux para computador. O sistema de build CMake pode ocultar a maioria das diferenças entre as duas plataformas. Para criar e executar no Android, verifique se o emulador do Android está em execução ou se o dispositivo Android está conectado. Execute o app. Por exemplo:

$ cd example
$ flutter run -d emulator-5554

Agora você pode encontrar o app de exemplo em execução no Android:

6. Como usar a Duktape no macOS e iOS

É hora de fazer o plug-in funcionar no macOS e no iOS, dois sistemas operacionais estreitamente relacionados. Comece com o macOS. O CMake oferece suporte a macOS e iOS, mas você não vai reutilizar o trabalho feito para Linux e Android, porque o Flutter no macOS e iOS usa CocoaPods para importar bibliotecas.

Como limpar o código

Na etapa anterior, você criou um aplicativo funcional para Android, Windows e Linux. No entanto, agora precisa limpar alguns arquivos restantes do modelo original. Remova-os agora desta forma.

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

macOS

O Flutter na plataforma macOS usa CocoaPods para importar código C e C++. Isso significa que esse pacote precisa ser integrado à infraestrutura de build do CocoaPods. Para ativar a reutilização do código C já configurado para criação com o CMake na etapa anterior, será necessário adicionar um único arquivo de encaminhamento no executor da plataforma macOS.

macos/Classes/duktape.c (link em inglês)

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

Esse arquivo usa o poder do pré-processador C para incluir o código-fonte configurado na etapa anterior. Consulte macos/ffigen_app.podspec (link em inglês) para mais detalhes sobre como isso funciona.

A execução desse aplicativo agora segue o mesmo padrão observado no Windows e no Linux.

$ cd example
$ flutter run -d macos

No iOS

Assim como a configuração para macOS, o iOS também requer a adição de um único arquivo de encaminhamento C.

ios/Classes/duktape.c (link em inglês)

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

Com esse único arquivo, o plug-in também já está configurado para funcionar no iOS. Execute-o normalmente.

$ flutter run -d iPhone

Parabéns! Você integrou o código nativo em cinco plataformas. Isso é motivo de orgulho. Na próxima etapa você pode até mesmo deixar a interface do usuário mais funcional.

7. Implementar o Read Eval Print Loop

Interagir com uma linguagem de programação é muito mais divertido em um ambiente interativo rápido. A implementação original desse ambiente era em um Read Eval Print Loop (REPL) do LISP. Você vai implementar algo semelhante com a Duktape nesta etapa.

Como preparar tudo para produção

O código atual que interage com a biblioteca C Duktape presume que nada pode dar errado. Além disso, ele não carrega as bibliotecas de links dinâmicos da Duktape durante testes. Para preparar a integração para produção, é preciso fazer algumas alterações no arquivo lib/ffigen_app.dart.

lib/ffigen_app.dart (link em inglês)

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

O código para carregar a biblioteca de links dinâmicos foi estendido para lidar com o caso em que o plug-in é usado em um executor de teste. Assim, é possível programar um teste de integração para usar essa API como um teste do Flutter. O código para avaliar uma string de código JavaScript foi estendido para lidar corretamente com condições de erro, por exemplo, código incompleto ou incorreto. Esse código extra mostra como resolver situações em que as strings são retornadas como matrizes de bytes e precisam ser convertidas em strings do Dart.

Como adicionar pacotes

Ao criar um REPL, você vai mostrar uma interação entre o usuário e o mecanismo JavaScript da Duktape. O usuário insere linhas de código, e a Duktape responde com o resultado da computação ou com uma exceção. Você vai usar freezed para reduzir a quantidade de código boilerplate que precisa escrever. Também vai usar google_fonts para deixar o conteúdo mostrado mais relacionado ao tema e flutter_riverpod para gerenciamento de estado.

Adicione as dependências necessárias ao app de exemplo:

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

Em seguida, crie um arquivo para registrar a interação com REPL:

example/lib/duktape_message.dart (link em inglês)

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

Essa classe usa o recurso de tipo de união de freezed para facilitar a expressão da forma de cada linha exibida no REPL como um dos três tipos. Neste ponto, o código provavelmente está mostrando algum tipo de erro, porque é preciso gerar mais códigos. Faça isso agora desta forma:

$ flutter pub run build_runner build

Isso gera o arquivo example/lib/duktape_message.freezed.dart, do qual o código que você acabou de digitar depende.

Em seguida, faça algumas modificações nos arquivos de configuração do macOS para que o elemento google_fonts possa fazer solicitações de rede para dados de fonte.

example/macos/Runner/DebugProfile.entitlements (link em inglês)

<?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 (link em inglês)

<?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>

Como criar o REPL

Agora que você atualizou a camada de integração para lidar com erros e criou uma representação de dados para a interação, é hora de criar a interface de usuário do app de exemplo.

example/lib/main.dart (link em inglês)

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

Este código tem muitos detalhes, mas explicar tudo está fora do escopo deste codelab. Sugerimos que você execute o código e, em seguida, faça modificações nele, após revisar a documentação adequada.

$ cd example
$ flutter run

8. Parabéns

Parabéns! Você criou um plug-in do Flutter baseado em FFI para Windows, macOS, Linux, Android e iOS.

Depois de criar um plug-in, você pode compartilhá-lo on-line para que outras pessoas usem. Para a documentação completa sobre como publicar seu plug-in, consulte pub.dev em Como desenvolver pacotes de plug-in (links em inglês).