quickjs介绍

使用方法

按照官方安装说明使用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 函数的所有变量,因此需要都加到闭包里。

7.6.1 pass 1
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值