PHP FFI: Utilizzo di una libreria multi-piattaforma Kotlin – Parte 4

Con Stéphane Péchard https://www.linkedin.com/in/stephanepechard/, esperto Android e guru di KMP, ci siamo chiesti se fosse fattibile sviluppare un algoritmo in KMP ed eseguirlo direttamente in PHP. 
Le specifiche erano semplici: lo sviluppatore Android (Stéphane) mi fornisce un file header, un file .so e io mi occupo del resto. 
Con il file header, il contratto, dovremmo essere in grado di escogitare qualcosa... Così siamo riusciti a effettuare la nostra chiamata alla funzione. Ma per essere onesti, ho comunque faticato un bel po' prima di riuscirci.

Ecco il file header fornito:

#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 */

Sfortunatamente, l'header ha fatto crashare PHP 🙂 Ripulendolo un po' siamo arrivati a questo contenuto:

#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);

Per comprendere meglio gli argomenti dell'header che non sono compatibili, ecco il diff. Alla fine non è così complicato fare la pulizia.

image
image-1

Di base, KMP ci esporta una funzione che fornisce una struttura -> all'interno di una struttura -> all'interno di una struttura con due puntatori a funzioni: factorial e fibonacci.

Il trucco per accedere alle funzioni è navigare attraverso le strutture:

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

Ecco il codice 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);

E l'output dell'esecuzione:

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) {
          }
        }
      }
    }
  }
}

Quindi è chiaramente fattibile realizzare un algoritmo molto specifico con KMP e recuperarlo in PHP. Dovremmo compararlo in base al tipo di operazioni svolte, ma se ciò evita settimane di sviluppo per portare la libreria in PHP, è una soluzione da considerare.
A confronto, non è detto che un algoritmo in PHP sia più veloce di una chiamata a una libreria esportata da Kotlin tramite php-ffi. In ogni caso, penso che valga la pena esplorare questa possibilità. Soprattutto se avete deciso di passare da un backend PHP a un backend KMP e desiderate farlo gradualmente o semplicemente condividere un po' di codice.

Continuando con l'obiettivo di massima coolness, nella nostra prossima parte dello studio di caso vedremo come integrare una vera libreria per creare realmente qualcosa di utile.
Esamineremo QuickJs di Fabrice Bellard, che ci permetterà di eseguire codice Javascript direttamente da PHP.
Potrete pensare che sia inutile ma vi assicuro che ho visto integrazioni molto interessanti di JS eseguito da PHP.
In breve, per andare oltre è questa la direzione:

PHP FFI: Parte 5 (attualmente in scrittura)

Grazie a Thomas Bourdin, Cédric Le Jallé, Stéphane Péchard per il loro aiuto, consigli e revisione.