Cài đặt
Không gian làm việc của tôi sẽ đơn giản. Một thư mục với nguồn của QuickJS, một thư mục với nguồn C (sẽ giúp chúng ta đơn giản hóa phiên bản đầu tiên của mình), và một thư mục cho thư viện PHP của chúng ta. Cài đặt 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 .. 
    Gọi QuickJS và thực thi mã JavaScript đơn giản.
Trong quá trình biên dịch thư viện của chúng ta, chúng ta đã tạo thư viện của mình nhưng cũng là tài liệu sẽ giúp chúng ta hiểu rõ hơn về cách hoạt động của trình động JavaScript này. Một cái nhìn nhanh vào mục lục và chúng ta đã thấy mình ngay lập tức trong mô tả của API cho chúng ta biết rằng hàm JS_EVAL() cho phép chúng ta khởi chạy một đánh giá mã JavaScript. Tuyệt, đó chính xác là những gì chúng ta đang tìm kiếm. Và với mục tiêu đầu tiên của chúng ta, chúng ta sẽ thử làm 2*3=6? Đó sẽ là một khởi đầu tốt... Hãy đến với tệp tiêu đề để khám phá ra quả cầu thông tin chúng ta cần: https://github.com/bellard/quickjs/blob/master/quickjs.h Ồ... okay, thứ đó dài như một con đường cao tốc... Vì vậy, tại dòng 781 chúng ta tìm thấy những gì chúng ta đang tìm kiếm:JSValue JS_Eval(JSContext *ctx, const char *input, size_t input_len,
                const char *filename, int eval_flags);typedef struct JSRuntime JSRuntime;
typedef struct JSContext JSContext;
typedef struct JSObject JSObject;
typedef struct JSClass JSClass;
typedef uint32_t JSClassID;
typedef uint32_t JSAtom;JSContext *JS_NewContext(JSRuntime *rt);JSRuntime *JS_NewRuntime(void);typedef struct JSValue {
    JSValueUnion u;
    int64_t tag;
} JSValue;
typedef union JSValueUnion {
    int32_t int32;
    double float64;
    void *ptr;
} JSValueUnion;FFI::cdef('VOS DEFINITIONS.h', 'VOTRE_POINT_SO.so');FFI::load('VOS DEFINITIONS.h');#define FFI_LIB "/path/to/your_lib.so";        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());
        } 
     
    $this->runtime = $this->ffi->JS_NewRuntime();
$this->ffi->JS_NewContext($this->runtime).object(FFI\CData:struct JSRuntime*)#366 (1) {
[0]=> object(FFI\CData:struct JSRuntime)#365 (0) {}
}$ffi = new JsEvalService();
$ffi->setRuntime();
$this->assertInstanceOf(\FFI\CData::class, $ffi->getRuntime());
$ctype = \FFI::typeof($ffi->getRuntime());
$this->assertEquals('struct JSRuntime*', $ctype->getName());Cũng tương tự cho bối cảnh:
$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());Ở đây, mẹo là tìm kiếm đối tượng \FFI\CType của \FFI\CData của bạn. Bạn sẽ có quyền truy cập vào một loạt các phương thức.
- FFI\CType::getAlignment — Mô tả
- FFI\CType::getArrayElementType — Mô tả
- FFI\CType::getArrayLength — Mô tả
- FFI\CType::getAttributes — Mô tả
- FFI\CType::getEnumKind — Mô tả
- FFI\CType::getFuncABI — Mô tả
- FFI\CType::getFuncParameterCount — Mô tả
- FFI\CType::getFuncParameterType — Mô tả
- FFI\CType::getFuncReturnType — Mô tả
- FFI\CType::getKind — Mô tả
- FFI\CType::getName — Mô tả
- FFI\CType::getPointerType — Mô tả
- FFI\CType::getSize — Mô tả
- FFI\CType::getStructFieldNames — Mô tả
- FFI\CType::getStructFieldOffset — Mô tả
- FFI\CType::getStructFieldType — Mô tả
getName() sẽ đủ để tôi xác nhận rằng kiểu mong đợi của mỗi lệnh của tôi thực sự phù hợp.
Chúng ta tiếp tục trong việc lắp ráp cuộc gọi của chúng ta đến động cơ JS. Khi nhìn vào header của chúng ta, chúng ta hiểu rằng chúng ta cần phải truyền bối cảnh cho hàm JS_EVAL() của mình, tất nhiên là mã JavaScript của chúng ta, kích thước của mã JS của chúng ta và hai tham số tiếp theo không thực sự cần thiết cho những gì chúng ta muốn làm. Cơ bản là chúng ta có thể truyền một tệp với các mô-đun bổ sung (đã biên dịch với quickjs) và tham số cuối cùng là một cờ để chỉ định một số tùy chọn (nếu chúng ta đang trong một mô-đun, nếu chúng ta muốn kích hoạt backtrace, v.v.). Tôi chưa thực sự nắm vững tất cả những tinh tế nhưng dù sao những tùy chọn này cũng không cần thiết cho chúng ta.
Một phương pháp kiểu này sẽ đủ để đảm bảo rằng kết nối của chúng ta với thư viện thực sự tốt.
$this->ffi->JS_Eval( $this->context, $js, strlen($js) , "<evalScript>", 0);Mã cho cuộc gọi đầu tiên của chúng ta:
$jsString = '2 * 3';
$ffi = new JsEvalService();
$ffi->init();
$math = $ffi->eval($jsString);Trả lại cho chúng ta:
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)
}
Để giải mã một cách thô thiển, JS_Eval() luôn phải trả về một cấu trúc kiểu JSValue. Điều sẽ thu hút chúng ta ở đây là kiểu và địa chỉ của con trỏ. Kiểu cho chúng ta biết liệu chúng ta có đang trong lỗi hay nếu chúng ta có một giá trị thực sự. Và địa chỉ sẽ cho phép chúng ta thực sự lấy lại giá trị của kết quả JavaScript trả về của chúng ta.
Dưới đây là danh sách các kiểu tìm thấy trong mã:
// 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;Vì vậy, chúng ta thuộc về kiểu JS_TAG_INT, đó là tin tốt lành. QuickJS cũng được làm tốt lắm. Khi chúng ta xem xét mã nguồn, chúng ta nhận thấy rằng có một phương pháp để lấy lại mỗi giá trị cho mỗi kiểu. Ở đây nó sẽ là JS_ToInt32()
Vì vậy, chúng ta sẽ thêm thông tin cần thiết vào header của mình.
typedef int32_t int32;
int JS_ToInt32(JSContext *ctx, int32_t *pres, JSValue val);Tôi sẽ xây dựng một phương pháp nhỏ để lấy dữ liệu.
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;
    }Tôi tận dụng cơ hội để chuẩn bị mặt bằng cho các kiểu tiếp theo cần quản lý. Nó không rất sạch sẽ nhưng nó cho phép tôi tiến lên phía trước và tập trung vào phần QuickJS mà tôi chưa nắm vững. Với một chút may mắn, tôi sẽ đến được cuối chủ đề và tôi sẽ dọn dẹp toàn bộ mã. Trong khi đó, tôi có thể viết bài viết song song và chia sẻ với bạn tất cả những phát hiện nhỏ của tôi.
Để lấy giá trị của chúng ta, tức là tùy thuộc vào kiểu, lấy giá trị đúng theo kiểu, tôi sẽ phải kiểm tra trên thẻ đưa ra cho tôi kiểu của nó và tùy thuộc vào kiểu đi qua phương pháp nội bộ đúng đắn của động cơ JS. Điều này sẽ cần bối cảnh, con trỏ của một CData sẽ chứa kết quả và cuối cùng là JSValue của chúng ta chứa kết quả của việc thực thi JavaScript của chúng ta. Vì vậy, kết quả có thể được tìm thấy qua $value->CData
Dưới đây là bài kiểm tra mà tôi thiết lập:
    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));
    }Bài viết này đang kết thúc. Chúng ta đã thực hiện đúng cuộc gọi đầu tiên của mình đến thư viện QuickJS. Điều này cho phép chúng ta thiết lập một phôi mã. Nhưng quan trọng nhất, để xem cách thực hiện các bài kiểm tra đơn vị trên các cuộc gọi khác nhau của chúng ta.
Trong bài viết tiếp theo, chúng ta sẽ làm phong phú thêm mã một chút bằng cách thêm tất cả các kiểu trả về và thực hiện các Callbacks đầu tiên của chúng ta đến mã PHP. Vâng, QuickJS sẽ có thể gọi mã PHP để lấy động giá trị cần sử dụng trong mã JavaScript của chúng ta.
Tôi cung cấp cho bạn mã "hoàn chỉnh" (nên tôi nói chưa hoàn chỉnh) được sử dụng cho bài viết này để hiểu rõ hơn cách các yếu tố khác nhau được gọi.
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));
    }
}