DEV Community

Satoshi Nishimura
Satoshi Nishimura

Posted on

Tried FFI on PHP 7.4

Getting PHP 7.4 and check the launch

I tried FFI on PHP 7.4.

First, get php sources and compile. Needs configure with --with-ffi option.

git clone https://github.com/php/php-src.git
cd php-src
git checkout PHP-7.4
./configure --with-ffi
make
Enter fullscreen mode Exit fullscreen mode

Starting server with FFI needs ffi.enable=1 definition.

php-src/sapi/cli/php -d ffi.enable=1 -S localhost:8000
Enter fullscreen mode Exit fullscreen mode

When the server is up, first copy the example here https://php.net/ffi.examples-basic and make sure there are no errors.

Make shared library by C language for FFI

After launch PHP 7.4, make shared library by c language.

#include <string.h>

#define BUF_SIZE 1024

const char * sample(const char *data, int mod)
{
  char buf[BUF_SIZE];
  const char * ret = buf;
  int i;

  for (i = 0; i <= strlen(data) && i < BUF_SIZE; i++){
    if (data[i] >= 97 && data[i] <= 122 && i % mod == 0)
      buf[i] = data[i] - 32;
    else
      buf[i] = data[i];
  }

  return ret;
}
Enter fullscreen mode Exit fullscreen mode

This is the simple program that getting string and int arguments and converting to upper case.

Compile the program to shared library, and load from php program.

gcc -shared -o libsample.so sample.c
Enter fullscreen mode Exit fullscreen mode
<?php
header('Content-Type: text/plain');

$ffi = FFI::cdef("

const char * sample(const char *data, int mod);

", __DIR__ . '/libsample.so');

var_dump($ffi);
var_dump($ffi->sample("sample test test", 3));
var_dump($ffi->sample("sample test test", 4));
var_dump($ffi->sample("sample test test", 1));

/* output:

object(FFI)#1 (0) {
}
string(16) "SamPle teSt TesT"
string(16) "SampLe tEst Test"
string(16) "SAMPLE TEST TEST"
*/
Enter fullscreen mode Exit fullscreen mode

It worked well!

PHP's string type variables can be received in C as const char * type variables.
It seems to be required const.
PHP int type variable is int type variable also in C.

Use a callback function

Currently, the callbacks in the PHP manual are described by zend_write, but I think it's not so clear.
https://php.net/ffi.examples-callback

So, I wrote a original callback program to make it easy to understand.

#include <string.h>

#define BUF_SIZE 1024

typedef int (*callback_t)(int);

const char * sample(const char *data, callback_t callback)
{
  char buf[BUF_SIZE];
  const char * ret = buf;
  int i;

  for (i = 0; i <= strlen(data) && i < BUF_SIZE; i++){
    if (data[i] >= 97 && data[i] <= 122 && callback(i))
      buf[i] = data[i] - 32;
    else
      buf[i] = data[i];
  }

  return ret;
}
Enter fullscreen mode Exit fullscreen mode

Tried the behavior that PHP closure is mapped to C function pointer.
This program uses a simple callback function that receives int argument and returns int.

Here is an example using a structure, callback and reference:

<?php
header('Content-Type: text/plain');

$ffi = FFI::cdef("

typedef int (*callback_t)(int);
const char * sample(const char *data, callback_t callback);

", __DIR__ . '/libsample.so');

var_dump($ffi);
var_dump($ffi->sample("sample test test", fn($i) => $i % 3));
var_dump($ffi->sample("sample test test", fn($i) => $i % 4));
var_dump($ffi->sample("sample test test", fn($_) => true));

/* output:

object(FFI)#1 (0) {
}
string(16) "sAMpLE TEsT tESt"
string(16) "sAMPlE TeST tEST"
string(16) "SAMPLE TEST TEST"
*/
Enter fullscreen mode Exit fullscreen mode

Now that the Arrow Functions function has been merged, can use it.

Callback function cannot return string type. When try it, raised error that Uncaught FFI\Exception: FFI internal error. Unsupported return type.

Use struct of C language

Structures can be used by calling FFI :: cdef with a structure declaration.

#include <string.h>

#define BUF_SIZE 1024

typedef int (*callback_t)(int);
struct cbdata {
  callback_t f;
};

const char * sample(const char *data, struct cbdata *cbdata)
{
  char buf[BUF_SIZE];
  const char * ret = buf;
  int i;

  for (i = 0; i <= strlen(data) && i < BUF_SIZE; i++){
    if (data[i] >= 97 && data[i] <= 122 && cbdata->f(i))
      buf[i] = data[i] - 32;
    else
      buf[i] = data[i];
  }

  return ret;
}
Enter fullscreen mode Exit fullscreen mode
<?php
header('Content-Type: text/plain');

$ffi = FFI::cdef("

typedef int (*callback_t)(int);
struct cbdata {
  callback_t f;
};

const char * sample(const char *data, struct cbdata *cbdata);

", __DIR__ . '/libsample.so');

$cbdata = $ffi->new('struct cbdata');
$n = 3;
$cbdata->f = function($i) use(&$n){
    return $i % $n === 0;
};

$pcbdata = FFI::addr($cbdata);

var_dump($ffi->sample("sample test test", $pcbdata));

$n = 4;
var_dump($ffi->sample("sample test test", $pcbdata));

$n = 1;
var_dump($ffi->sample("sample test test", $pcbdata));

/* output:

string(16) "SamPle teSt TesT"
string(16) "SampLe tEst Test"
string(16) "SAMPLE TEST TEST"
 */
Enter fullscreen mode Exit fullscreen mode

From PHP references can be used from callbacks from C functions, it seems possible to execute C functions while changing values at any time.

Use another language

If I want to execute numerical calculation at high speed, it may be better in C language, but I do not want to do string processing in C.
If other languages have FFI, they can communicate through FFI.

{-# LANGUAGE ForeignFunctionInterface #-}

#include "template_operations.h"

module HaskellPhp where

import Foreign
import Foreign.C.String
import Foreign.C.Types

data Tops = Tops { assign::FunPtr (CString -> CString -> IO CInt) }

foreign export ccall hsParse :: CString -> Ptr Tops -> IO CString
foreign import ccall "dynamic" mkFun :: FunPtr (CString -> CString -> IO CInt)
                                     -> (CString -> CString -> IO CInt)
foreign import ccall "assign_value_get" value_get :: IO CString



instance Storable Tops where
    sizeOf _ = #size CTOPS
    alignment _ = #alignment CTOPS
    peek ptr = do
      assign' <- (#peek CTOPS, assign) ptr
      return Tops { assign=assign' }
    poke ptr (Tops assign') = do
                           (#poke CTOPS, assign) ptr assign'


--
-- Function to parse some string
--
hsParse :: CString -> Ptr Tops -> IO CString
hsParse cs cops = do
  ops <- peek cops
  let f = mkFun $ assign ops
  a <- newCString "foo"
  b <- newCString "bar"
  r <-  f a b
  cstr <- value_get
  s <- peekCString cstr
  newCString $ "hsParse finish: [" ++ show r ++ "]" ++ "[" ++ s ++ "]"
Enter fullscreen mode Exit fullscreen mode
typedef struct template_operations
{
  size_t (*assign)(const char * key, const char * value);
} CTOPS;
Enter fullscreen mode Exit fullscreen mode
#include <stdio.h>
#include <HsFFI.h>

#ifdef __GLASGOW_HASKELL__
#include "Callback_stub.h"
#endif

#include "template_operations.h"

int prepare() {
  hs_init(0,0);
}
int finish() {
  hs_exit();
}

static const char * assign_string = "";
void assign_value_set(const char *value)
{
  assign_string = value;
}
const char *assign_value_get()
{
  return assign_string;
}

const char* parse(char *data, struct template_operations *tops)
{
  return hsParse(data, tops);
}
Enter fullscreen mode Exit fullscreen mode
<?php
header('Content-Type: text/plain');
$ffi = FFI::cdef('
int prepare();
int finish();

typedef struct template_operations
{
  size_t (*assign)(const char * key, const char * value);
} CTOPS;

const char* parse(char *data, struct template_operations *tops);
void assign_value_set(const char *value);
const char * assign_value_get();

', __DIR__ . '/libcallback.so');

$ffi->prepare();

$tops = $ffi->new('struct template_operations');
$tops->assign = function($key, $value) use ($ffi){
    $value = "$key = $value";
    $ffi->assign_value_set($value);

    // Notice:
    // return string value is not supported
    return strlen($value);
};

$data = '<div>$foo</div>';
$ret = $ffi->parse($data, FFI::addr($tops));

var_dump($ffi->assign_value_get());
var_dump($ret);

$ffi->finish();

/* Output

string(9) "foo = bar"
string(30) "hsParse finish: [9][foo = bar]"

 */
Enter fullscreen mode Exit fullscreen mode

Here is an example using Haskell.
It assumes that Haskell will create a templating engine from which to interact with PHP.
Use hsc2hs to use ffi in haskell.

Since the return value can not be a string type, I made it a global variable of C language.
PHP is not originally thread safe, there is no problem, isn't it? maybe...

If it write up to this point, it can call Haskell directly without using C language.

{-# NOINLINE return_value #-}
return_value :: IORef CString
return_value = unsafePerformIO $ newCString "" >>= newIORef

return_value_set :: CString -> IO ()
return_value_set a = writeIORef return_value a

return_value_get :: IO CString
return_value_get = readIORef return_value
Enter fullscreen mode Exit fullscreen mode
<?php
header('Content-Type: text/plain');
$ffi = FFI::cdef('
int hs_init(int *, char **[]);
int hs_exit();

typedef struct template_operations
{
  size_t (*assign)(const char * key, const char * value);
} CTOPS;

const char* parse(char *data, struct template_operations *tops);
void return_value_set(const char *value);
const char * return_value_get();

', __DIR__ . '/libcallback.so');

$argc = FFI::new('int');
$argv = FFI::new('char[0]');
$pargv = FFI::addr($argv);
$ffi->hs_init(FFI::addr($argc), FFI::addr($pargv));

$tops = $ffi->new('struct template_operations');
$tops->assign = function($key, $value) use ($ffi){
    $value = "$key = $value";
    $ffi->return_value_set($value);

    // Notice:
    // return string value is not supported
    return strlen($value);
};

$data = '<div>$foo</div>';
$ret = $ffi->parse($data, FFI::addr($tops));

var_dump($ffi->return_value_get());
var_dump($ret);


$ffi->hs_exit();


/* Output

string(9) "foo = bar"
string(30) "hsParse finish: [9][foo = bar]"

 */
Enter fullscreen mode Exit fullscreen mode

Conclusion

The sample programs I wrote so far are located here:

PHP FFI Samples

2019/05/18

Compile PHP 7.4

mkdir work
cd work

git clone https://github.com/php/php-src.git
cd php-src
git checkout PHP-7.4
./configure --with-ffi
make

cd ..
Enter fullscreen mode Exit fullscreen mode

Checkout Samples

git clone https://github.com/nishimura/php_ffi_samples.git
Enter fullscreen mode Exit fullscreen mode

Start Server with FFI

cd php_ffi_samples
../php-src/sapi/cli/php -d ffi.enable=1 -S localhost:8000

# and access to
#   http://localhost:8080/
#   http://localhost:8080/1/
#   ...
Enter fullscreen mode Exit fullscreen mode

Create DSO Module

require: c build tools

cd 2
make

# and access to
#   http://localhost:8080/2/
Enter fullscreen mode Exit fullscreen mode

Other Samples

  • 3: call with closure argument
  • 4: c struct, php reference and callback
  • 5: FFI bridge
  • 6, 7: call haskell functions directly

FFI Bridge to Haskell

require: haskell build tools (ghc)

sample5

PHP => FFI C => FFI Haskell => Callback C => Callback PHP => Set c variable instead of return value

sample6, sample7

PHP => FFI Haskell Callback PHP => Set haskell IORef instead of return value

By using FFI, PHP is dedicated to form manipulation and DB calls, and logic can be written in any language you like, such as Haskell, Go, Rust!

Top comments (0)