FFI: fundamentos, Foreign Function Interface e integración con C

Autor: IT Sectr Publicado: 2026-06-05 Tiempo de lectura: 10 min

FFI (Foreign Function Interface) es un mecanismo del lenguaje Dart, proporcionado por el paquete dart:ffi, que permite llamar funciones de bibliotecas nativas C directamente, sin capas intermedias en Kotlin, Swift o Java. El desarrollador carga una biblioteca dinámica (.so en Android, .dylib en iOS, .dll en Windows), declara las firmas de las funciones C y las llama como funciones Dart normales. Según la Referencia de Dart API (2025), FFI reduce la sobrecarga de las llamadas entre lenguajes a 0,1 µs, lo que es decenas de veces más rápido que a través de Method Channel.

Puntos clave

  • FFI (Foreign Function Interface) — un mecanismo para llamar funciones C directamente desde Dart
  • dart:ffi proporciona una API para cargar bibliotecas y declarar firmas
  • Rendimiento de las llamadas FFI es 50–100 veces mayor que a través de Method Channel
  • Tipado FFI admite tipos primitivos, estructuras y punteros C
  • Flutter usa FFI para integrarse con bibliotecas nativas: OpenCV, SQLite, FFmpeg

¿Qué es FFI?

FFI (Foreign Function Interface) es un mecanismo que permite a un lenguaje de programación llamar funciones escritas en otros lenguajes. En el contexto de Dart y Flutter, FFI significa la capacidad de llamar funciones de bibliotecas C/C++ directamente desde código Dart, sin necesidad de escribir código específico de plataforma en Java (Android) o Swift/Objective-C (iOS).

El paquete dart:ffi apareció en Dart 2.12 (2021) y desde entonces se ha convertido en una herramienta clave para integrar Flutter con código nativo. Antes de dart:ffi, la única forma de llamar una función C desde Dart era a través de Method Channel, un mecanismo asíncrono que enviaba mensajes mediante serialización JSON entre Dart y la parte nativa. FFI funciona de otra manera: el código Dart accede directamente a la memoria de la biblioteca C, llamando funciones a través de la ABI nativa (Application Binary Interface) sin serialización ni cambio de contexto.

FFI es especialmente demandado en escenarios donde el rendimiento es crítico: procesamiento de imágenes (OpenCV), audio (FFmpeg), criptografía (OpenSSL), aprendizaje automático (TensorFlow Lite) y bases de datos (SQLite). En todos estos casos, Method Channel crea retrasos inaceptables, mientras que FFI proporciona un rendimiento comparable al código C/C++ nativo. La biblioteca dart:ffi también admite la gestión de memoria: asignación, liberación y manipulación de punteros.

FFI vs Method Channel: diferencia fundamental

Method Channel funciona de forma asíncrona: Dart envía un mensaje al código nativo, el código nativo lo procesa y envía el resultado de vuelta. Cada llamada requiere serialización de argumentos en un Map, paso a través de una cola y deserialización. Esto toma 0,5–5 ms por llamada. FFI funciona de forma síncrona y sin serialización — una llamada a una función C toma 0,01–0,1 µs. Una diferencia de 50–500 veces, que es crítica para operaciones de alta frecuencia.

¿Cómo funciona dart:ffi?

Trabajar con dart:ffi consta de tres etapas: cargar la biblioteca, declarar firmas y llamar funciones. Cada etapa utiliza el tipado estricto de Dart, minimizando errores en tiempo de ejecución.

En la primera etapa, se carga la biblioteca dinámica mediante la clase DynamicLibrary. La biblioteca puede cargarse por nombre (libxyz.so, libxyz.dylib, xyz.dll) o por ruta completa. Dart busca automáticamente la biblioteca en las rutas estándar del sistema. DynamicLibrary proporciona el método lookupFunction, que vincula una función Dart con una función C por nombre de símbolo.

En la segunda etapa, se declara una función Dart con anotaciones de tipo que corresponden a la firma C. Se utilizan tipos especiales de dart:ffi: Int32, Float, Double, Pointer, NativeFunction, Handle y otros. La anotación lookupFunction acepta dos parámetros genéricos: el tipo de función Dart (cómo se verá en Dart) y el tipo de función C nativa (cómo está declarada en C).

En la tercera etapa, la función Dart generada se llama como una función normal. Los argumentos se pasan directamente, el resultado se devuelve inmediatamente. Si la función C modifica la memoria a través de punteros, Dart puede leer estos cambios mediante la clase Pointer. La gestión de memoria del lado C sigue siendo responsabilidad del desarrollador — dart:ffi no gestiona la memoria asignada por malloc en C.

Ejemplo básico: llamar una función C desde Dart

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

// Declaración de función 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
}

En este ejemplo, add es una función C que toma dos ints y devuelve un int. El typedef AddNative describe la firma C con tipos de dart:ffi, mientras que AddDart describe cómo se verá esta función en Dart. lookupFunction las vincula y devuelve una función Dart que se puede llamar como una normal.

Tipos de datos en FFI

dart:ffi proporciona un conjunto de tipos que corresponden a los tipos de C. Cada tipo tiene un tamaño fijo y reglas de conversión entre Dart y C. Comprender la correspondencia de tipos es críticamente importante para el correcto funcionamiento de FFI — un error en el tamaño o el signo del tipo puede provocar el fallo de la aplicación.

Tipo CTipo dart:ffiTipo DartTamaño (bytes)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8 (puntero)
void*Pointer<Void>Pointer8 (puntero)
structPointer<T> (Struct)Pointerdepende de los campos

Para trabajar con cadenas C (char*), dart:ffi usa Pointer<Int8>. La conversión de Dart String a C char* y viceversa se realiza mediante toNativeUtf8 (del paquete ffi) y fromUtf8. Es importante liberar las cadenas C después de su uso mediante calloc.free para evitar fugas de memoria.

Estructuras (Struct)

dart:ffi admite la declaración de estructuras C como clases Dart que heredan de Struct. Los campos de la estructura se declaran con anotaciones @Int32(), @Float(), @Array() y otras. El tamaño y el desplazamiento de los campos se calculan automáticamente según la ABI de la plataforma. Pointer<Point> puede obtenerse de una función C que devuelve un puntero a una estructura, o asignarse en Dart mediante calloc.

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

    @Int32()
    external int y
}

// Llamando a función C que devuelve 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 memoria

La clase Point extiende Struct y declara los campos x e y con anotaciones @Int32(). El código C generado tendrá exactamente la misma disposición de memoria. Pointer.ref proporciona acceso a los campos de la estructura a través de getters y setters.

Ejemplos prácticos de FFI

Veamos un ejemplo más complejo — la integración con una biblioteca C para calcular el hash SHA256. Esta es una tarea típica donde FFI proporciona una ganancia significativa de rendimiento en comparación con Method Channel.

Integración con OpenSSL mediante FFI

La biblioteca OpenSSL proporciona la función SHA256, que calcula el hash de una cadena. A través de dart:ffi, podemos llamarla directamente, sin escribir envoltorios Java o Swift. Este es un ejemplo de cómo FFI permite reutilizar bibliotecas C existentes en Flutter.

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

// Firma: 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
}

En este ejemplo, la función sha256 carga la biblioteca libcrypto.so, encuentra el símbolo SHA256 y lo llama con punteros a los datos de entrada y salida. toNativeUtf8 convierte un Dart String en una cadena C (asigna memoria), y asTypedList permite leer la matriz de bytes del resultado. La memoria se libera después de su uso — este es un paso obligatorio para prevenir fugas.

Asignación y liberación de memoria

El paquete ffi proporciona la función calloc para asignar memoria compatible con C. La memoria asignada debe liberarse mediante calloc.free, de lo contrario se producirá una fuga. Para la gestión automática de memoria, se puede usar la clase Arena del paquete ffi, que libera toda la memoria asignada dentro de ella cuando se llama a arena.release(). Esto es especialmente conveniente para una gran cantidad de asignaciones temporales.

Limitaciones y mejores prácticas

A pesar del poder de FFI, tiene limitaciones que deben considerarse al diseñar la arquitectura de una aplicación Flutter. Las principales limitaciones están relacionadas con la seguridad de tipos, la gestión de memoria y la compatibilidad de plataformas.

Seguridad

FFI no realiza comprobaciones de tipos en tiempo de ejecución. Si una función C espera un puntero pero recibe un número, la aplicación fallará con un error de segmentación. Se recomienda usar FFIgen, una herramienta que genera envoltorios Dart con seguridad de tipos basados en archivos de cabecera C (.h). FFIgen analiza las declaraciones de funciones C y crea código Dart con tipos correctos, eliminando errores en la etapa de escritura del código.

Compatibilidad de plataformas

Los nombres y rutas de las bibliotecas dinámicas difieren entre plataformas: libxyz.so en Android/Linux, libxyz.dylib en iOS/macOS, xyz.dll en Windows. Para bibliotecas multiplataforma, se utiliza compilación condicional mediante dart:io (Platform.isAndroid, Platform.isIOS) o abstracciones como package:ffi. Se recomienda crear un método fábrica que devuelva la biblioteca correcta para la plataforma actual.

Gestión de memoria

FFI no gestiona la memoria del lado C. Si una función C asigna memoria mediante malloc, debe liberarse mediante free, de lo contrario se producirá una fuga. Dart no tiene un recolector de basura para memoria C. Recomendación: liberar siempre la memoria en el mismo método donde se asignó, o usar Arena para la liberación grupal.

Rendimiento y subprocesos

Las llamadas FFI se ejecutan en el mismo subproceso que el código Dart. Las operaciones síncronas largas (más de 10 ms) bloquean el subproceso de la UI y causan pérdida de fotogramas. Para operaciones largas, se debe llamar a la función C en un Isolate o asegurarse de que la función C ejecute el trabajo en un subproceso en segundo plano y notifique a Dart mediante Port o callback.

dart
// FFI en isolate para operaciones largas
import 'dart:isolate'

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

    return await port.first as String
}

Mover las llamadas FFI a un isolate garantiza que el subproceso de la UI no se bloquee. Sin embargo, hay que tener en cuenta que la transferencia de grandes volúmenes de datos entre isolates requiere copia de memoria. Para búferes grandes (>10 MB), es preferible usar SharedMemory o archivos mapeados en memoria.

Preguntas frecuentes

¿En qué se diferencia FFI de Method Channel?

FFI llama funciones C directamente, de forma síncrona y sin serialización — latencia de 0,01–0,1 µs. Method Channel funciona de forma asíncrona mediante serialización JSON con una latencia de 0,5–5 ms. FFI es adecuado para operaciones de alto rendimiento, Method Channel para llamadas simples a API de plataforma.

¿Se pueden llamar funciones C++ a través de FFI?

Directamente — no, dart:ffi solo admite funciones C. Para llamar C++, es necesario crear un envoltorio C con extern “C” (puntos de entrada que se exportan como símbolos C). Las clases C++ requieren una capa adicional que traduzca las llamadas a métodos en funciones C.

¿Cómo manejar errores en funciones C?

FFI no admite excepciones — si una función C devuelve un código de error, debe verificarse manualmente. Se recomienda envolver las llamadas FFI en try-catch en Dart y verificar los códigos de retorno de las funciones C. Los errores críticos (segfault) no se pueden capturar.

¿Qué bibliotecas no se pueden usar a través de FFI?

FFI no funciona con bibliotecas que requieren una inicialización compleja de Java (JNI) u Objective-C (Message Dispatch). Por ejemplo, UIKit y Android Views no son accesibles a través de FFI. La limitación se debe a que FFI funciona al nivel de la ABI de C, mientras que estas API requieren entornos de ejecución específicos.

¿Es necesario compilar las bibliotecas C para cada plataforma?

Sí, las bibliotecas C se compilan por separado para cada plataforma objetivo. Para Android, se construye .so para diferentes ABI (armeabi-v7a, arm64-v8a, x86_64). Para iOS — un .dylib universal (arm64). Para Windows — .dll. Flutter empaqueta automáticamente la versión correcta de la biblioteca durante la compilación.

Resumen

  • FFI (Foreign Function Interface) — un mecanismo para llamar funciones C directamente desde Dart mediante dart:ffi
  • Rendimiento de las llamadas FFI es 50–500 veces mayor que a través de Method Channel
  • Arquitectura incluye carga de biblioteca, declaración de firmas y llamada a funciones
  • Tipos de datos dart:ffi admite Int32, Float, Double, Pointer, Struct y otros tipos C
  • Memoria del lado C se gestiona manualmente mediante calloc/free o Arena
  • Limitaciones de FFI: sin comprobación de tipos en tiempo de ejecución, sin soporte directo de C++, bloquea el subproceso de UI
  • Use FFI para integración de alto rendimiento con bibliotecas nativas en Flutter

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también