介绍
什么是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
。
包含头文件
首先要做的是包含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
:以何种方式执行。这里是全局执行,也是默认方式
这个函数会执行整个脚本,并返回一个值。因为我们的脚本内没有返回任何值,所以这里的result
是JS_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标准库
使用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_loop
或js_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
预加载模块,然后执行
我们也可以走两步:
- 先通过模块模式
import
需要的模块入JSContext
- 执行我们自己的代码。这样可以直接使用已经导入的模块
这里首先以模块模式执行:
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
绑定全局变量
我们首先从最简单的全局变量绑定开始。
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
进行的。这里步骤如下:
- 首先使用
JS_NewInt32
创建一个整数类型的JSValue
- 做异常检查
- 得到全局对象
JS_GetGlobalObject
- 使用
JS_SetPropertyStr
将我们的变量注册到全局对象中 - 清理内存
重点是注册的函数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)
绑定基础类型变量的规则
绑定对象的步骤如下:
- 使用
JS_NewXXX
来创建一个JS对象 - 使用
JS_SetPropertyStr
来将对象绑定在另一个JS对象中 - 清理内存
可创建的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_id
,class_id
是类的唯一标识,在你注册类的时候会生成一个(等待后面绑定类的时候会说到)。
然后使用JS_SetOpaque
函数将我们的类对象塞给JSValue
即可。
绑定空对象
使用JS_NewObject
创建一个空对象(即创建一个JS中的Object
实例)。这个对象不和任何类相关联,所以也不需要一个class_id
。
内置JSValue常量
JS_UNDEFINED
JS_NULL
这两个是内置的字面常量,可以直接使用无需JS_NewXXX
。
绑定全局函数
本节介绍了如何绑定全局函数
例子
假设我们有一个简单的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_getter
和JS_CFUNC_setter
:getter和setterJS_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_f
和JS_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
类型。要如何绑定呢?有两种方法:
-
使用特定的绑定函数,比如magic相关的就是
JS_NewCFunctionMagic
,getter/setter就是JS_DefinePropertyGetSet()
-
使用
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);
绑定类
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_GetOpaque
从JSValue
中拿到特定类的指针(注意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;
}
注意创建类对象的方法:
- 使用
JS_NewObjectClass
创建类对象 - 使用
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_E
:JS_PROP_CONFIGURABLE
,JS_PROP_WRITABLE
,JS_PROP_ENUMERABLE
的组合JS_PROP_GETSET
:标识属性是getter/setter(一般和JS_DefineProperty
配合使用)。注意:- 使用这个枚举的时候还需要配合
JS_PROP_HAS_GET
,JS_PROP_HAS_SET
来告诉QuickJS是否有getter/setter - 传入的getter/setter相关
JSValue
需要被JS_FreeValue
,因为底层调用了js_dup
- 使用这个枚举的时候还需要配合
JS_PROP_THROW
:如果JS_SetPropertyXXX
或JS_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
则是定义属性。他等于是完全重新创建属性。
绑定模块
本节介绍如何绑定模块
例子
首先需要创建模块:
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_SetModuleExport
将JSValue
绑定在模块定义m
中(也可以使用函数JS_SetModuleExportList
)。
最后,我们需要指定哪些属性是需要导出的:
JS_AddModuleExport(ctx, module_def, "Add");
JS_AddModuleExport(ctx, module_def, "Person");
使用JS_AddModuleExport
可以指定导出模块中的哪些属性(也可以使用JS_AddModuleExportList
)。