Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

介绍

什么是QuickJS

QuickJS is a small and embeddable Javascript engine. It supports the ES2023 specification including modules, asynchronous generators, proxies and BigInt.

Main Features:

  • Small and easily embeddable: just a few C files, no external dependency, 367 KiB of x86 code for a simple hello world program.
  • Fast interpreter with very low startup time: runs the 78000 tests of the ECMAScript Test Suite in about 2 minutes on a single core of a desktop PC. The complete life cycle of a runtime instance completes in less than 300 microseconds.
  • Almost complete ES2023 support including modules, asynchronous generators and full Annex B support (legacy web compatibility).
  • Passes nearly 100% of the ECMAScript Test Suite tests when selecting the ES2023 features (warning: the report available at test262.fyi underestimates the QuickJS results because it does not use the right executable).
  • Can compile Javascript sources to executables with no external dependency.
  • Garbage collection using reference counting (to reduce memory usage and have deterministic behavior) with cycle removal.
  • Command line interpreter with contextual colorization implemented in Javascript.
  • Small built-in standard library with C library wrappers.

QuickJS是由大神Bellard制作的一款小巧,快速的JavaScript运行时。目标是用于嵌入各种程序中。

为何写这份教程

QuickJS本身几乎没有任何的API文档。所有API基本上只能通过看例子进行学习。而且很多API还有很多坑。我在将QuickJS嵌入自己的游戏引擎的过程中遇到了很多坑,所以想做一份较为完善的教程。

本教程有/没有的内容

本教程会介绍如何在C++代码中嵌入QuickJS,将C/C++的代码绑定给QuickJS并且在C++中调用js代码。

本教程不会说如何从Js编译成独立可执行文件,并且调用C++链接库。本教程着眼于JS作为脚本语言嵌入,而非JS作为主语言的情况。

代码实例

本教程附带一份代码实例,使用QuickJS-NG在C++20下编译。

之所以在C++20下是因为在初始化时指定结构体成员的C相关语法只能在C++20下编译通过:

struct Person {
    float age;
    float height;
};

int main() {
    // can compile under C, but can't compile before C++20
    Person p = {.age = 21.0f, .height = 190.0f};
    return 0;
}

注意事项

本教程的所有知识来源于QuickJS-NG而非Bellard的QuickJS。因为最初的QuickJS只能在Linux下编译,而QuickJS-NG做了跨平台处理。QuickJS-NG和最初的QuickJS API相差无几,本书的知识点应该可以同时用在两者之上。

作者不是个熟练的JS使用者

我的工作是游戏引擎开发,并不是前后端开发,我也不太使用JavaScript,所以我并不十分了解JavaScript。如果本教程有疏漏还请指正(email: 2142587070@qq.com)

基础知识

本章会介绍一些QuickJS基础知识。包括一个HelloWorld,使用QuickJS内部标准库,码执行方式和遇到异常时如何检查和处理。。

HelloWorld

本HelloWorld展示了执行一个JS文件。此JS文件会在屏幕上输出一行Hello World

Github代码链接

包含头文件

首先要做的是包含QuickJS头文件:

#include "quickjs.h"
#include "quickjs-libc.h" // optional

quickjs.h包含了所有需要的API,是最基础头文件。而quickjs-libc.h则是拥有很多实用工具的文件(可选)

创建Runtime

首先创建JSRuntime:

JSRuntime* runtime = JS_NewRuntime();
if (!runtime) {
    std::cerr << "init runtime failed" << std::endl;
    return 1;
}

JSRuntime的官方文档解释是:

JSRuntime 表示与对象堆对应的 JavaScript 运行时。多个运行时可以同时存在,但它们不能交换对象。在给定的运行时中,不支持多线程。

JSRuntime是整个QuickJS环境的基础。

创建Context

然后我们需要创建一个Context:

JSContext* ctx = JS_NewContext(runtime);
if (!ctx) {
    std::cerr << "create context failed" << std::endl;
    JS_FreeRuntime(runtime);
    return 2;
}

JSContext的官方文档解释是:

JSContext 表示 JavaScript 上下文(或 Realm)。每个 JSContext 都有其自己的全局对象和系统对象。每个 JSRuntime 可以有多个 JSContext,并且它们可以共享对象,类似于在 Web 浏览器中共享 JavaScript 对象的同源框架。

初始化std帮助库

QuickJS环境初始化之后,环境内除几乎没有任何可用的帮助对象(比如在JS中常见的console.log)。这个时候我们需要从quickjs-libc.h中的函数

js_std_add_helpers(ctx, 0, NULL);

来给JSContext注册一些辅助对象。这些对象包含:

  • console.log(...args):用于控制台输出(注:没有其他的console函数,他只注册了log函数)
  • print(...args):也是用于控制台输出
  • scriptArgs:提供命令行参数。第一个参数是脚本名称

我们会在JS脚本中使用console.log函数输出HelloWorld

读取JS文件

我们的JS文件如下:

// main.js
console.log("Hello QuickJS!");

首先将文件读入内存:

std::ifstream file("demos/01-HelloWorld/main.js",
                   std::ios::in | std::ios::binary);
if (!file) {
    std::cerr << "open file main.js failed" << std::endl;
}
std::stringstream ss;
ss << file.rdbuf();
std::string content = ss.str();

注意需要以二进制形式读取

然后使用JS_Eval函数执行:

JSValue result = JS_Eval(ctx, content.c_str(), content.size(), nullptr, JS_EVAL_TYPE_GLOBAL);

参数分别是:

  • JSContext:你的JS上下文
  • code:代码
  • code_len:代码长度
  • filename:代码所在文件名。可以给空指针或者随便给一个,只是调试使用
  • flags:以何种方式执行。这里是全局执行,也是默认方式

这个函数会执行整个脚本,并返回一个值。因为我们的脚本内没有返回任何值,所以这里的resultJS_UNDEFINED(即JS中的undefine

错误处理

有些时候脚本会执行出错(出现了语法错误,运行时错误或用户在JS代码中抛了异常等),这时JS_Eval会返回一个异常,可以进行处理:

if (JS_IsException(result)) {
    // from quickjs-libc.hpp, to log exception
    js_std_dump_error(ctx);
}

这里先判断返回值是否是异常。然后使用了quickjs-libc的辅助函数帮我们将异常信息输出到控制台上。

释放内存

最后不要忘记释放内存:

JS_FreeValue(ctx, result);
JS_FreeContext(ctx);
JS_FreeRuntime(runtime);

异常处理

本节介绍如何处理QuickJS异常。

何时发生异常

  • 返回JSValue的函数,使用JS_IsException判断是否是异常
  • 返回整数值的函数,如果小于0,则发生异常。

发生异常时得到异常

典型的方式如下:

void LogException(JSContext* ctx) {
    JSValue exception_val = JS_GetException(ctx);
    bool is_error = JS_IsError(ctx, exception_val);

    // convert quickjs exception to string
    const char* str = JS_ToCString(ctx, exception_val);
    if (str) {
        std::cerr << str << std::endl;
        // don't forget to free
        JS_FreeCString(ctx, str);
    }

    if (is_error) {
        // get stack info
        val = JS_GetPropertyStr(ctx, exception_val, "stack");
        if (!JS_IsUndefined(val)) {
            const char* stack_info = JS_ToCString(ctx, val);
            std::cerr << "stack: " << stack_info << std::endl;
            JS_FreeCString(ctx, stack_info);
        }
        JS_FreeValue(ctx, val);
    }

    JS_FreeValue(ctx, exception_val);
}

当有异常抛出时,异常会被记录在JSContext中。使用JS_GetException拿出,转换成字符串输出即可。如果有堆栈信息也可以一并输出。

在有异常时即可调用:

JSValue result = JS_Eval(ctx, content.c_str(), content.size(), nullptr,
                         JS_EVAL_FLAG_STRICT | flags);

if (JS_IsException(result)) {
    LogException(ctx);
}

使用QuickJS标准库

Github代码链接

使用QuickJS标准库

QuickJS有自己的标准库(输入输出,文件操作,json操作等),详见QuickJS-NG文档。但是要使用的话需要调用额外函数进行初始化。

HelloWorld章节已经看到了如何注册console.log和其他全局变量/函数了。其他的函数则需要通过js_init_module_xxx进行初始化:

js_init_module_os(ctx, "os");
js_init_module_std(ctx, "std");
js_init_module_bjson(ctx, "json");

第二个参数是库注册的名称。

JS代码加载库

有至少三种方式:

使用模块模式执行代码

如果想要在JS代码中使用

import * as std from 'std'

这类代码,在执行代码时必须将其视为模块执行:

JSValue result = JS_Eval(ctx, content.c_str(), content.size(), nullptr, 
                         // use this flag!
                         JS_EVAL_TYPE_MODULE);

否则会说没有import语法。

作为模块执行的话有一个很严重的缺点:如果出现某些语法错误/运行时错误,QuickJS会直接静默而非抛出异常。这意味着调试困难。一般如果模块找不到的话他会正常报错。但是在你使用模块内部的对象(比如对一个不存在的类进行new),QuickJS则会abort,并且不会抛出异常(检查JS_Eval的返回值不会是异常)

使用异步加载方式加载

这种方式可以不以模块方式加载:

import('std').then(module => {
    module.puts("I am std module in non-module mode\n");
}).catch(err => {
    print("Error loading module:", err);
});

但这是一段异步代码。我们需要在执行完之后使用js_std_loopjs_std_await等待异步执行完毕:

JSValue result = JS_Eval(ctx, content.c_str(), content.size(), nullptr, JS_EVAL_FLAG_GLOBAL);
js_std_loop(ctx);

js_std_loop会对整个JSContext中需要等待的脚本做等待。只指定某个脚本等待的话需调用js_std_await

预加载模块,然后执行

我们也可以走两步:

  1. 先通过模块模式import需要的模块入JSContext
  2. 执行我们自己的代码。这样可以直接使用已经导入的模块

这里首先以模块模式执行:

std::cout << "execute in pre-module mode" << std::endl;
const char* module_preload_code = R"(
    import * as std from 'std';
    globalThis.std = std;
)";
auto value =
    JS_Eval(ctx, module_preload_code, strlen(module_preload_code), nullptr, JS_EVAL_TYPE_MODULE);

注意这里需要将模块放入globalThis中。

然后执行我们自己的代码:

std.puts("I am std module when preload module\n") // call function in std module
JSValue result = JS_Eval(ctx, content.c_str(), content.size(), nullptr, JS_EVAL_FLAG_GLOBAL);

代码执行方式

本节解释JS_Eval的最后一个参数flag的含义。以及如何使用编译后的JS二进制代码执行。

二进制代码执行的例子在这里

代码执行方式

有两种Type:

  • JS_EVAL_TYPE_GLOBAL:全局模式执行(默认的),即所有代码都在全局模式下执行。某些代码中定义的对象可以在其他代码中使用
  • JS_EVAL_TYPE_MODULE:以模块模式执行。代码被视为模块(类似nodejs模块),并且可以使用模块导入语法import * from 'Module'

和四种Flag(可组合):

  • JS_EVAL_FLAG_STRICT:以严格模式执行(相当于JS代码中写use strict,但QuickJS会忽略你写的,所以你只能通过指定此Flag来执行严格模式)
  • JS_EVAL_FLAG_COMPILE_ONLY:只是编译,不运行
  • JS_EVAL_FALG_BACKTRACE_BARRIER:不要在出错时的栈回溯中包含此代码执行之前的堆栈帧
  • JS_EVAL_FLAG_ASYNC:只能搭配JS_EVAL_TYPE_GLOBAL。代码以异步执行,JS_Eval返回一个Promise,需要等待。

以二进制方式执行代码

QuickJS有个编译器qjsc可以将JS代码编译成二进制,这样减少了代码体积,在运行时速度也会更快。

使用qjsc编译我们的代码:

qjsc -b -n "main.js" -o output.qjs main.js
  • -b代表编译成二进制而不是C代码(没错可以编译成C代码,直接嵌入C文件中执行)
  • -n指定编译之后的脚本名称(用于调试,在抛出异常时包含在stack traces中)
  • -o指定编译的结果文件

最后的参数是要编译的代码文件。

然后在C++代码中执行:

JSValue obj = JS_ReadObject(ctx, (uint8_t*)content.data(), content.size(), JS_READ_OBJ_BYTECODE);
JSValue result = JS_EvalFunction(ctx, obj);

首先使用JS_ReadObject从二进制代码中读取JSValue,然后使用JS_EvalFunction执行。

让我们开始绑定

从这一节开始进行C++代码绑定到QuickJS。我们先介绍如何绑定普通的全局变量和函数。然后介绍如何绑定类。最后介绍如何绑定一个模块到QuickJS

绑定全局变量

Github代码

我们首先从最简单的全局变量绑定开始。

QuickJS的类型

QuickJS中使用JSValue包装所有C/C++类型。支持的类型如下:

  • Number:数字类型
  • String:字符串类型(是C风格字符串不是std::string
  • Boolean:布尔值
  • Class:类
  • Module:模块
  • Exception:异常
  • Function:函数
  • Array:数组

例子

现在希望绑定一个数值类型的变量:

int gGlobalVar = 123;

做法如下:

void Bind(JSContext* ctx) {
    // Int32 value is directly copied into JSValue(no malloc), so we don't need JS_FreeValue it
    JSValue new_obj = JS_NewInt32(ctx, gGlobalVar);
    if (JS_IsException(new_obj)) {
        js_std_dump_error(ctx);
        JS_FreeValue(ctx, new_obj);
        return;
    }

    JSValue global_this = JS_GetGlobalObject(ctx);

    // JS_WRITABLE | JS_ENUMERABLE | JS_CONFIGURABLE by default
    JS_SetPropertyStr(ctx, global_this, "global_var", new_obj);

    // don't forget cleanup
    JS_FreeValue(ctx, global_this);
}

C++和JS的所有数据交换都是通过JSValue进行的。这里步骤如下:

  1. 首先使用JS_NewInt32创建一个整数类型的JSValue
  2. 做异常检查
  3. 得到全局对象JS_GetGlobalObject
  4. 使用JS_SetPropertyStr将我们的变量注册到全局对象中
  5. 清理内存

重点是注册的函数JS_SetPropertyStr。其原型如下:

int JS_SetPropertyStr(JSContext *ctx, JSValueConst this_obj, const char *prop, JSValue val)
  • this_object:要注册到的对象
  • prop:要注册的对象名称(在JS中使用的名称)
  • val:要注册的对象

注册完之后就可以在JS中使用了:

console.log("global_var: ", global_var)

绑定基础类型变量的规则

绑定对象的步骤如下:

  1. 使用JS_NewXXX来创建一个JS对象
  2. 使用JS_SetPropertyStr来将对象绑定在另一个JS对象中
  3. 清理内存

可创建的JS对象

  • 通用的数值类创建使用JS_NewNumber。细分如下:
    • JS_NewInt32:创建32位的整数
    • JS_NewInt64:创建64位整数(底层规则是:如果是32位,调用JS_NewInt32,否则直接调用JS_NewFloat64存在64位浮点数中)
    • JS_NewUint32:创建无符号32位整数(规则通JS_NewInt64
    • JS_NewFloat64:创建double类型(JS中没有float类型都是double)
  • 大数类型:
    • JS_NewBigInt64
    • JS_NewBigUInt64
  • JS_NewBool:创建布尔值
  • JS_NewClass:创建类
  • JS_NewObject:创建对象
  • 字符串类:
    • JS_NewString
    • JS_NewStringLen:可指定字符串长度

何时释放JSValue?

JSValue的释放也是有讲究的。其底层实现为:

void JS_FreeValueRT(JSRuntime *rt, JSValue v)
{
    if (JS_VALUE_HAS_REF_COUNT(v)) {
        JSRefCountHeader *p = (JSRefCountHeader *)JS_VALUE_GET_PTR(v);
        if (--p->ref_count <= 0) {
            js_free_value_rt(rt, v);
        }
    }
}

是先判断是否有引用计数,如果有的话,当计数降为0释放内存。

有引用计数的一般是类和对象类型。像基础的数值类型是直接值拷贝入JSValue的,也不会有内存分配/释放:

// JSValue impl
typedef union JSValueUnion {
    int32_t int32;
    double float64;
    void *ptr;
    int32_t short_big_int;
} JSValueUnion;

typedef struct JSValue {
    JSValueUnion u;
    int64_t tag;
} JSValue;
  • tag:即JS_TAG_XXX类型,标识JSValue的类型
  • u:如果是数值类型,就记录在非ptr中并且没有内存分配。否则进行内存分配,将指针记录在ptr

JS_VALUE_HAS_REF_COUNT也让我们知道哪些是会进行内存分配的:

#define JS_VALUE_HAS_REF_COUNT(v) ((unsigned)JS_VALUE_GET_TAG(v) >= (unsigned)JS_TAG_FIRST)

enum {
    // has memory allocation
    JS_TAG_FIRST       = -9, /* first negative tag */
    JS_TAG_BIG_INT     = -9,
    JS_TAG_SYMBOL      = -8,
    JS_TAG_STRING      = -7,
    JS_TAG_MODULE      = -3, /* used internally */
    JS_TAG_FUNCTION_BYTECODE = -2, /* used internally */
    JS_TAG_OBJECT      = -1,

    // no memory allocation 
    JS_TAG_INT         = 0,
    JS_TAG_BOOL        = 1,
    JS_TAG_NULL        = 2,
    JS_TAG_UNDEFINED   = 3,
    JS_TAG_UNINITIALIZED = 4,
    JS_TAG_CATCH_OFFSET = 5,
    JS_TAG_EXCEPTION   = 6,
    JS_TAG_SHORT_BIG_INT = 7,
    JS_TAG_FLOAT64     = 8,
};

那么所有时候,只要使用了JS_NewXXX就一定要调用JS_FreeValue吗?答案是否定的。具体是否需要释放要看之后的函数是否影响了其引用计数。比如上面HelloWorld中的代码我们就没有释放new_obj。因为new_obj被创建出来时引用计数是1。而JS_SetPropertyStr传入new_obj后是不会改变其引用计数的。这个时候如果调用了JS_FreeValue则会将new_obj的引用计数降为0,进而释放内存。但此时其已被注册在global_this中了。在JSRuntime释放的时候会尝试释放所有被引用的节点,这个时候会再次释放new_obj造成程序崩溃。

所以是否要释放JSValue,关键在于之后使用的函数是否增加了JSValue的引用计数。如果增加了就需要释放。

或者说的更严谨一点,当其他函数内部使用了js_dup()时就是增加了引用计数,这个时候我们就得手动释放。

绑定类对象

如果想要绑定类对象,需要使用JS_NewClassObject并且将类对象设置进去:

Person* p = new Person{}; // C++ class object
JSValue result = JS_NewObjectClass(ctx, class_id);
JS_SetOpaque(result, person);
JS_SetPropertyStr(ctx, global_this, "person", result);

这里需要一个class_idclass_id是类的唯一标识,在你注册类的时候会生成一个(等待后面绑定类的时候会说到)。

然后使用JS_SetOpaque函数将我们的类对象塞给JSValue即可。

绑定空对象

使用JS_NewObject创建一个空对象(即创建一个JS中的Object实例)。这个对象不和任何类相关联,所以也不需要一个class_id

内置JSValue常量

  • JS_UNDEFINED
  • JS_NULL

这两个是内置的字面常量,可以直接使用无需JS_NewXXX

绑定全局函数

Github代码

本节介绍了如何绑定全局函数

例子

假设我们有一个简单的Add函数:

int Add(int a, int b) {
    return a + b; 
}

我们和绑定变量一样绑定他到全局对象。只是使用JS_NewCFunction创建JSValue

JSValue global_this = JS_GetGlobalObject(ctx);

constexpr int FnParamCount = 2;
JSValue fn = JS_NewCFunction(ctx, AddFnBinding, "Add", FnParamCount);

JS_SetPropertyStr(ctx, global_this, "Add", fn);

JS_FreeValue(ctx, global_this);

注意JS_NewCFunction的第二个参数AddFnBinding。他的类型是JSCFunction,一个函数指针:

typedef JSValue JSCFunction(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv);

QuickJS在看到JS调用C++函数时其实会调用我们给的这个函数指针。我们需要实现此函数指针并且在内部调用我们自己的C++函数:

JSValue AddFnBinding(JSContext* ctx, JSValue self, int argc,
                     JSValueConst* argv) {
    if (argc != 2) {
        return JS_ThrowPlainError(ctx, "Add function must has two parameters");
    }

    JSValueConst param1 = argv[0];
    JSValueConst param2 = argv[1];
    if (!JS_IsNumber(param1) || !JS_IsNumber(param2)) {
        return JS_ThrowTypeError(ctx, "Add accept two integral");
    }

    int32_t value1, value2;
    JS_ToInt32(ctx, &value1, param1);
    JS_ToInt32(ctx, &value2, param2);

    return JS_NewInt32(ctx, Add(value1, value2));
}

参数解释:

  • ctx:JS上下文

  • self:如果函数是类对象函数,那这里会传入类对象。否则是JS_UNDEFINED

  • argc:传给函数的参数格式

  • argv:传给函数的参数。注意这里写的虽是JSValueConst但并不代表他们是常量。因为底层定义如下:

    #define JSValueConst JSValue
    

这里首先检查参数个数和参数类型是否是我们想要的,然后将两个参数使用JS_ToXXX转换成C++类型,然后调用我们的Add函数并将结果穿给JSValue

更多函数类型

QuickJS内置了很多函数类型。JSCFunction只是最通用和最常见的类型。还有如下类型(定义在JSCFunctionEnum):

  • JS_CFUNC_generic:最通用的函数类型(就是我们使用的JSCFunction
  • JS_CFUNC_constructor:类的构造函数
  • 专门给数学函数定义的类型:
    • JS_CFUNC_f_f:形如double(*)(double)的函数指针
  • JS_CFUNC_f_f_f:形如double(*)(double, double)的函数指针
  • JS_CFUNC_getterJS_CFUNC_setter:getter和setter
  • JS_CFUNC_iterator_next:用于迭代器的类型
  • JS_CFUNC_constructor_or_func:<不清楚是什么,以后研究>
  • 以及某些函数的magic版本(在类型后面加_magic例如JS_CFUNC_generic_magic

这些函数的签名可以在JSCFunctionType看到:

typedef union JSCFunctionType {
    JSCFunction *generic;
    JSValue (*generic_magic)(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic);
    JSCFunction *constructor;
    JSValue (*constructor_magic)(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv, int magic);
    JSCFunction *constructor_or_func;
    double (*f_f)(double);
    double (*f_f_f)(double, double);
    JSValue (*getter)(JSContext *ctx, JSValueConst this_val);
    JSValue (*setter)(JSContext *ctx, JSValueConst this_val, JSValueConst val);
    JSValue (*getter_magic)(JSContext *ctx, JSValueConst this_val, int magic);
    JSValue (*setter_magic)(JSContext *ctx, JSValueConst this_val, JSValueConst val, int magic);
    JSValue (*iterator_next)(JSContext *ctx, JSValueConst this_val,
                             int argc, JSValueConst *argv, int *pdone, int magic);
} JSCFunctionType;

数学函数类型

JS_CFUNC_f_fJS_CFUNC_f_f_f

这两个函数都是接收double作为参数(f_f接收一个,f_f_f接收两个),返回一个double值。一般用于绑定sin,cos这种数学函数。优点是函数签名不含JSValue直接是double,不需要做额外的转换。

getter和setter

用于变量的Getter和Setter。可以使用这种方式实现只读变量(只实现getter不实现setter)。函数签名分别是:

  • Getter: JSValue (*getter)(JSContext *ctx, JSValueConst this_val)。传入要获得值的对象
  • Setter:JSValue (*setter)(JSContext *ctx, JSValueConst this_val, JSValueConst val)。传入值的来源val和放入值的变量this_val

迭代器类型

<暂时没研究,之后补上>

带有magic的类型

几乎所有类型的函数都有magic版本。magic版本的作用是通过额外的参数int magic来将多个函数聚合在一起:

// binding function
JSValue BindMagicFn(JSContext* ctx, JSValue, int argc, JSValueConst* argv, int magic) {
    if (magic == 0) {
        MagicFn1();
    } else if (magic == 1) {
        MagicFn2();
    }

    return JS_UNDEFINED;
}

// bind
JSValue fn1 = JS_NewCFunctionMagic(ctx, BindMagicFn, "MagicFn1", FnParamCount, JS_CFUNC_generic_magic, 0);
JSValue fn2 = JS_NewCFunctionMagic(ctx, BindMagicFn, "MagicFn2", FnParamCount, JS_CFUNC_generic_magic, 1);

使用JS_NewCFunctionMagic来绑定magic函数。通过最后的参数来区分内部到底是在使用哪个函数。

如何绑定这些花里胡哨的函数类型

虽然函数类型和签名很多,但是JS_SetPropertyStr的第二个参数只接收JSCFunction类型。要如何绑定呢?有两种方法:

  1. 使用特定的绑定函数,比如magic相关的就是JS_NewCFunctionMagic,getter/setter就是 JS_DefinePropertyGetSet()

  2. 使用JSCFunctionType进行转换:

    JSCFunctionType fn_type;
    // JS_CFUNC_f_f_f pass two double elem and return one double elem
    fn_type.f_f_f = +[](double param1, double param2) -> double { return param1 + param2; };
    JSValue fn = JS_NewCFunction2(ctx, fn_type.generic, "Sum", 1, JS_CFUNC_f_f_f, 0);
    

绑定类

Github代码

JS中的原型对象(prototype)

我们需要先理解JS中的原型对象。因为C++这边的绑定和JS的原理是一致的。

JS类的一个典型古典定义如下:

function Person(name) {
    this.name = name;
}

Person.prototype.sayHello = function() {
    console.log("Hello, my name is " + this.name);
};

let alice = new Person("Alice");
alice.sayHello();

类本身是一个函数Person。而其成员函数都是绑定在函数的原型prototype中。当实例化的时候,会调用Person函数初始化成员变量,并且拷贝prototype到对象中以找到成员函数。

C++绑定类的例子

假设我们有个Person类:

struct Person {
    static int ID;
    
    char name[512] = {0};
    float height;
    float weight;
    int age;

    Person(const std::string& name, float height, int age, float weight)
        : height{height}, age{age}, weight{weight} {
        ChangeName(name);
    }

    void Introduce() const {
        std::cout << "I am " << name << ", age " << age << ", height " << height
                  << ", weight " << weight << std::endl;
    }

    float GetBMI() const { return weight / (height * height); }

    void ChangeName(const std::string& name) {
        strcpy(this->name, name.data());
    }
};

绑定的过程如下:

首先,需要创建一份JSClassID。QuickJS内部用JSClassID唯一标识一个类:

gClassID = JS_NewClassID(runtime, &gClassID);
if (gClassID == 0) {
    std::cerr << "create class id failed" << std::endl;
}

然后需要一个类定义对象JSClassDef

JSClassDef def{};
// will call when value be freed
def.finalizer = +[](JSRuntime*, JSValue self) {
    if (!JS_IsObject(self)) {
        std::cerr << "in finalizer, self is not object" << std::endl;
    }

    Person* opaque = static_cast<Person*>(JS_GetOpaque(self, gClassID));
    if (!opaque) {
        std::cerr << "self is nullptr" << std::endl;
    }

    delete opaque;
};
def.class_name = class_name;

def.finalizer是类的析构函数。当JSValue被GC掉的时候会调用。我们需要在这里清理内存。

def.class_name则是类名。

接下来需要将这个类定义注册到JSRuntime中:

JS_NewClass(runtime, gClassID, &def);

然后我们需要根据原型对象的原理组建一个原型对象:

JSValue proto = JS_NewObject(ctx);

接下来需要为成员变量/函数创建对应的JSValue。注意这里成员变量和函数在C++中都是函数(成员变量由getter/setter表示)。比如说getter/setter:

JSValue NameGetter(JSContext* ctx, JSValue self) {
    // I'm lazy to check type :-)
    const Person* p = static_cast<const Person*>(JS_GetOpaque(self, gClassID));
    return JS_NewString(ctx, p->name);
}

JSValue NameSetter(JSContext* ctx, JSValue self, JSValueConst param) {
    // I'm lazy to check type :-)
    Person* p = static_cast<Person*>(JS_GetOpaque(self, gClassID));
    p->ChangeName(JS_ToCString(ctx, param));
    return JS_UNDEFINED;
}

使用JS_GetOpaqueJSValue中拿到特定类的指针(注意gClassID一定要对得上。如果class id是无效的会返回空指针,这也就意味着你必须先注册对应的类)。

然后绑定给prototype:

JSAtom name = JS_NewAtom(ctx, "name");
JS_DefinePropertyGetSet(ctx, proto, NameGetterJSValue, NameSetterJSValue, nameAtom, 0);
JS_FreeAtom(ctx, atom);

其他成员函数同理。

然后定义构造函数:

JSValue ConstructorBinding(JSContext* ctx, JSValue self, int argc,
                           JSValueConst* argv) {
    // I'm lazy to check argv type :-)
    const char* name = JS_ToCString(ctx, argv[0]);
    double height;
    JS_ToFloat64(ctx, &height, argv[1]);
    int age;
    JS_ToInt32(ctx, &age, argv[2]);
    double weight;
    JS_ToFloat64(ctx, &weight, argv[3]);

    Person* person = new Person(name, height, age, weight);
    JSValue result = JS_NewObjectClass(ctx, gClassID);
    JS_SetOpaque(result, person);
    return result;
}

注意创建类对象的方法:

  1. 使用JS_NewObjectClass创建类对象
  2. 使用JS_SetOpaque将C++对象传给JS

然后,告诉QuickJS我们需要将此prototype和哪个类相关联:

JS_SetClassProto(ctx, gClassID, proto);

最后将构造函数注册给global_this,我们就可以在JS中使用此类啦:

JSValue global_var = JS_GetGlobalObject(ctx);
JS_DefinePropertyValueStr(ctx, global_var, "Person", constructor, JS_CFUNC_constructor);
JS_FreeValue(ctx, global_var);

在JS中使用:

let person = new Person("QJSKid", 150, 15, 40)
console.log(person.name) 
person.name = "John"
console.log(person.name)
console.log(person.bmi)
person.introduce()

如何绑定类静态函数/成员?

静态函数和成员直接绑定在构造函数上即可,无需绑定在prototype上:

JSValue id_value = JS_NewInt32(ctx, Person::ID);
JS_SetPropertyStr(ctx, constructor, "ID", id_value);

使用:

console.log(Person.ID)

更多设置对象属性的方法

在之前的绑定中,我们基本上是使用JS_SetPrototypeStr进行代码绑定。但还有其他的方法。

JS_SetProperty系列

JS_SetPrototype系列有如下函数:

int JS_SetPropertyStr(JSContext *ctx, JSValueConst this_obj, const char *prop, JSValue val)
  • this_obj:要设置到的值
  • prop:设置的成员名称
  • val:要设置的值
int JS_SetProperty(JSContext *ctx, JSValueConst this_obj, JSAtom prop, JSValue val)

JS_SetPropertyStr的区别是第三个参数是JSAtom。这其实是更底层的函数。JSAtom就是一个字符串。只是如果你用JS_NewAtom创建之后你可以复用他,这样QuickJS就不用将字符串字面量在底层到处拷贝(比如使用JS_SetPropertyStr时)

JS_DefineProperty系列

这个系列函数给了你更加精细的控制:

int JS_DefineProperty(JSContext *ctx, JSValueConst this_obj, JSAtom prop, JSValueConst val, JSValueConst getter, JSValueConst setter, int flags)
int JS_DefinePropertyValue(JSContext *ctx, JSValueConst this_obj, JSAtom prop, JSValue val, int flags)
int JS_DefinePropertyValueStr(JSContext *ctx, JSValueConst this_obj, const char *prop, JSValue val, int flags)
int JS_DefinePropertyGetSet(JSContext *ctx, JSValueConst this_obj, JSAtom prop, JSValue getter, JSValue setter, int flags)

同样的,有带有JSAtom和直接使用字符串的。但都多出了int flags这个参数。取值如下:

#define JS_PROP_CONFIGURABLE  (1 << 0)
#define JS_PROP_WRITABLE      (1 << 1)
#define JS_PROP_ENUMERABLE    (1 << 2)
#define JS_PROP_C_W_E         (JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE | JS_PROP_ENUMERABLE)
#define JS_PROP_LENGTH        (1 << 3) /* used internally in Arrays */
#define JS_PROP_TMASK         (3 << 4) /* mask for NORMAL, GETSET, VARREF, AUTOINIT */
#define JS_PROP_NORMAL         (0 << 4)
#define JS_PROP_GETSET         (1 << 4)
#define JS_PROP_VARREF         (2 << 4) /* used internally */
#define JS_PROP_AUTOINIT       (3 << 4) /* used internally */
/* throw an exception if false would be returned
   (JS_DefineProperty/JS_SetProperty) */
#define JS_PROP_THROW            (1 << 14)
/* throw an exception if false would be returned in strict mode
   (JS_SetProperty) */
#define JS_PROP_THROW_STRICT     (1 << 15)
  • JS_PROP_NORMAL:默认值,无任何属性
  • JS_PROP_ENUMERABLE:属性可被枚举(可被for...in遍历到)
  • JS_PROP_WRITABLE:可写入
  • JS_PROP_CONFIGURABLE:属性的配置可被修改。如果第一次使用JS_DefinePropertyXXX未指定这个值,那之后使用JS_DefinePropertyXXX不允许修改属性的PROP。
  • JS_PROP_C_W_EJS_PROP_CONFIGURABLE,JS_PROP_WRITABLE,JS_PROP_ENUMERABLE的组合
  • JS_PROP_GETSET:标识属性是getter/setter(一般和JS_DefineProperty配合使用)。注意:
    1. 使用这个枚举的时候还需要配合JS_PROP_HAS_GET,JS_PROP_HAS_SET来告诉QuickJS是否有getter/setter
    2. 传入的getter/setter相关JSValue需要被JS_FreeValue,因为底层调用了js_dup
  • JS_PROP_THROW:如果JS_SetPropertyXXXJS_DefinePropertyXXX产生了非法行为,抛出一个异常。
  • JS_PROP_THROW_STRICT:在严格模式下判断是否非法并抛出异常

JS_SetPropertyFunctionList

可以通过这个函数一次性绑定多个属性:

const JSCFunctionListEntry entries[] = {
    // bind member function
    JS_CFUNC_DEF("introduce", 0, IntroduceBinding),
	
    // bind getter/settrer
    JS_CGETSET_DEF("name", NameGetter, NameSetter),
    JS_CGETSET_DEF("bmi", BMIBinding, nullptr),
};

JS_SetPropertyFunctionList(ctx, proto, entries, std::size(entries));

首先构造一个JSCFunctionListEntry数组,然后使用一些方便的宏就可以进行绑定。

只需要将我们的绑定函数的函数指针传给宏即可,不需要担心函数类型不一致,宏会自动帮你处理(也就不需要JSCFunctionType

最后使用JS_SetPropertyFunctionList即可完成。

缺点:JSCFunctionListEntry[]是传指针进去的。也就是说你需要保证此数组在JS_Eval之前是有效的。

删除属性

使用JS_DeleteProperty函数。

JS_SetProperty和JS_DefineProperty的区别

JS_SetPropertyXXX相当于JS中的复制。当没有这个属性的时候,赋值时会生成一份属性。但不能给const属性赋值:

JS_SetPropertyStr(ctx, global_this, "const_global_var2", new_obj); // error!

JS_DefinePropertyXXX则是定义属性。他等于是完全重新创建属性。

绑定模块

Github代码

本节介绍如何绑定模块

例子

首先需要创建模块:

JSModuleDef* module_def = JS_NewCModule(ctx, "MyModule", ModuleInitFn);

JS_NewCModule的签名如下:

JSModuleDef *JS_NewCModule(JSContext *ctx, const char *name_str, JSModuleInitFunc *func)

传入模块的名称,以及一个JSModuleInitFunc

JSModuleInitFunc是模块在初始化的时候调用的回调函数。一般在其中将属性绑定到模块上:

int ModuleInitFn(JSContext* ctx, JSModuleDef* m) {
    // set JSValue to module
    JS_SetModuleExport(ctx, m, "Add", JS_NewCFunction(ctx, AddFnBinding, "Add", 2))
    JS_SetModuleExport(ctx, m, "Person", gClassConstructor)
    // 0 - success
    // < 0 - failed
    return 0;
}

通过JS_SetModuleExportJSValue绑定在模块定义m中(也可以使用函数JS_SetModuleExportList)。

最后,我们需要指定哪些属性是需要导出的:

JS_AddModuleExport(ctx, module_def, "Add");
JS_AddModuleExport(ctx, module_def, "Person");

使用JS_AddModuleExport可以指定导出模块中的哪些属性(也可以使用JS_AddModuleExportList)。