QuickJS 源码解读(二):基础设施和标准库

上一篇文章 里面主要解释了 了 QuickJS 虚拟机的运作。第二篇文章打算介绍一下 QuickJS 里面 JavaScript 基础设施的实现。

基础设施

注意,使用 QuickJS 新建 JSContext 的时候,默认是不带基础设施的 (比如说 JSON 解析、Object、等等)。 这时候,可以调用以下命令进行添加,这些内建对象的支持都是内置的:

void JS_AddIntrinsicBaseObjects(JSContext *ctx);
void JS_AddIntrinsicDate(JSContext *ctx);
void JS_AddIntrinsicEval(JSContext *ctx);
void JS_AddIntrinsicStringNormalize(JSContext *ctx);
void JS_AddIntrinsicRegExpCompiler(JSContext *ctx);
void JS_AddIntrinsicRegExp(JSContext *ctx);
void JS_AddIntrinsicJSON(JSContext *ctx);
void JS_AddIntrinsicProxy(JSContext *ctx);
void JS_AddIntrinsicMapSet(JSContext *ctx);
void JS_AddIntrinsicTypedArrays(JSContext *ctx);
void JS_AddIntrinsicPromise(JSContext *ctx);
void JS_AddIntrinsicBigInt(JSContext *ctx);
void JS_AddIntrinsicBigFloat(JSContext *ctx);
void JS_AddIntrinsicBigDecimal(JSContext *ctx);

至于这些内置方法,比如 Object 是怎么实现,我们可以直接点开 JS_AddIntrinsicBaseObjects 代码

/* Object */
obj = JS_NewGlobalCConstructor(ctx, "Object", js_object_constructor, 1,
                                ctx->class_proto[JS_CLASS_OBJECT]);
JS_SetPropertyFunctionList(ctx, obj, js_object_funcs, countof(js_object_funcs));
JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_OBJECT],
                            js_object_proto_funcs, countof(js_object_proto_funcs));

这两行分别给 Object 和 Object.prototype 设置了相应的函数, 函数的定义在 js_object_proto_funcs 这个静态变量里面。

我们可以跟踪看看 js_object_proto_funcs 分别定义了什么函数:

static const JSCFunctionListEntry js_object_proto_funcs[] = {
    JS_CFUNC_DEF("toString", 0, js_object_toString ),
    JS_CFUNC_DEF("toLocaleString", 0, js_object_toLocaleString ),
    JS_CFUNC_DEF("valueOf", 0, js_object_valueOf ),
    JS_CFUNC_DEF("hasOwnProperty", 1, js_object_hasOwnProperty ),
    JS_CFUNC_DEF("isPrototypeOf", 1, js_object_isPrototypeOf ),
    JS_CFUNC_DEF("propertyIsEnumerable", 1, js_object_propertyIsEnumerable ),
    JS_CGETSET_DEF("__proto__", js_object_get___proto__, js_object_set___proto__ ),
    JS_CFUNC_MAGIC_DEF("__defineGetter__", 2, js_object___defineGetter__, 0 ),
    JS_CFUNC_MAGIC_DEF("__defineSetter__", 2, js_object___defineGetter__, 1 ),
    JS_CFUNC_MAGIC_DEF("__lookupGetter__", 1, js_object___lookupGetter__, 0 ),
    JS_CFUNC_MAGIC_DEF("__lookupSetter__", 1, js_object___lookupGetter__, 1 ),
};

可以看到这些内置方法都是 C 实现的方法。对应的函数都是在 quickjs.c 里面定义。

知道了这些,我们就可以动手修改 QuickJS 代码,比如说我们想改变 Object 的 toString 的表现。让它输出更详细的信息,那我们更改 js_object_toString 的实现就可以了。

标准库

标准库的实现,不是语言的一部分。这一部分的实现内容放在了 quickjs-libc.c 这个文件。 这里稍微提一句,Bellard 实现标准库用了不少 POSIX 方法,这样实现起来代码会比较简单,但是 同时导致了代码无法在 Windows 上编译(除非用 MingW),所以想要在 Windows 上独立编译通过的话,需要做不少改动。

我自己也做了一份 Fork,修复了 Windows 的编译问题,同时支持 CMake: https://github.com/vincentdchan/quickjs

需要的可以自取。

console.log

写过 JS 的人估计都用过 console.log 吧。那么在 QuickJS 里面,console.log 怎么实现呢。 答案就在 quickjs-libc.c 这个文件的 js_std_add_helpers 这个函数里面:

JSValue global_obj, console, args;
int i;

global_obj = JS_GetGlobalObject(ctx);
console = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, console, "log",
                  JS_NewCFunction(ctx, js_print, "log", 1));
JS_SetPropertyStr(ctx, global_obj, "console", console);

可以看到实现方法就是 js_print 这个函数:

static JSValue js_print(JSContext *ctx, JSValueConst this_val,
                              int argc, JSValueConst *argv)
{
    int i;
    const char *str;

    for(i = 0; i < argc; i++) {
        if (i != 0)
            putchar(' ');
        str = JS_ToCString(ctx, argv[i]);
        if (!str)
            return JS_EXCEPTION;
        fputs(str, stdout);
        JS_FreeCString(ctx, str);
    }
    putchar('\n');
    return JS_UNDEFINED;
}

可以看到底层调用的是 C 语言的 fputs 方法输出到 stdout。 如果你想让 console.log 输出到自己的文件,或者数据库,那么你就可以更改 js_print 这个方法了。

我们还可以看到,Bellard 只实现了 console.log,但是没有输出 console.error。 那么我们就可以把 console.error 给实现上,输出到 stderr,也是轻而易举了。

setTimout

setTimeout 的实现会稍微复杂一点。要了解 setTimeout 的实现,就要了解 QuickJS 的 事件循环。要了解 QuickJS 的事件循环,其实只要看懂一个函数:

/* 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;
    }
}

QuickJS 里面维护着一个队列。而主循环就是一直从这个队列里面捞任务出来进行处理。 JS_ExecutePendingJob 就是执行一个 JS 任务。这个 JS 任务可能添加了一个系统任务。 比如如果代码调用了 setTimeout 那么 QuickJS 会往系统添加一个定时器任务。

随后这个循环调用 os_poll_func 这个方法,会一直阻塞,等到有任务完成, 这个函数会往 QuickJS 队列添加一个回调,然后返回。

返回后进入下一次循环,就会执行 setTimeout 的回调,这样就完成了 setTimeout 的调用。

这里和我们熟知的 v8 不一样,v8 使用的是 libuv 作为事件循环的库。 而 QuickJS 为了轻量化,简单的封装了一下系统的信号,有兴趣的同学可以深入了解 os_poll_func 函数的实现。