PHP FFI: Usando una Biblioteca Multiplataforma Kotlin – Parte 4

Con Stéphane Péchard https://www.linkedin.com/in/stephanepechard/, experto en Android y gurú de KMP, nos preguntamos si sería factible desarrollar un algoritmo bajo KMP y ejecutarlo directamente en PHP. 
Las especificaciones eran simples: el desarrollador de Android (Stéphane) me proporciona un archivo de cabecera, un archivo .so y yo me encargo de él. 
Con el archivo de cabecera, el contrato, deberíamos poder idear algo... Así que logramos hacer nuestra llamada a la función. Pero para ser honesto, todavía luché bastante antes de llegar allí.

Este es el archivo de cabecera proporcionado:

#ifndef KONAN_LIBALGOBSCURE_H
#define KONAN_LIBALGOBSCURE_H
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
typedef bool            libalgobscure_KBoolean;
#else
typedef _Bool           libalgobscure_KBoolean;
#endif
typedef unsigned short     libalgobscure_KChar;
typedef signed char        libalgobscure_KByte;
typedef short              libalgobscure_KShort;
typedef int                libalgobscure_KInt;
typedef long long          libalgobscure_KLong;
typedef unsigned char      libalgobscure_KUByte;
typedef unsigned short     libalgobscure_KUShort;
typedef unsigned int       libalgobscure_KUInt;
typedef unsigned long long libalgobscure_KULong;
typedef float              libalgobscure_KFloat;
typedef double             libalgobscure_KDouble;
typedef float __attribute__ ((__vector_size__ (16))) libalgobscure_KVector128;
typedef void*              libalgobscure_KNativePtr;
struct libalgobscure_KType;
typedef struct libalgobscure_KType libalgobscure_KType;

typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Byte;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Short;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Int;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Long;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Float;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Double;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Char;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Boolean;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Unit;


typedef struct {
  /* Service functions. */
  void (*DisposeStablePointer)(libalgobscure_KNativePtr ptr);
  void (*DisposeString)(const char* string);
  libalgobscure_KBoolean (*IsInstance)(libalgobscure_KNativePtr ref, const libalgobscure_KType* type);
  libalgobscure_kref_kotlin_Byte (*createNullableByte)(libalgobscure_KByte);
  libalgobscure_kref_kotlin_Short (*createNullableShort)(libalgobscure_KShort);
  libalgobscure_kref_kotlin_Int (*createNullableInt)(libalgobscure_KInt);
  libalgobscure_kref_kotlin_Long (*createNullableLong)(libalgobscure_KLong);
  libalgobscure_kref_kotlin_Float (*createNullableFloat)(libalgobscure_KFloat);
  libalgobscure_kref_kotlin_Double (*createNullableDouble)(libalgobscure_KDouble);
  libalgobscure_kref_kotlin_Char (*createNullableChar)(libalgobscure_KChar);
  libalgobscure_kref_kotlin_Boolean (*createNullableBoolean)(libalgobscure_KBoolean);
  libalgobscure_kref_kotlin_Unit (*createNullableUnit)(void);

  /* User functions. */
  struct {
    struct {
      libalgobscure_KLong (*factoriel)(libalgobscure_KLong n);
      libalgobscure_KLong (*fibonacci)(libalgobscure_KLong n);
      void (*main)();
    } root;
  } kotlin;
} libalgobscure_ExportedSymbols;
extern libalgobscure_ExportedSymbols* libalgobscure_symbols(void);
#ifdef __cplusplus
}  /* extern "C" */
#endif
#endif  /* KONAN_LIBALGOBSCURE_H */

Lamentablemente, la cabecera hizo que PHP se colgara 🙂 Al limpiarlo un poco llegamos a este contenido:

#define FFI_LIB "./linuxX64/releaseShared/libalgobscure.so"

typedef bool            libalgobscure_KBoolean;
typedef unsigned short     libalgobscure_KChar;
typedef signed char        libalgobscure_KByte;
typedef short              libalgobscure_KShort;
typedef int                libalgobscure_KInt;
typedef long long          libalgobscure_KLong;
typedef unsigned char      libalgobscure_KUByte;
typedef unsigned short     libalgobscure_KUShort;
typedef unsigned int       libalgobscure_KUInt;
typedef unsigned long long libalgobscure_KULong;
typedef float              libalgobscure_KFloat;
typedef double             libalgobscure_KDouble;
typedef float   libalgobscure_KVector128;
typedef void*              libalgobscure_KNativePtr;
struct libalgobscure_KType;
typedef struct libalgobscure_KType libalgobscure_KType;

typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Byte;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Short;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Int;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Long;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Float;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Double;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Char;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Boolean;
typedef struct {
  libalgobscure_KNativePtr pinned;
} libalgobscure_kref_kotlin_Unit;


typedef struct {
  /* Service functions. */
  void (*DisposeStablePointer)(libalgobscure_KNativePtr ptr);
  void (*DisposeString)(const char* string);
  libalgobscure_KBoolean (*IsInstance)(libalgobscure_KNativePtr ref, const libalgobscure_KType* type);
  libalgobscure_kref_kotlin_Byte (*createNullableByte)(libalgobscure_KByte);
  libalgobscure_kref_kotlin_Short (*createNullableShort)(libalgobscure_KShort);
  libalgobscure_kref_kotlin_Int (*createNullableInt)(libalgobscure_KInt);
  libalgobscure_kref_kotlin_Long (*createNullableLong)(libalgobscure_KLong);
  libalgobscure_kref_kotlin_Float (*createNullableFloat)(libalgobscure_KFloat);
  libalgobscure_kref_kotlin_Double (*createNullableDouble)(libalgobscure_KDouble);
  libalgobscure_kref_kotlin_Char (*createNullableChar)(libalgobscure_KChar);
  libalgobscure_kref_kotlin_Boolean (*createNullableBoolean)(libalgobscure_KBoolean);
  libalgobscure_kref_kotlin_Unit (*createNullableUnit)(void);

  /* User functions. */
  struct {
    struct {
      libalgobscure_KLong (*factoriel)(libalgobscure_KLong n);
      libalgobscure_KLong (*fibonacci)(libalgobscure_KLong n);
      void (*main)();
    } root;
  } kotlin;
} libalgobscure_ExportedSymbols;

extern libalgobscure_ExportedSymbols* libalgobscure_symbols(void);

Para entender mejor los argumentos de la cabecera que no son compatibles, aquí está la diferencia. No es tan complicado hacer la limpieza después de todo.

image
image-1

En esencia, KMP nos exporta una función que proporciona una estructura -> dentro de una estructura -> dentro de una estructura con dos punteros a funciones: factorial y fibonacci.

El truco para acceder a las funciones es navegar a través de las estructuras:

$ffi = FFI::load(__DIR__ . '/kmp.h');
// On vient chercher la methode libalgobscure_symbols()
// qui nous renvoie une structure libalgobscure_ExportedSymbols
$libalgobscure = $ffi->libalgobscure_symbols();
// ensuite on remonte le fil et on récupère l'adresse de nos fonction : 
$factoriel = FFI::addr($libalgobscure->kotlin->root->factoriel)[0];
$fibonacci = FFI::addr($libalgobscure->kotlin->root->fibonacci)[0];

// Et on peut exécuter directement notre fonction anonyme : 
$factoriel(5)

Aquí está el código completo:

#!/usr/bin/php8.1
<?php
opcache_reset();
$ffi = FFI::load(__DIR__ . '/kmp.h');
$libalgobscure = $ffi->libalgobscure_symbols();
$factoriel = FFI::addr($libalgobscure->kotlin->root->factoriel)[0];
echo "factoriel 5 = ";
var_dump($factoriel(5));

echo "factoriel 12 = ";
var_dump($factoriel(12));

$fibonacci = FFI::addr($libalgobscure->kotlin->root->fibonacci)[0];
echo "fibonacci 6 = ";

var_dump($fibonacci(6));

echo "fibonacci 12 = ";
var_dump($fibonacci(12));

var_dump($libalgobscure);

Y la salida de la ejecución:

factoriel 5 = int(120)
factoriel 12 = int(479001600)
fibonacci 6 = int(8)
fibonacci 12 = int(144)
object(FFI\CData:struct <anonymous>*)#2 (1) {
  [0]=>
  object(FFI\CData:struct <anonymous>)#5 (13) {
    ["DisposeStablePointer"]=>
    object(FFI\CData:void(*)())#6 (1) {
      [0]=>
      object(FFI\CData:void())#19 (0) {
      }
    }
    ["DisposeString"]=>
    object(FFI\CData:void(*)())#7 (1) {
      [0]=>
      object(FFI\CData:void())#19 (0) {
      }
    }
    ["IsInstance"]=>
    object(FFI\CData:bool(*)())#8 (1) {
      [0]=>
      object(FFI\CData:bool())#19 (0) {
      }
    }
    ["createNullableByte"]=>
    object(FFI\CData:struct <anonymous>(*)())#9 (1) {
      [0]=>
      object(FFI\CData:struct <anonymous>())#19 (0) {
      }
    }
    ["createNullableShort"]=>
    object(FFI\CData:struct <anonymous>(*)())#10 (1) {
      [0]=>
      object(FFI\CData:struct <anonymous>())#19 (0) {
      }
    }
    ["createNullableInt"]=>
    object(FFI\CData:struct <anonymous>(*)())#11 (1) {
      [0]=>
      object(FFI\CData:struct <anonymous>())#19 (0) {
      }
    }
    ["createNullableLong"]=>
    object(FFI\CData:struct <anonymous>(*)())#12 (1) {
      [0]=>
      object(FFI\CData:struct <anonymous>())#19 (0) {
      }
    }
    ["createNullableFloat"]=>
    object(FFI\CData:struct <anonymous>(*)())#13 (1) {
      [0]=>
      object(FFI\CData:struct <anonymous>())#19 (0) {
      }
    }
    ["createNullableDouble"]=>
    object(FFI\CData:struct <anonymous>(*)())#14 (1) {
      [0]=>
      object(FFI\CData:struct <anonymous>())#19 (0) {
      }
    }
    ["createNullableChar"]=>
    object(FFI\CData:struct <anonymous>(*)())#15 (1) {
      [0]=>
      object(FFI\CData:struct <anonymous>())#19 (0) {
      }
    }
    ["createNullableBoolean"]=>
    object(FFI\CData:struct <anonymous>(*)())#16 (1) {
      [0]=>
      object(FFI\CData:struct <anonymous>())#19 (0) {
      }
    }
    ["createNullableUnit"]=>
    object(FFI\CData:struct <anonymous>(*)())#17 (1) {
      [0]=>
      object(FFI\CData:struct <anonymous>())#19 (0) {
      }
    }
    ["kotlin"]=>
    object(FFI\CData:struct <anonymous>)#18 (1) {
      ["root"]=>
      object(FFI\CData:struct <anonymous>)#19 (3) {
        ["factoriel"]=>
        object(FFI\CData:int64_t(*)())#20 (1) {
          [0]=>
          object(FFI\CData:int64_t())#23 (0) {
          }
        }
        ["fibonacci"]=>
        object(FFI\CData:int64_t(*)())#21 (1) {
          [0]=>
          object(FFI\CData:int64_t())#23 (0) {
          }
        }
        ["main"]=>
        object(FFI\CData:void(*)())#22 (1) {
          [0]=>
          object(FFI\CData:void())#23 (0) {
          }
        }
      }
    }
  }
}

Así que está claro que es factible realizar un algoritmo muy específico con KMP y recuperarlo en PHP. Deberíamos comparar basándonos en el tipo de operaciones realizadas, pero si evita semanas de desarrollo para portar la librería a PHP, es una solución que vale la pena considerar.
Comparativamente, no es seguro que un algoritmo en PHP vaya a ser más rápido que una llamada a una librería exportada por Kotlin a través de php-ffi. En cualquier caso, creo que vale la pena explorar esta avenida. Especialmente si has decidido cambiar de un backend en PHP a uno en KMP y quieres hacerlo de manera gradual o simplemente compartir algo de código.

Siguiendo con el objetivo de máxima genialidad, veremos en nuestra próxima parte de nuestro caso de estudio cómo integrar una librería real para realmente sacarle partido.
Observaremos QuickJs de Fabrice Bellard, lo que nos permitirá ejecutar código Javascript directamente desde PHP.
Puede parecer inútil pero les aseguro que he visto integraciones muy interesantes de JS ejecutado por PHP.
En resumen, para avanzar es por aquí:

PHP FFI: Parte 5 (en proceso de redacción)

Gracias a Thomas Bourdin, Cédric Le Jallé, Stéphane Péchard por su ayuda, consejos y correcciones.