leanmind logo leanmind text logo

Blog

Refactorización Avanzada

Dominar el refactoring productivo para maximizar el retorno de la inversión.

Explorando la integración de librerías nativas en Flutter mediante FFI

Por Daniel Alvarez

Introducción

A la hora de hacer aplicaciones multiplataforma, una de las opciones más populares actualmente es Flutter. Es un framework versátil, ya que no sólo se limita a aplicaciones móviles, sino también para escritorio y web.

Sin embargo, es posible que en algún momento necesitemos integrar en nuestro código librerías de terceros escritas en lenguajes como C/C++, o en su defecto, que compilen a módulos de C como Go o Rust.

En estos casos, se emplean mecanismos denominados “foreign function interface” (FFI), los cuales nos permiten cumplir las acciones mencionadas previamente.

En el caso de Flutter, existe un paquete de Dart llamado FFI, que proporciona una serie de utilidades para interactuar con código nativo. En este artículo veremos un ejemplo de cómo implementar una librería nativa de C, dentro de un proyecto de Dart.

Caso práctico

FFI

Imaginemos que tenemos el siguiente código en C:

1
2
3
4
5
// math_operations.c

int multiply(int first, int second) {
    return first* second;
}

Para poder emplear esta función ‘multiply’ desde nuestro proyecto de Dart, debemos de compilar el fichero generando una biblioteca compartida:

1
$ gcc -shared -o sharedLib.so math_operations.c

Ahora nos dirigimos al proyecto de Dart/Flutter, e instalamos la dependencia de FFI:

1
$ dart pub add ffi
1
$ flutter pub add ffi

A continuación, creamos un fichero e incluimos las dependencias necesarias para poder empezar a llamar a las funciones nativas:

1
2
3
4
5
6
// ffi_example.dart

import 'dart:ffi';

final sharedLibPath = "path/to/sharedLib.so";
final DynamicLibrary _dynamicLib = DynamicLibrary.open(sharedLibPath);

Hemos hecho lo siguiente:

Con esto, podemos proseguir con el ejemplo y llamar a la función “multiply”, de nuestra librería en C:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// ffi_example.dart

import 'dart:ffi';

final path = "path/to/sharedLib.so";
final DynamicLibrary _dynamicLib = DynamicLibrary.open(path);

void main() {
  final int Function(int, int) multiplyFFI = _dynamicLib 
      .lookup<NativeFunction<Int32 Function(Int32, Int32)>>("multiply")
      .asFunction();

 final firstDigit = 10;
 final secondDigit = 2;

  final multiplyResult = multiplyFFI(firstDigit, secondDigit);

}

En esta parte del código, después de haber cargado la biblioteca dinámica compartida (_dynamicLib), se procede a:

1
2
3
4
final firstDigit = 10;
final secondDigit = 2;

final multiplyResult = multiplyFFI(firstDigit, secondDigit);

FFIGen

Con esto, ya habríamos cumplido nuestro propósito. Sin embargo, aunque a primera vista parezca simple porque sólo hemos trabajado con un método, no ocurre lo mismo cuando queremos emplear un gran número de ellos.

Realizar este proceso repetidamente, nos llevaría una larga cantidad de tiempo, y en algunos casos sería hasta inviable. Es por ello, que para solventar este problema, podemos emplear otro paquete denominado FFIGen.

Con esta herramienta, todas las conversiones se pueden realizar de manera automática. Imaginemos que tenemos los siguientes ficheros en C:

1
2
3
4
5
6
7
// math_operations.h

#include <stdio.h>
#include <stdlib.h>

int multiply(int first, int second);
int add(int first, int second);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// math_operations.c

#include "math_operations.h"

int multiply(int first, int second) {
    return first * second;
}

int add(int first, int second) {
    return first + second;
}

En este caso contamos con dos operaciones “add” y “multiply”. Las definiciones de dichos métodos que queremos usar, se encuentran en math_operations.h. Es decir, este será nuestro punto de entrada, y así lo tenemos que indicar dentro del pubspec.yaml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
...
ffigen:
  # Fichero donde se van a generar todos los puentes entre C y Dart
  output: 'lib/generated_bindings.dart'
  # Puntos de entrada
  headers:
    entry-points:
      - 'native_code/math_operations/math_operations.h'
    include-directives:
      - 'native_code/math_operations/math_operations.h'
...

Si contáramos con varios puntos de entrada, se podrían incluir sin ningún problema. Además, la configuración de FFIGen no tiene por qué ir incluida en el pubspec.yaml, puede ir aparte.

Cabe recalcar que hemos modificado el código de C, por lo que debemos de volver a generar la biblioteca compartida. Cuando nuestro código va escalando de tamaño, es mejor emplear CMake para estas generaciones, en el proyecto de este artículo puedes encontrar un ejemplo de uso para este caso.

Ahora procedemos a instalar FFIGen. En primer lugar, necesitaremos instalar algunas dependencias en función del sistema operativo. Con esto realizado, escribimos lo siguiente en la terminal:

1
$ dart pub add ffigen
1
$ flutter pub add ffigen

Ya con todo configurado, generamos las funciones que servirán de enlace con nuestro código de C:

1
$ dart pub run ffigen
1
$ flutter pub run ffigen

La salida de esta ejecución nos generará un fichero con el nombre que hayamos especificado (generated_bindings.dart en mi caso). Este fichero debemos importarlo en nuestro código de Dart para poder emplearlo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import 'dart:ffi';
// Importamos las generaciones
import 'package:ffi_example/generated_bindings.dart';

final path = "path/to/sharedLib.so";
final DynamicLibrary _dynamicLib = DynamicLibrary.open(path);

void main() {
  final NativeLibrary _bindings = NativeLibrary(_dynamicLib );

  var firstDigit = 10;
  var secondDigit = 2;

  final multiplyResult = _bindings.multiply(firstDigit, secondDigit);
  final addResult = _bindings.add(firstDigit, secondDigit);

  print("Multiply with ffigen: $firstDigit * $secondDigit = $multiplyResult");
  print("Add with ffigen: $firstDigit + $secondDigit = $addResult");
}

Cómo se puede apreciar, solo tenemos que hacer una llamada, para crearnos un objeto con todas las funciones necesarias:

1
final NativeLibrary _bindings = NativeLibrary(_dynamicLib );

Ya con esto, mediante _bindings, podemos acceder a las funciones que realizamos en C previamente:

1
2
final multiplyResult = _bindings.multiply(firstDigit, secondDigit);
final addResult = _bindings.add(firstDigit, secondDigit);

Alternativamente, si solo hubiéramos empleado FFI, el código hubiera sido de la siguiente manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import 'dart:ffi';

final path = "path/to/sharedLib.so";
final DynamicLibrary _dynamicLib = DynamicLibrary.open(path);

void main() {
  final int Function(int, int) multiplyFFI = _dynamicLib 
      .lookup<NativeFunction<Int32 Function(Int32, Int32)>>("multiply")
      .asFunction();

  final int Function(int, int) addFFI = _dynamicLib 
      .lookup<NativeFunction<Int32 Function(Int32, Int32)>>("add")
      .asFunction();

  var firstDigit = 10;
  var secondDigit = 2;

  final multiplyResult = multiplyFFI(firstDigit, secondDigit);
  final addResult = addFFI(firstDigit, secondDigit);

  print("Multiply with ffi: $firstDigit * $secondDigit = $multiplyResult");
  print("Add with ffigen: $firstDigit + $secondDigit = $addResult");

}

Conclusiones

Se percibe claramente que la versión usando FFIGen, es más corta y sencilla de entender, que la versión empleando únicamente FFI. Además, en este caso estamos trabajando solo con datos sencillos, pero para datos más complejos, es preferible que las vinculaciones se realicen de manera automática.

Con esto, ya tienes todo lo necesario para poder emplear tu librería de C dentro de tus proyectos de Dart/Flutter. Próximamente realizaré otro artículo de cómo serían las interacciones con tipos más complejos (struct, punteros…).

¡Hasta la próxima!🫡

Referencias:

Publicado el 10/11/2023 por
Daniel image

Daniel Alvarez

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers