PHP FFI: Creating a Bind with a Library, QuickJS – Part 5

To conclude this mini technical file on PHP's foreign functions interface, I set out to simply connect to a lib. A kind of challenge that could at the same time help me progress on the topic. So I had to come up with a project idea. Something not too small or too simple but also not too big. Failed, I found an idea but this turns out to be much more complex than I imagined. No matter, I still tried. So for our article, I thought I would attempt a more or less successful and more or less complete integration of QuickJS.

Installation

My workspace will be simple. A directory with the source of QuickJS, a directory with C source bindings (which will help us simplify our first version), and a directory for our PHP lib.

Installing QuickJS:

sudo apt-get install gcc-multilib texlive texinfo
git clone https://github.com/bellard/quickjs
cd quickjs
./unicode_download.sh
./release.sh binary  quickjs
make build_doc
gcc -shared -o ../php/src/lib/libquickjs.so .obj/qjsc.o .obj/quickjs.o .obj/libregexp.o .obj/libunicode.o .obj/cutils.o .obj/quickjs-libc.o .obj/libbf.o
cd ..

About my workspace, I created a 'php' folder in which I will initialize a composer architecture. This will provide me with a workspace with everything I need to create my code files as well as unit tests that will confirm that everything is working properly. If you wish, you can refer to the article Create a library with composer.

image-79

Calling QuickJS and executing simple JavaScript code.

During our library's compilation, we created our library but also documentation that will help us better understand how this JavaScript engine works. A quick look at the index and we find ourselves directly in the description of the API which teaches us that the function JS_EVAL() allows us to launch an evaluation of JavaScript code. Great, that's exactly what we were looking for.

And as our first goal we will try to make 2*3=6 ? it will be a good start...

Let's head to the header file to unravel the ball of information we need:

https://github.com/bellard/quickjs/blob/master/quickjs.h

Uh... okay, that thing is as long as a highway...

so at line 781 we find what we're looking for:

JSValue JS_Eval(JSContext *ctx, const char *input, size_t input_len,
                const char *filename, int eval_flags);

What this teaches us is that we need a type that would be JSContext. A quick search for "struct JSContext" points us to line 50. Ok, we'll take the whole block, it should be useful...

typedef struct JSRuntime JSRuntime;
typedef struct JSContext JSContext;
typedef struct JSObject JSObject;
typedef struct JSClass JSClass;
typedef uint32_t JSClassID;
typedef uint32_t JSAtom;

So now, we will look for how to generate this famous JSContext. and there we notice: line 361

JSContext *JS_NewContext(JSRuntime *rt);

Ok... So now we will search for who generates JSRuntime...it's endless this story!

Line 331

JSRuntime *JS_NewRuntime(void);

In the end we notice that the return of the function eval() will be a structure of type JSValue composed of a union and an int.

typedef struct JSValue {
    JSValueUnion u;
    int64_t tag;
} JSValue;

typedef union JSValueUnion {
    int32_t int32;
    double float64;
    void *ptr;
} JSValueUnion;

Ok the contract seems to be as follows:

We create a Runtime, we create a context with our runtime and then we can attempt to pass our addition to js_eval.

We can therefore start to create our first files, the code as well as its associated test.

First remark, I have not found how to specify the header file and the library separately.

You have the choice between

FFI::cdef('VOS DEFINITIONS.h', 'VOTRE_POINT_SO.so');

This forces you to board everything at once, even if it means having definitions of several hundred lines. Personally, I don't like it.

Or use:

FFI::load('VOS DEFINITIONS.h');

With at the front end of your header:

#define FFI_LIB "/path/to/your_lib.so";

This does not seem particularly flexible to me. Well my code may be foul, at this point it doesn't matter. On the other hand it bothers me a little not to have the flexibility to specify what I want to use. So I will recover via a file_get_contents my definitions. We will omit the beauty of the code for now.

Second remark, php::ffi is not capable of working relatively. Also, if you want to pass your .h or your .so with a ./lib/lib.so, it won't work. This is a limitation of the ffi lib, not php. In the interim and in order to move forward I've directly put the path in my file. I add this to my growing todo list when I want to make my code cleaner and more flexible.

So I start to make my first class, a bit of a catch-all I admit. But still, I want to test what I do. And then, I have questions:

How to test \FFI::cdef? You can have an error in your definition, either it is not found, or it is invalid. But you can also have an error during the load of the library itself. I haven't found absolutely any resource on this subject. I deliver to you all the same what I managed to find to satisfy my need for tests:

        try{
            $ffi = new JsEvalService();
            $this->assertInstanceOf(\FFi::class, $ffi->init());
        }catch (\FFI\ParserException $e){
            $this->fail('FFI\ParserException: ' . $e->getMessage());
        }catch(\FFI\Exception $e){
            $this->fail('FFI\Exception: ' . $e->getMessage());
        }
image-76
image-77

As you can see, I check that an instance of the class FFI is present. But if an error occurs, I have two solutions. Either test the parser that will raise a \FFI\ParserException, or check that an \FFI\Exception is not raised. At present these are the only two exceptions existing in PHP's FFI module.

How to test FFI::CData? Again, I am breaking the ground because I cannot find an example on the subject. The doc still being very empty and the tutorials non-existent. For QuickJs I have to initialize a runtime and then a context.

$this->runtime = $this->ffi->JS_NewRuntime();
$this->ffi->JS_NewContext($this->runtime)

JS_NewRuntime() gives me this:

.object(FFI\CData:struct JSRuntime*)#366 (1) {

[0]=> object(FFI\CData:struct JSRuntime)#365 (0) {}

}

So, I can test if the class is of type CData but also verify that the structure is the one expected:

$ffi = new JsEvalService();
$ffi->setRuntime();
$this->assertInstanceOf(\FFI\CData::class, $ffi->getRuntime());
$ctype = \FFI::typeof($ffi->getRuntime());
$this->assertEquals('struct JSRuntime*', $ctype->getName());

The same goes for the context:

$ffi = new JsEvalService();
$ffi->setRuntime();
$ffi->setContext();
$this->assertInstanceOf(\FFI\CData::class, $ffi->getContext());
$ctype = \FFI::typeof($ffi->getContext());
$this->assertEquals('struct JSContext*', $ctype->getName());

Here the trick is to seek the \FFI\CType object of your \FFI\CData. You will have access to a bunch of methods.

getName() will be enough for me to verify that the expected type of each of my instructions is indeed compliant.

We continue in the assembly of our call to the JS engine. By looking at our header, we understand that we need to pass the context to our function JS_EVAL(), of course our JavaScript code, the size of our JS code and the next two parameters are not really necessary for what we want to do. Basically we can pass a file with additional modules (compiled with quickjs) and the last parameter is a flag to specify some options (if we are in a module, if we want to enable backtrace, etc.). I do not yet master all the subtleties but in any case these options are not necessary for us.

A method of this type should be enough to ensure that our connection to the lib is really good.

$this->ffi->JS_Eval( $this->context, $js, strlen($js) , "<evalScript>", 0);

The code for our first call:

$jsString = '2 * 3';
$ffi = new JsEvalService();
$ffi->init();
$math = $ffi->eval($jsString);

Returns this to us:

object(FFI\CData:struct JSValue)#354 (2) {

["u"]=>

object(FFI\CData:union JSValueUnion)#350 (3) {

["int32"]=>

int(6)

["float64"]=>

float(3.0E-323)

["ptr"]=>

string(3) "0x6"

}
["tag"]=>

int(0)

}

To decode vulgarly, JS_Eval() must always return a structure of type JSValue. What will interest us here is the type and address of the pointer. The type tells us if we are in error or if we have a real value. And the address will allow us to really retrieve the value of our JavaScript return.

Here is the list of types found in the code:

// quickjs.h:67
const JS_TAG_FIRST       = -11; /* first negative tag */
const JS_TAG_BIG_DECIMAL = -11;
const JS_TAG_BIG_INT     = -10;
const JS_TAG_BIG_FLOAT   = -9;
const JS_TAG_SYMBOL      = -8;
const JS_TAG_STRING      = -7;
const JS_TAG_MODULE      = -3; /* used internally */
const JS_TAG_FUNCTION_BYTECODE = -2; /* used internally */
const JS_TAG_OBJECT      = -1;
const JS_TAG_INT            = 0;
const JS_TAG_BOOL           = 1;
const JS_TAG_NULL           = 2;
const JS_TAG_UNDEFINED      = 3;
const JS_TAG_UNINITIALIZED  = 4;
const JS_TAG_CATCH_OFFSET   = 5;
const JS_TAG_EXCEPTION      = 6;
const JS_TAG_FLOAT64        = 7;

We are therefore of type JS_TAG_INT which is rather good news. QuickJS is well done too. When we look at the source code we notice that there is a method to retrieve each value for each type. Here it will be JS_ToInt32()

We will therefore add the necessary information to our header.

typedef int32_t int32;
int JS_ToInt32(JSContext *ctx, int32_t *pres, JSValue val);

I will thus build a small method to retrieve the data.

const JS_TAG_FIRST = -11; /* first negative tag */
const JS_TAG_BIG_DECIMAL = -11;
const JS_TAG_BIG_INT = -10;
const JS_TAG_BIG_FLOAT = -9;
const JS_TAG_SYMBOL = -8;
const JS_TAG_STRING = -7;
const JS_TAG_MODULE = -3; /* used internally */
const JS_TAG_FUNCTION_BYTECODE = -2; /* used internally */
const JS_TAG_OBJECT = -1;
const JS_TAG_INT = 0;
const JS_TAG_BOOL = 1;
const JS_TAG_NULL = 2;
const JS_TAG_UNDEFINED = 3;
const JS_TAG_UNINITIALIZED = 4;
const JS_TAG_CATCH_OFFSET = 5;
const JS_TAG_EXCEPTION = 6;
const JS_TAG_FLOAT64 = 7;   
 
public function getValue(\FFI\CData $jsValue){
        switch($jsValue->tag) {
            case self::JS_TAG_INT:
                $value = \FFI::new('int32_t');
                $this->ffi->JS_ToInt32($this->context, FFI::addr($value), $jsValue);
                $value = $value->cdata;
                break;
            default:
                break;
        }
        return $value;
    }

I take the opportunity to prepare the ground for the next types to manage. It's not very clean but it allows me to move forward and focus on the QuickJS part that I do not master. With a bit of luck I will get to the end of the subject and I will clean up the whole code. Meanwhile, I can write the article in parallel and share with you all of my little findings.

To retrieve our value, that is to say depending on the type, retrieve the correct typed value, I will have to test on the tag that gives me its type and depending on the type go through the right internal method to the JS engine. This one will need the context, the pointer of a CData that will contain the result and finally our JSValue which contains the result of our JavaScript execution. So the result can be found via $value->CData

Here is the test that I set up:

    public function testMath(){
        $jsString = '2 * 3';
        $ffi = new JsEvalService();
        $ffi->init();
        $jsValue = $ffi->eval($jsString);
        $this->assertEquals(JsEvalService::JS_TAG_INT, $jsValue->tag);
        $this->assertIsInt($ffi->getValue($jsValue));
        $this->assertEquals(6, $ffi->getValue($jsValue));
    }

This article is coming to an end. We have correctly made our first call to the QuickJS library. This allowed us to set up an embryo of code. But most importantly, to see how to perform unit tests on our different calls.

In the next article we will flesh out the code a bit by adding all the return types and make our first Callbacks to PHP code. Yes, QuickJS will be able to call PHP code to dynamically retrieve values to be used in our JavaScript code.

I provide you with the "complete" (should I say incomplete) code used for this article to better understand how the different elements are called.

typedef struct JSRuntime JSRuntime;
typedef struct JSContext JSContext;
typedef struct JSObject JSObject;
typedef struct JSClass JSClass;
typedef uint32_t JSClassID;
typedef uint32_t JSAtom;

typedef union JSValueUnion {
int32_t int32;
double float64;
void *ptr;
} JSValueUnion;

typedef struct JSValue {
JSValueUnion u;
int64_t tag;
} JSValue;

JSRuntime *JS_NewRuntime(void);
JSContext *JS_NewContext(JSRuntime *rt);
JSValue JS_Eval(JSContext *ctx, const char *input, size_t input_len, const char *filename, int eval_flags);
typedef int32_t int32;
int JS_ToInt32(JSContext *ctx, int32_t *pres, JSValue val);
<?php
declare(strict_types=1);

namespace Partitech\PhpQuickjs;

Class JsEvalService
{
    const JS_TAG_FIRST = -11; /* first negative tag */
    const JS_TAG_BIG_DECIMAL = -11;
    const JS_TAG_BIG_INT = -10;
    const JS_TAG_BIG_FLOAT = -9;
    const JS_TAG_SYMBOL = -8;
    const JS_TAG_STRING = -7;
    const JS_TAG_MODULE = -3; /* used internally */
    const JS_TAG_FUNCTION_BYTECODE = -2; /* used internally */
    const JS_TAG_OBJECT = -1;
    const JS_TAG_INT = 0;
    const JS_TAG_BOOL = 1;
    const JS_TAG_NULL = 2;
    const JS_TAG_UNDEFINED = 3;
    const JS_TAG_UNINITIALIZED = 4;
    const JS_TAG_CATCH_OFFSET = 5;
    const JS_TAG_EXCEPTION = 6;
    const JS_TAG_FLOAT64 = 7;

    private string $libPath = '/home/geraud/Projets/Partitech/QuickJs/php/src/lib/';
    private string $quickjsSharedObjectName = 'libquickjs.so';
    private string $headerFileName = 'headers.h';
    private $ffi;
    private $runtime;
    private $context;
    public function __construct(){
        $headers = file_get_contents($this->getHeaderFilePath());
        $this->ffi = \FFI::cdef($headers, $this->getQuickjsSharedObjectPath());
    }

    public function getQuickjsSharedObjectPath(): string
    {
        return $this->libPath . $this->quickjsSharedObjectName;
    }

    public function getHeaderFilePath(): string
    {
        return  $this->libPath . $this->headerFileName;
    }

    public function setRuntime(): self
    {
        $this->runtime = $this->ffi->JS_NewRuntime();
        return $this;
    }
    public function getRuntime()
    {
        return $this->runtime;
    }
    public function setContext(): self
    {
        $this->context = $this->ffi->JS_NewContext($this->runtime);
        return $this;
    }
    public function getContext(){
        return $this->context;
    }

    public function init(){
        $this->setRuntime();
        $this->setContext();
        return $this->ffi;
    }

    public function eval($js)
    {
        return $this->ffi->JS_Eval( $this->context, $js, strlen($js) , '', 0);
    }

    public function getValue(\FFI\CData $jsValue){
        switch($jsValue->tag) {
            case self::JS_TAG_INT:
                $value = \FFI::new('int32_t');
                $this->ffi->JS_ToInt32($this->context, \FFI::addr($value), $jsValue);
                $value = $value->cdata;
                break;
            default:
                $value = null;
                break;
        }
        return $value;
    }
}
<?php
declare(strict_types=1);

namespace Partitech\PhpQuickjs\Tests;

use Partitech\PhpQuickjs\JsEvalService;

class JsEvalTest extends \PHPUnit\Framework\TestCase
{
    public function testRuntime()
    {
        $ffi = new JsEvalService();
        $ffi->setRuntime();
        $this->assertInstanceOf(\FFI\CData::class, $ffi->getRuntime());
        $ctype = \FFI::typeof($ffi->getRuntime());
        $this->assertEquals('struct JSRuntime*', $ctype->getName());
    }
    public function testContext()
    {
        $ffi = new JsEvalService();
        $ffi->setRuntime();
        $ffi->setContext();
        $this->assertInstanceOf(\FFI\CData::class, $ffi->getContext());
        $ctype = \FFI::typeof($ffi->getContext());
        $this->assertEquals('struct JSContext*', $ctype->getName());
    }

    public function testInit()
    {
        try{
            $ffi = new JsEvalService();
            $this->assertInstanceOf(\FFi::class, $ffi->init());
        }catch (\FFI\ParserException $e){
            $this->fail('FFI\ParserException: ' . $e->getMessage());
        }catch(\FFI\Exception $e){
            $this->fail('FFI\Exception: ' . $e->getMessage());
        }
    }

    public function testMath(){
        $jsString = '2 * 3';
        $ffi = new JsEvalService();
        $ffi->init();
        $jsValue = $ffi->eval($jsString);
        $this->assertEquals(JsEvalService::JS_TAG_INT, $jsValue->tag);
        $this->assertIsInt($ffi->getValue($jsValue));
        $this->assertEquals(6, $ffi->getValue($jsValue));
    }
}