使用方法
按照官方安装说明使用makefile安装后,命令行工具会被安装到/usr/local/bin目录下,此目录下会有JS解释器qjs,有编译器qjsc(QuickJS compiler,将js文件编译为可执行文件,具体实现是将QuickJS引擎+JS文件打包,使用qjs解释执行目标JS文件),还有一个可以对任意长度数字计算的qjscalc。编译的库会放到/usr/local/lib/quickjs目录下,有静态库libquickjs.a,可以生成更小和速度更快的库libquickjs.lto.a(lto即Link Time Optimization,使用它需要在编译时加上-flto标识)。
使用qjsc将js文件编译为可执行文件,编写JavaScript代码如下:
let myString1="Hello";
let myString2="World";
console.log(myString1+" "+myString2);
使用如下命令进行编译:
qjsc -o hello helloworld.js
qjsc还可以把js文件编译成.c文件,还是以上面的JavaScript代码为例:
qjsc -e -o helloworld.c helloworld.js
文件内容如下:
/* File generated automatically by the QuickJS compiler. */
#include "quickjs-libc.h"
const uint32_t qjsc_helloworld_size = 165;
const uint8_t qjsc_helloworld[165] = {
0x02, 0x08, 0x12, 0x6d, 0x79, 0x53, 0x74, 0x72,
0x69, 0x6e, 0x67, 0x31, 0x12, 0x6d, 0x79, 0x53,
0x74, 0x72, 0x69, 0x6e, 0x67, 0x32, 0x0a, 0x48,
0x65, 0x6c, 0x6c, 0x6f, 0x0a, 0x57, 0x6f, 0x72,
0x6c, 0x64, 0x0e, 0x63, 0x6f, 0x6e, 0x73, 0x6f,
0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x67, 0x02, 0x20,
0x1a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f,
0x72, 0x6c, 0x64, 0x2e, 0x6a, 0x73, 0x0e, 0x00,
0x06, 0x00, 0xa0, 0x01, 0x00, 0x01, 0x00, 0x04,
0x00, 0x00, 0x4c, 0x01, 0xa2, 0x01, 0x00, 0x00,
0x00, 0x3f, 0xe1, 0x00, 0x00, 0x00, 0x80, 0x3f,
0xe2, 0x00, 0x00, 0x00, 0x80, 0x3e, 0xe1, 0x00,
0x00, 0x00, 0x82, 0x3e, 0xe2, 0x00, 0x00, 0x00,
0x82, 0x04, 0xe3, 0x00, 0x00, 0x00, 0x3a, 0xe1,
0x00, 0x00, 0x00, 0x04, 0xe4, 0x00, 0x00, 0x00,
0x3a, 0xe2, 0x00, 0x00, 0x00, 0x38, 0xe5, 0x00,
0x00, 0x00, 0x42, 0xe6, 0x00, 0x00, 0x00, 0x38,
0xe1, 0x00, 0x00, 0x00, 0x04, 0xe7, 0x00, 0x00,
0x00, 0x9d, 0x38, 0xe2, 0x00, 0x00, 0x00, 0x9d,
0x24, 0x01, 0x00, 0xcd, 0x28, 0xd0, 0x03, 0x01,
0x04, 0x3d, 0x3f, 0x35, 0x36,
};
static JSContext *JS_NewCustomContext(JSRuntime *rt)
{
JSContext *ctx = JS_NewContextRaw(rt);
if (!ctx)
return NULL;
JS_AddIntrinsicBaseObjects(ctx);
JS_AddIntrinsicDate(ctx);
JS_AddIntrinsicEval(ctx);
JS_AddIntrinsicStringNormalize(ctx);
JS_AddIntrinsicRegExp(ctx);
JS_AddIntrinsicJSON(ctx);
JS_AddIntrinsicProxy(ctx);
JS_AddIntrinsicMapSet(ctx);
JS_AddIntrinsicTypedArrays(ctx);
JS_AddIntrinsicPromise(ctx);
JS_AddIntrinsicBigInt(ctx);
return ctx;
}
int main(int argc, char **argv)
{
JSRuntime *rt;
JSContext *ctx;
rt = JS_NewRuntime();
js_std_set_worker_new_context_func(JS_NewCustomContext);
js_std_init_handlers(rt);
JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL);
ctx = JS_NewCustomContext(rt);
js_std_add_helpers(ctx, argc, argv);
js_std_eval_binary(ctx, qjsc_helloworld, qjsc_helloworld_size, 0);
js_std_loop(ctx);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
上面的代码中,长度为165的数组qjsc_helloworld记录的就是quickjs编译生成的字节码。js_std_loop就是Event Loop处理的函数,是调用用户js回调的主循环,js_std_loop函数实现代码很简洁,如下:
/* main loop which calls the user JS callbacks */
void js_std_loop(JSContext *ctx)
{
JSContext *ctx1;
int err;
for(;;) {
/* execute the pending jobs */
for(;;) {
err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1);
if (err <= 0) {
if (err < 0) {
js_std_dump_error(ctx1);
}
break;
}
}
if (!os_poll_func || os_poll_func(ctx))
break;
}
}
上面代码中的os_poll_func就是js_os_poll函数的调用,js_os_poll函数在quickjs-libc.c里定义,会在主线程检查有没有需要执行的任务,没有的话会在后台等待事件执行。
简单说QuickJS集成使用过程是先将QuickJS源码编译成静态或动态库。makefile会把头文件、库文件和可执行文件copy到标准目录下。然后C源码调用QuickJS提供的API头文件。最后编译生成可执行文件。
那么js和原生c的交互如何进行呢?
调用原生函数
在QuickJS的js代码里可以通过import导入一个c的库,调用库里的函数。比如QuickJS中的fib.c文件,其函数js_fib就是对js里可调用的fib函数的实现,使用的是JS_CFUNC_DEF宏来做js方法和对应c函数的映射。映射代码如下:
static const JSCFunctionListEntry js_fib_funcs[] = {
JS_CFUNC_DEF("fib", 1, js_fib ),
};
此时我们就可以在js中通过如下的方式进行调用:
import { fib } from "./fib.so";
var f = fib(10);
quickjs-libc内置了些std和os原生函数可以直接供js使用,比如std.out.printf函数,看下js_std_file_proto_funcs里的映射代码:
JS_CFUNC_DEF("printf", 1, js_std_printf ),
可以看到对应的是js_std_printf函数,JS_CFUNC_DEF宏的第二个参数为1表示out,在js里使用的代码如下:
import * as std from 'std'
const hi = 'hi'
std.out.printf('%s', hi)
如何新建一个自己的库呢?QuickJS里的fib就是个例子,通过生成的test_fib.c可以看到下面的代码:
待补充
源码文件介绍
首先对QuickJS源码文件进行介绍,主要文件如下:
- quickjs.c和quickjs.h:QuickJS的核心代码,其他文件基本都会依赖于它
- quickjs-lib.c和quicklib.h:调用QuickJS接口API,供C程序使用
- quickjs-atom.h:定义了js的关键字原子字符串
- quick-opcode.h:定义了字节码操作符
文件夹:
- examples/:一些js代码的示例,包含了和c交互的js代码
- tests/:测试QuickJS核心功能
- doc/:QuickJS官方使用说明文档
一些可执行程序相关的文件:
- qjsc.c:编译完生成可执行文件qjsc,是QuickJS的JavaScript编译器
- qjs.c:可交互解释执行的程序qjs,是QuickJS的解释器。qjs具有栈虚拟机内核,通过读取js文件解释执行,函数调用链是main->eval_file->eval_buf->JS_EvalFunction->JS_EvalFunctionInternal->JS_CallFree->JS_CallInternal
- repl.js:js写的REPL程序
- qjscalc.js:一个计算器程序
其中qjsc.c会根据参数输入,挨个调用compile_file函数进行编译。参数说明如下:
(pytorch_wgy) wangguoyu910@910-3090-2:~/github/test_c$ /usr/local/bin/qjsc -h
QuickJS Compiler version 2021-03-27
usage: qjsc [options] [files]
options are:
-c only output bytecode in a C file
-e output main() and bytecode in a C file (default = executable output)
-o output set the output filename
-N cname set the C name of the generated data
-m compile as Javascript module (default=autodetect)
-D module_name compile a dynamically loaded module or worker
-M module_name[,cname] add initialization code for an external C module
-x byte swapped output
-p prefix set the prefix of the generated C names
-S n set the maximum stack size to 'n' bytes (default=262144)
-flto use link time optimization
-fbignum enable bignum extensions
-fno-[date|eval|string-normalize|regexp|json|proxy|map|typedarray|promise|module-loader|bigint]
disable selected language features (smaller code size)
待补充
QuickJS架构
QuickJS的架构图如下:
如上图所示,最上层是qjs和qjsc,qjs包含了命令行参数处理,引擎环境创建,加载模块js文件读取解释执行。qjsc可以编译js文件成字节码文件,生成的字节码可以直接被解释执行,性能上看是省掉了js文件解析的消耗。
中间层是核心,JSRuntime是js运行时,可以看做是js虚拟机环境,多个JSRuntime之间是隔离的,他们之间不能相互调用和通信。JSContext是虚拟机里的上下文环境,一个JSRuntime里可以有多个JSContext,每个上下文有自己的全局和系统对象,不同JSContext之间可以相互访问和共享对象。JS_Eval和JS_Parse会把js文件编译为字节码。JS_Call是用来解释执行字节码的。JS_OPCode是用来标识执行指令对应的操作符。JSClass包含标准的js的对象,是运行时创建的类,对象类型使用JSClassID来标记,使用JS_NewClassID和JS_NewClass函数注册,使用JS_NewObjectClass来创建对象。JSOpCode是字节码的结构体,通过quickjs-opcode.h里的字节码的定义可以看到,QuickJS对于每个字节码的大小都会精确控制,目的是不浪费内存使用,比如8位以下用不到一个字节和其他信息一起放在那一个字节里,8位用两个字节,整数在第二个字节里,16位用后两个字节。Unicode是QuickJS自己做的库libunicode.c,libunicode有Unicode规范化和脚本通用类别查询,包含所有Unicode二进制属性,libunicode还可以单独出来用在其他工程中。中间层还包含科学计算BigInt和BIgFloat的libbf库,正则表达式引擎libregexp。扩展模块Std Module和OS Module,提供标准能力和系统能力,比如文件操作和时间操作等。
底层是基础,JS_RunGC使用引用技术来管理对象的释放。JS_Exception是会把JSValue返回的异常对象存在JSContext里,通过JS_GetException函数取出异常对象。内存管理控制JS运行时内存分配上限使用的是JS_SetMemoryLimit函数,自定义分配内存用的是JS_NewRuntime2函数,堆栈的大小使用的是JS_SetMaxStackSize函数进行设置。
7 QuickJS核心代码流程
quickjs.c有五万多行代码。我们可以通过QuickJS解析执行JS代码的过程出发来分析QuickJS源码:
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
js_std_add_helpers(ctx, 0, NULL);
const char *scripts = "console.log('hello quickjs')";
JS_Eval(ctx, scripts, strlen(scripts), "main", 0);
上面代码中rt和ctx是先构造一个JS运行时和上下文环境,js_std_add_helpers是调用C的std方法帮助在控制台输出调试信息。
7.1 新建Runtime
除了创建一般的函数对象外,还有一种避开繁琐字节码处理进而更快创建函数对象的方法,这就是Shape。创建Shape的调用链是JS_NewCFunctionData->JS_NewObjectProtoClass->js_new_shape。创建shape的函数是js_new_shape2,创建对象调用的函数是JS_NewObjectFromShape。
创建的对象是JSObject结构体,JSObject是js的对象,JSObject的字段会使用union,结构体和union的区别是结构体的字段之间会有自己的内存,而union里的字段会使用相同的内存,union内存就是里面占用最多内存字段的内存。union使用的内存覆盖方式,只能有一个字段的值,每次有新字段复制都会覆盖先前的字段值。JSObject里第一个union是用到引用计数的,__gc_ref_count用来计数,__gc_mark用来描述当前GC的信息,值为JSGCObjectTypeEnum枚举。extensible是表示对象是否能扩展。is_exotic记录对象是否是exotic对象,es规范里定义只要不是普通的对象都是exotic对象,比如数组创建的实例就是exotic对象。fast_array为true用于JS_CLASS_ARRAY、JS_CLASS_ARGUMENTS和类型化数组这样只会用到get和put基本操作的数组。如果对象是构造函数is_constructor为true。当is_uncatchable_error字段为true时表示对象的错误不可捕获。class_id对应的是JS_CLASS打头的枚举值,这些枚举值定义了类的类型。原型和属性的名字还有flag记在shape字段,存属性的数组记录在prop字段。
first_weak_ref 指向第一次使用这个对象做键的 WeakMap 地址。在 js 中 WeakMap 的键必须要是对象,Map 的键可以是对象也可以是其他类型,当 Map 的键是对象时会多一次对对象引用的计数,而 WeakMap 则不会,WeakMap没法获取所有键和所有值。键使用对象主要是为了给实例存储一些额外的数据,如果使用 Map 的话释放对象时还需要考虑 Map 对应键和值的删除,维护起来不方便,而使用 WeakMap,当对象在其他地方释放完后对应的 WeakMap 键值就会被自动清除掉。
JS_ClASS 开头定义的类对象使用的是 union,因为一个实例对象只可能属于一种类型。其中 JS_CLASS_BOUND_FUNCTION 类型对应的结构体是 JSBoundFunction,JS_CLASS_BOUND_FUNCTION 类型是使用 bind() 方法创建的函数,创建的函数的 this 被指定是 bind() 的第一个参数,bind 的其他参数会给新创建的函数使用。JS_CLASS_C_FUNCTION_DATA 这种类型的对象是 QuickJS 的扩展函数,对应结构体是 JSCFunctionDataRecord。JS_CLASS_FOR_IN_ITERATOR 类型对象是 for…in 创建的迭代器函数,对应的结构体是 JSForInIterator。
JS_CLASS_ARRAY_BUFFER 表示当前对象是 ArrayBuffer 对象,ArrayBuffer 是用来访问二进制数据,比如加快数组操作,还有媒体和网络 Socket 的二进制数据,ArrayBuffer 对应 swift 里的 byte array,swift 的字符串类型是基于 Unicode scalar 值构建的,一个 Unicode scalar 是一个21位数字,用来代表一个字符或修饰符,比如 U+1F600 对应的修饰符是 😀,U+004D 对应的字符是 M。因此 Unicode scalar 是可以由 byte array 来构建的。byte array 和字符之间也可以相互转换,如下面的 swift 代码:
待补充
7.2 新建上下文
还是以如下代码段为例进行说明:
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
js_std_add_helpers(ctx, 0, NULL);
const char *scripts = "console.log('hello quickjs')";
JS_Eval(ctx, scripts, strlen(scripts), "main", 0);
JS_NewContext函数会通过JS_NewContextRaw初始化一个上下文。JSContext结构体里面包含了GC对象的header,JSRuntime,对象数量和大小,shape的数组,全局对象global_obj和全局变量包括let/const的定义。与big number相关的bf_ctx,rt->bf_ctx是指针,共享所有上下文。fp_env是全局FP环境。通过bignum_ext来控制是否开启数学模式。all_operator_overload来控制是否开启operator overloading。当计数器为0后,JSRuntime.interrupt_handler就会被调用。loaded_modules是JSModuleDef.linkd1列表。compile_regexp函数如果返回的JSValue是NULL表示输入正则表达式的模式不被支持。eval_internal函数如果返回NULL表示输入的字符串或者文件eval没法执行。
待补充
JS_Eval
JS_Eval方法就是执行JS脚本的入口方法。JS_Eval里调用情况如下:
JS_Eval->JS_EvalThis->JS_EvalInternal->eval_internal(__JS_EvalInternal)
实际起作用的是 __JS_EvalInternal 函数,内部会先声明整个脚本解析涉及的内容,比如 JSParseState s、函数对象和返回值 fun_obj、栈帧 sf、变量指针 var_refs、函数字节码 b、函数 fd、模块 m 等。通过函数 js_parse_init 来设置初始化 JSParseState。有上下文 ctx、文件名 filename、根据输入 input 字符串长度设置缓存大小和初始化 token 等。
待补充
创建顶级函数定义
js_new_function_def 会通过运行 JS_NewAtom 函数,来返回一个文件名 JSAtom。JS_NewAtom 函数调用链如下:
JS_NewAtom -> JSNewAtomLen -> JS_NewAtomStr -> __JS_NewAtom
js解析和生成字节码
js_create_function创建函数对象及包含的子函数
js_create_function 函数会从 JSFunctionDef 中创建一个函数对象和子函数,然后释放 JSFunctionDef。开始会重新计算作用域的关联,通过四步将作用域和变量关联起来,方便在作用域和父作用域中查找变量。第一步遍历函数定义里的作用域,设置链表头。第二步遍历变量列表,将变量对应作用域层级链表头放到变量的 scope_next 里,并将变量所在列表索引放到变量对应作用域层级链表头里。第三步再遍历作用域,将没有变量的作用域指向父作用域的链表。第四步将当前作用域和父作用域变量链表连起来。通过 fd->has_eval_call 来看是否有调用 eval,如果有,通过 add_eval_variables 来添加 eval 变量。如果函数里有 eval 调用,add_eval_variables 函数会将 eval 里的闭合变量按照作用域排序。add_eval_variables 函数会为 eval 定义一个给参数作用域用的额外的变量对象,还有需定义可能会使用的 arguments,还在参数作用域加个 arguments binding,另外,eval 可以使用 enclosing 函数的所有变量,因此需要都加到闭包里。