PHP FFI : utilisation d’une lib Kotlin Multiplateforme – partie 4

Avec Stéphane Péchard https://www.linkedin.com/in/stephanepechard/, expert Android et guru KMP, on s’est demandé si développer un algorithme sous KMP et l’exécuter directement en PHP serait réalisable. 
Le cahier des charges était simple : le dev Android (Stéphane donc) me fournit un header file, un .so et je me débrouille. 
Avec le header file, le contrat, à priori on devrait arriver à quelque chose… Alors on a réussi à faire notre appel de fonction. Mais en toute franchise j’ai tout de même pas mal galéré avant d’y arriver.

Voici le fichier header fourni :

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

Mauvaise surprise, le header faisait planter PHP 🙂 En nettoyant un peu on arrive à ce contenu :

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

Pour mieux saisir les arguments du header qui ne sont pas compatibles voici le diff. Ce n’est pas très compliqué de faire le nettoyage finalement.

image
image-1

Grosso modo, KMP nous exporte une fonction qui nous fournit une structure -> dans une structure -> dans une structure avec deux pointeurs sur fonction : factoriel et fibonacci.

L’astuce pour accéder aux fonctions est de remonter dans les structures :

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

Voici le code complet :

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

Et le retour d’exécution :

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

Il est donc clairement réalisable de faire un algo très spécifique avec KMP et de le récupérer dans PHP. Il faudrait comparer sur le type d’opérations réalisées, mais si cela permet d’éviter des semaines de développement pour porter la librairie en PHP, c’est une solution à envisager.
Comparativement, il n’est pas dit qu’un algorithme en PHP soit plus rapide qu’un appel à une lib exportée par kotlin via php-ffi. En tout cas je trouve que ça vaut le coup d’explorer cette voie là. Surtout si vous avez décidé de passer d’un backend PHP à un backend KMP et d’y aller petit à petit ou simplement mutualiser une partie de code.

Toujours dans une optique de coolitude extrême, on va voir dans notre prochaine partie de notre dossier voir comment intégrer une vraie librairie pour en faire vraiment quelque chose.
On va s’intéresser à QuickJs de Fabrice Bellard qui va nous permettre d’exécuter du code Javascript directement depuis PHP.
Vous pourriez penser que cela ne sert à rien mais je vous assure que j’ai vu des intégrations hyper intéressantes de JS exécutées par PHP.
Bref pour aller plus loin c’est par ici :

PHP FFI: Partie 5 (en cours de rédaction)

Merci à Thomas Bourdin, Cédric Le Jallé, Stéphane Péchard pour leur aide, conseils et relecture.