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 ..
Về không gian làm việc của tôi, tôi đã tạo một thư mục 'php' trong đó tôi sẽ khởi tạo một kiến trúc composer. Điều này sẽ cung cấp cho tôi một không gian làm việc với mọi thứ tôi cần để tạo các tệp mã của mình cũng như các bài kiểm tra đơn vị sẽ xác nhận rằng mọi thứ đang hoạt động đúng cách. Nếu bạn muốn, bạn có thể tham khảo bài viết Tạo một thư viện với composer.

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);
Điều này cho chúng ta biết là chúng ta cần một loại sẽ là JSContext. Một tìm kiếm nhanh cho "struct JSContext" chỉ cho chúng ta đến dòng 50. Ổn, chúng ta sẽ lấy toàn bộ khối, nó sẽ hữu ích...
typedef struct JSRuntime JSRuntime;
typedef struct JSContext JSContext;
typedef struct JSObject JSObject;
typedef struct JSClass JSClass;
typedef uint32_t JSClassID;
typedef uint32_t JSAtom;
Vì vậy, bây giờ, chúng ta sẽ tìm cách tạo ra cái nổi tiếng JSContext này. và ở đó chúng ta nhận thấy: dòng 361
JSContext *JS_NewContext(JSRuntime *rt);
Ổn... Vì vậy, bây giờ chúng ta sẽ tìm kiếm người tạo ra JSRuntime...nó vô tận câu chuyện này!
Dòng 331
JSRuntime *JS_NewRuntime(void);
Cuối cùng chúng ta nhận thấy rằng kết quả trả về của hàm eval() sẽ là một cấu trúc loại JSValue bao gồm một union và một int.
typedef struct JSValue {
JSValueUnion u;
int64_t tag;
} JSValue;
typedef union JSValueUnion {
int32_t int32;
double float64;
void *ptr;
} JSValueUnion;
Ổn hợp đồng dường như như sau:
Chúng ta tạo một Runtime, chúng ta tạo một context với runtime của chúng ta và sau đó chúng ta có thể thử truyền phép cộng của chúng ta vào js_eval.
Vì vậy, chúng ta có thể bắt đầu tạo các tệp đầu tiên của chúng ta, mã cũng như bài kiểm tra kèm theo.
Nhận xét đầu tiên, tôi không tìm thấy cách chỉ định tệp tiêu đề và thư viện riêng biệt.
Bạn có lựa chọn giữa
FFI::cdef('VOS DEFINITIONS.h', 'VOTRE_POINT_SO.so');
Điều này buộc bạn phải mang theo mọi thứ ngay từ đầu, ngay cả khi có nghĩa là phải có định nghĩa hàng trăm dòng. Cá nhân tôi không thích nó.
Hoặc sử dụng:
FFI::load('VOS DEFINITIONS.h');
Với ở phía trước của tệp tiêu đề của bạn:
#define FFI_LIB "/path/to/your_lib.so";
Điều này không có vẻ linh hoạt lắm đối với tôi. Ồ, mã của tôi có thể khá tồi tệ, ở thời điểm này không quan trọng. Mặt khác, tôi hơi bận tâm một chút vì không có sự linh hoạt để chỉ định những gì tôi muốn sử dụng. Vì vậy, tôi sẽ lấy qua một file_get_contents các định nghĩa của mình. Chúng ta sẽ bỏ qua vẻ đẹp của mã cho đến nay.
Nhận xét thứ hai, php::ffi không thể làm việc tương đối. Do đó, nếu bạn muốn truyền .h hoặc .so của mình với một ./lib/lib.so, nó sẽ không hoạt động. Đây là một hạn chế của thư viện ffi, không phải php. Trong thời gian chờ đợi và để tiến lên, tôi đã trực tiếp đặt đường dẫn trong tệp của mình. Tôi thêm điều này vào danh sách công việc cần làm của mình khi tôi muốn làm cho mã của mình sạch sẽ và linh hoạt hơn.
Vì vậy, tôi bắt đầu tạo lớp đầu tiên của mình, một chút hỗn độn, tôi thừa nhận. Nhưng vẫn vậy, tôi muốn kiểm tra những gì tôi làm. Và sau đó, tôi có câu hỏi:
Làm thế nào để kiểm tra \FFI::cdef? Bạn có thể có lỗi trong định nghĩa của mình, hoặc nó không được tìm thấy, hoặc nó không hợp lệ. Nhưng bạn cũng có thể gặp lỗi trong quá trình tải thư viện. Tôi không tìm thấy bất kỳ tài nguyên nào về chủ đề này. Tôi vẫn cung cấp cho bạn tất cả những gì tôi quản lý để tìm thấy để đáp ứng nhu cầu kiểm tra của mình:
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)
JS_NewRuntime() cho tôi cái này:
.object(FFI\CData:struct JSRuntime*)#366 (1) {
[0]=> object(FFI\CData:struct JSRuntime)#365 (0) {}
}
Vì vậy, tôi có thể kiểm tra xem lớp có phải là loại CData không nhưng cũng xác minh rằng cấu trúc là như mong đợi:
$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));
}
}