PHP FFI: Using a Kotlin Multiplatform Library – Part 4

With Stéphane Péchard https://www.linkedin.com/in/stephanepechard/, Android expert and KMP guru, we wondered if developing an algorithm under KMP and executing it directly in PHP would be feasible. 
The specifications were simple: the Android dev (Stéphane) provides me with a header file, a .so file and I handle it. 
With the header file, the contract, we should be able to come up with something... So we managed to make our function call. But to be honest, I still struggled quite a bit before getting there.

Here is the provided header file:

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

Unfortunately, the header crashed PHP 🙂 By cleaning it up a bit we come to this content:

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

To better understand the arguments of the header that are not compatible, here is the diff. It's not that complicated to do the clean-up after all.

image
image-1

Basically, KMP exports to us a function that provides a structure -> within a structure -> within a structure with two function pointers: factorial and fibonacci.

The trick to accessing the functions is to navigate through the 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)

Here is the complete code:

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

And the execution output:

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

So it is clearly feasible to make a very specific algorithm with KMP and retrieve it in PHP. We should compare based on the type of operations performed, but if it avoids weeks of development to port the library to PHP, it's a solution worth considering.
Comparatively, it's not certain that an algorithm in PHP will be faster than a call to a library exported by Kotlin via php-ffi. In any case, I think it's worth exploring this avenue. Especially if you have decided to switch from a PHP backend to a KMP backend and want to do it gradually or simply share some code.

Still with the goal of ultimate coolness, we'll see in our next part of our case study how to integrate a real library to really make something out of it.
We'll look at QuickJs by Fabrice Bellard, which will allow us to execute Javascript code directly from PHP.
You might think it's useless but I assure you that I've seen very interesting integrations of JS executed by PHP.
In short, to go further it's this way:

PHP FFI: Part 5 (currently being written)

Thanks to Thomas Bourdin, Cédric Le Jallé, Stéphane Péchard for their help, advice and proofreading.