FFI: fundamentos, Foreign Function Interface e integração com C

Autor: IT Sectr Publicado: 2026-06-05 Tempo de leitura: 10 min

FFI (Foreign Function Interface) é um mecanismo da linguagem Dart, fornecido pelo pacote dart:ffi, que permite chamar funções de bibliotecas nativas C diretamente, sem camadas intermediárias em Kotlin, Swift ou Java. O desenvolvedor carrega uma biblioteca dinâmica (.so no Android, .dylib no iOS, .dll no Windows), declara as assinaturas das funções C e as chama como funções Dart comuns. De acordo com a Referência da Dart API (2025), FFI reduz a sobrecarga das chamadas entre linguagens para 0,1 µs, o que é dezenas de vezes mais rápido que pelo Method Channel.

Principais conclusões

  • FFI (Foreign Function Interface) — mecanismo para chamar funções C diretamente do Dart
  • dart:ffi fornece API para carregar bibliotecas e declarar assinaturas
  • Desempenho das chamadas FFI é 50–100 vezes maior que pelo Method Channel
  • Tipagem FFI suporta tipos primitivos, structs e ponteiros C
  • Flutter usa FFI para integração com bibliotecas nativas: OpenCV, SQLite, FFmpeg

O que é FFI?

FFI (Foreign Function Interface) é um mecanismo que permite a uma linguagem de programação chamar funções escritas em outras linguagens. No contexto de Dart e Flutter, FFI significa a capacidade de chamar funções de bibliotecas C/C++ diretamente do código Dart, sem a necessidade de escrever código específico de plataforma em Java (Android) ou Swift/Objective-C (iOS).

O pacote dart:ffi apareceu no Dart 2.12 (2021) e desde então se tornou uma ferramenta fundamental para integrar o Flutter com código nativo. Antes do dart:ffi, a única maneira de chamar uma função C do Dart era através do Method Channel, um mecanismo assíncrono que transmitia mensagens via serialização JSON entre o Dart e o lado nativo. O FFI funciona de forma diferente: o código Dart acessa diretamente a memória da biblioteca C, chamando funções através da ABI nativa (Application Binary Interface) sem serialização ou troca de contexto.

O FFI é especialmente requisitado em cenários onde o desempenho é crítico: processamento de imagens (OpenCV), áudio (FFmpeg), criptografia (OpenSSL), aprendizado de máquina (TensorFlow Lite) e bancos de dados (SQLite). Em todos esses casos, o Method Channel cria atrasos inaceitáveis, enquanto o FFI oferece desempenho comparável ao código C/C++ nativo. A biblioteca dart:ffi também suporta gerenciamento de memória: alocação, liberação e manipulação de ponteiros.

FFI vs Method Channel: diferença fundamental

O Method Channel funciona de forma assíncrona: o Dart envia uma mensagem para o código nativo, o código nativo a processa e envia o resultado de volta. Cada chamada requer serialização dos argumentos em um Map, passagem por uma fila e desserialização. Isso leva 0,5–5 ms por chamada. O FFI funciona de forma síncrona e sem serialização — uma chamada de função C leva 0,01–0,1 µs. Uma diferença de 50–500 vezes, que é crítica para operações de alta frequência.

Como o dart:ffi funciona?

Trabalhar com dart:ffi consiste em três etapas: carregar a biblioteca, declarar assinaturas e chamar funções. Cada etapa utiliza a tipagem estrita do Dart, minimizando erros em tempo de execução.

Na primeira etapa, a biblioteca dinâmica é carregada através da classe DynamicLibrary. A biblioteca pode ser carregada pelo nome (libxyz.so, libxyz.dylib, xyz.dll) ou pelo caminho completo. O Dart procura automaticamente a biblioteca nos caminhos padrão do sistema. DynamicLibrary fornece o método lookupFunction, que vincula uma função Dart a uma função C pelo nome do símbolo.

Na segunda etapa, uma função Dart é declarada com anotações de tipo correspondentes à assinatura C. Tipos especiais do dart:ffi são usados: Int32, Float, Double, Pointer, NativeFunction, Handle e outros. A anotação lookupFunction aceita dois parâmetros genéricos: o tipo da função Dart (como ela será no Dart) e o tipo da função C nativa (como ela é declarada em C).

Na terceira etapa, a função Dart gerada é chamada como uma função normal. Os argumentos são passados diretamente, o resultado é retornado imediatamente. Se a função C modificar a memória através de ponteiros, o Dart pode ler essas alterações através da classe Pointer. O gerenciamento de memória do lado C continua sendo responsabilidade do desenvolvedor — o dart:ffi não gerencia a memória alocada por malloc em C.

Exemplo básico: chamar uma função C do Dart

dart
import 'dart:ffi'
import 'package:ffi/ffi.dart'

// Declaração de função C: int add(int a, int b)
typedef AddNative = Int32 Function(Int32, Int32)
typedef AddDart = int Function(int, int)

void main() {
    final lib = DynamicLibrary.open('libcalculator.so')
    final AddDart add = lib
        .lookupFunction<AddNative, AddDart>('add')

    print(add(5, 3)) // 8
}

Neste exemplo, add é uma função C que recebe dois ints e retorna um int. O typedef AddNative descreve a assinatura C com tipos dart:ffi, enquanto AddDart descreve como essa função será no Dart. lookupFunction as vincula e retorna uma função Dart que pode ser chamada como uma normal.

Tipos de dados em FFI

dart:ffi fornece um conjunto de tipos correspondentes aos tipos C. Cada tipo tem um tamanho fixo e regras de conversão entre Dart e C. Compreender o mapeamento de tipos é criticamente importante para o funcionamento correto do FFI — um erro no tamanho ou sinal do tipo pode causar a falha do aplicativo.

Tipo CTipo dart:ffiTipo DartTamanho (bytes)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8 (ponteiro)
void*Pointer<Void>Pointer8 (ponteiro)
structPointer<T> (Struct)Pointerdepende dos campos

Para trabalhar com strings C (char*), o dart:ffi usa Pointer<Int8>. A conversão de Dart String para C char* e vice-versa é feita através de toNativeUtf8 (do pacote ffi) e fromUtf8. É importante liberar as strings C após o uso através de calloc.free para evitar vazamentos de memória.

Estruturas (Struct)

dart:ffi suporta a declaração de estruturas C como classes Dart que herdam de Struct. Os campos da estrutura são declarados com anotações @Int32(), @Float(), @Array() e outras. O tamanho e o deslocamento dos campos são calculados automaticamente de acordo com a ABI da plataforma. Pointer<Point> pode ser obtido de uma função C que retorna um ponteiro para uma estrutura, ou alocado no Dart via calloc.

dart
// Struct C: typedef struct { int x; int y; } Point;
final class Point extends Struct {
    @Int32()
    external int x

    @Int32()
    external int y
}

// Chamando função C que retorna Point*
typedef CreatePointNative = Pointer<Point> Function(Int32, Int32)
typedef CreatePointDart = Pointer<Point> Function(int, int)

final Pointer<Point> p = createPoint(10, 20)
print('x: ${p.ref.x}, y: ${p.ref.y}')
calloc.free(p) // liberar memória

A classe Point estende Struct e declara os campos x e y com anotações @Int32(). O código C gerado terá exatamente o mesmo layout de memória. Pointer.ref fornece acesso aos campos da estrutura através de getters e setters.

Exemplos práticos de FFI

Vamos ver um exemplo mais complexo — integração com uma biblioteca C para calcular o hash SHA256. Esta é uma tarefa típica onde o FFI oferece um ganho significativo de desempenho em comparação com o Method Channel.

Integração com OpenSSL via FFI

A biblioteca OpenSSL fornece a função SHA256, que calcula o hash de uma string. Através do dart:ffi, podemos chamá-la diretamente, sem escrever wrappers em Java ou Swift. Este é um exemplo de como o FFI permite reutilizar bibliotecas C existentes no Flutter.

dart
import 'dart:ffi'
import 'package:ffi/ffi.dart'

// Assinatura: unsigned char* SHA256(
//   const unsigned char *d, size_t n, unsigned char *md)
typedef Sha256Native = Pointer<Uint8> Function(
    Pointer<Uint8>, Size, Pointer<Uint8>)
typedef Sha256Dart = Pointer<Uint8> Function(
    Pointer<Uint8>, int, Pointer<Uint8>)

String sha256(String input) {
    final lib = DynamicLibrary.open('libcrypto.so')
    final Sha256Dart sha256Fn = lib
        .lookupFunction<Sha256Native, Sha256Dart>('SHA256')

    final inputPtr = input.toNativeUtf8()
    final outputPtr = calloc(Uint8)(32) // SHA256 = 32 bytes

    sha256Fn(inputPtr, input.length, outputPtr)

    final digest = outputPtr.asTypedList(32)
    final hex = digest.map((b) => b.toRadixString(16)
        .padLeft(2, '0')).join()

    calloc.free(inputPtr)
    calloc.free(outputPtr)

    return hex
}

Neste exemplo, a função sha256 carrega a biblioteca libcrypto.so, encontra o símbolo SHA256 e o chama com ponteiros para os dados de entrada e saída. toNativeUtf8 converte uma Dart String em uma string C (aloca memória), e asTypedList permite ler o array de bytes do resultado. A memória é liberada após o uso — esta é uma etapa obrigatória para evitar vazamentos.

Alocação e liberação de memória

O pacote ffi fornece a função calloc para alocar memória compatível com C. A memória alocada deve ser liberada através de calloc.free, caso contrário ocorrerá um vazamento. Para gerenciamento automático de memória, você pode usar a classe Arena do pacote ffi, que libera toda a memória alocada dentro dela quando arena.release() é chamado. Isso é especialmente conveniente para um grande número de alocações temporárias.

Limitações e melhores práticas

Apesar do poder do FFI, ele tem limitações que devem ser consideradas ao projetar a arquitetura de um aplicativo Flutter. As principais limitações estão relacionadas à segurança de tipos, gerenciamento de memória e compatibilidade de plataforma.

Segurança

O FFI não realiza verificação de tipos em tempo de execução. Se uma função C espera um ponteiro mas recebe um número, o aplicativo falhará com uma falha de segmentação. Recomenda-se usar o FFIgen, uma ferramenta que gera wrappers Dart com segurança de tipos com base em arquivos de cabeçalho C (.h). FFIgen analisa declarações de funções C e cria código Dart com tipos corretos, eliminando erros na fase de escrita do código.

Compatibilidade de plataforma

Os nomes e caminhos das bibliotecas dinâmicas diferem entre plataformas: libxyz.so no Android/Linux, libxyz.dylib no iOS/macOS, xyz.dll no Windows. Para bibliotecas multiplataforma, é usada compilação condicional através de dart:io (Platform.isAndroid, Platform.isIOS) ou abstrações como package:ffi. Recomenda-se criar um método fábrica que retorne a biblioteca correta para a plataforma atual.

Gerenciamento de memória

O FFI não gerencia a memória do lado C. Se uma função C aloca memória via malloc, ela deve ser liberada via free, caso contrário ocorrerá um vazamento. O Dart não possui coletor de lixo para memória C. Recomendação: sempre liberar a memória no mesmo método onde foi alocada, ou usar Arena para liberação em grupo.

Desempenho e threads

As chamadas FFI são executadas na mesma thread que o código Dart. Operações síncronas longas (mais de 10 ms) bloqueiam a thread da UI e causam queda de quadros. Para operações longas, deve-se chamar a função C em um Isolate ou garantir que a função C execute o trabalho em uma thread em segundo plano e notifique o Dart através de Port ou callback.

dart
// FFI em isolate para operações longas
import 'dart:isolate'

Future<String> computeHash(String input) async {
    final port = ReceivePort()
    await Isolate.spawn((SendPort sendPort) {
        final result = sha256(input) // Chamada FFI
        sendPort.send(result)
    }, port.sendPort)

    return await port.first as String
}

Mover as chamadas FFI para um isolate garante que a thread da UI não seja bloqueada. No entanto, deve-se considerar que a transferência de grandes quantidades de dados entre isolates requer cópia de memória. Para buffers grandes (>10 MB), é preferível usar SharedMemory ou arquivos mapeados em memória.

Perguntas frequentes

Como o FFI difere do Method Channel?

O FFI chama funções C diretamente, de forma síncrona e sem serialização — latência de 0,01–0,1 µs. O Method Channel funciona de forma assíncrona via serialização JSON com latência de 0,5–5 ms. FFI é adequado para operações de alto desempenho, Method Channel para chamadas simples de API de plataforma.

Pode-se chamar funções C++ via FFI?

Diretamente — não, o dart:ffi suporta apenas funções C. Para chamar C++, é necessário criar um wrapper C com extern “C” (pontos de entrada que são exportados como símbolos C). Classes C++ exigem uma camada adicional que traduz chamadas de método em funções C.

Como lidar com erros em funções C?

O FFI não suporta exceções — se uma função C retornar um código de erro, ele deve ser verificado manualmente. Recomenda-se envolver as chamadas FFI em try-catch no Dart e verificar os códigos de retorno das funções C. Erros críticos (segfault) não podem ser capturados.

Quais bibliotecas não podem ser usadas via FFI?

O FFI não funciona com bibliotecas que exigem inicialização complexa de Java (JNI) ou Objective-C (Message Dispatch). Por exemplo, UIKit e Android Views não são acessíveis via FFI. A limitação se deve ao fato de que o FFI opera no nível da ABI C, enquanto essas APIs exigem runtimes específicos.

É necessário compilar bibliotecas C para cada plataforma?

Sim, as bibliotecas C são compiladas separadamente para cada plataforma alvo. Para Android, o .so é construído para diferentes ABIs (armeabi-v7a, arm64-v8a, x86_64). Para iOS — um .dylib universal (arm64). Para Windows — .dll. Flutter empacota automaticamente a versão correta da biblioteca durante a compilação.

Resumo

  • FFI (Foreign Function Interface) — mecanismo para chamar funções C diretamente do Dart via dart:ffi
  • Desempenho das chamadas FFI é 50–500 vezes maior que pelo Method Channel
  • Arquitetura inclui carregamento de biblioteca, declaração de assinaturas e chamada de funções
  • Tipos de dados dart:ffi suporta Int32, Float, Double, Pointer, Struct e outros tipos C
  • Memória do lado C é gerenciada manualmente via calloc/free ou Arena
  • Limitações do FFI: sem verificação de tipos em runtime, sem suporte direto a C++, bloqueia a thread da UI
  • Use FFI para integração de alto desempenho com bibliotecas nativas no Flutter

Vamos desenvolver um aplicativo móvel chave na mão

A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.

Discutir o projeto

Leia também