Skip to content

使用C语言为QuickJS开发一个原生模块

llgoer edited this page Jul 31, 2019 · 1 revision

QuickJS编写原生模块的基础介绍

不久前,QEMU和FFmpeg的创建者Fabrice Bellard发布了一个全新的Javascript引擎。

这引起了我的注意,因为我是一名Javascript开发人员而且我对NodeJS内核开发很感兴趣,所以我觉得这是一个了解JS更为底层执行方式的好机会。

我知道这个引擎是在为嵌入式系统开发场景下设计创建的,非常小巧、轻便,并且实际情况是它依赖库也非常小,我试图了解它是如何工作的,主要如何扩展它。

在这里我需要跟大家解释,这是一个有C++基础的人,而不是一个专家的解决方案。

QuickJS不是另外一种NodeJS,而更像V8

我已经看到了很多混淆,例如QuickJS只是NodeJS的替代品,实际上,你不能将代码从NodeJS直接移植到QuickJS,因为NodeJS有它自己的API(fs、path、process、网络等)而QuickJS有一小部分可用的原生函数。

因此,为了提高我们对它的理解,我们将创建一个基本的C模块来为Javascript扩展更多功能。

入门

目标

我们像在最后使用QuickJS编译器编译后,使这段代码在一个独立的文件中能够运行:

import * as myModule from 'my_module'

const value = myModule.plus(1, 2)
console.log("Result:", value)

// 输出结果 => Result: 3

准备源码

你可以阅读编译安装说明,编译QuickJS。 采用make编译完源码后,尝试并运行上面的描述代码:

# compile
./qjsc -m -o my_module my_module.js
# run
./my_module

你会得到错误

ReferenceError: could not load module filename 'my_module'

将模块名称添加到编译器

编辑qjsc.c在455行左右添加我们自己的系统模块:

/ *添加系统模块* / 
namelist_add(&cmodule_list,“std”,“std”,0); 
namelist_add(&cmodule_list,“os”,“os”,0); 
//我们的模块
namelist_add(&cmodule_list,“my_module”,“my_module”,0); 

再次构建编译器并测试:

# This will build the compiler only, it's faster
make qjsc
# Test again
./qjsc -m -o my_module my_module.js

然后,将显示如下错误:

/tmp/ccenbi7V.o: In function `main’:
out19678.c:(.text.startup+0x84): undefined reference to `js_init_module_my_module’
collect2: error: ld returned 1 exit status

编译器无法构建,因为我们告诉它我们有另一个系统模块调用my_module,但找不到实现,因为我们想要一个静态链接,编译器会查找一个被调用的函数js_init_module_my_module,这个名字是在编译时动态创建的,所以,不创建二进制文件。

原生模块模板

添加以下C代码,基于QuickJS源代码中的示例创建:

#include "quickjs.h"
#include "cutils.h"

static JSValue plusNumbers(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
    int a, b;

    if (JS_ToInt32(ctx, &a, argv[0]))
        return JS_EXCEPTION;
    
    if (JS_ToInt32(ctx, &b, argv[1]))
        return JS_EXCEPTION;
        
    return JS_NewInt32(ctx, a + b);
}

static const JSCFunctionListEntry js_my_module_funcs[] = {
    JS_CFUNC_DEF("plus", 2, plusNumbers),
};

static int js_my_module_init(JSContext *ctx, JSModuleDef *m)
{
    return JS_SetModuleExportList(ctx, m, js_my_module_funcs, countof(js_my_module_funcs));
}

JSModuleDef *js_init_module_my_module(JSContext *ctx, const char *module_name)
{
    JSModuleDef *m;
    m = JS_NewCModule(ctx, module_name, js_my_module_init);
    
    if (!m)
        return NULL;
    
    JS_AddModuleExportList(ctx, m, js_my_module_funcs, countof(js_my_module_funcs));
    return m;
}

my_module.c 正如您所看到的那样*js_init_module_my_module是模块的入口点,js_my_module_init是初始化函数。函数列表js_my_module_funcs和函数本身plusNumbers,唯一的强制函数名称是入口点,因为它是根据描述的模块名称动态生成的qjsc.c,它具有有这种格式:*js_init_module_[MODULE_NAME]

要向我们的模块添加更多功能,只需扩展JS_CFUNC_DEF,它会等待3个参数,函数名称,参数数量和函数定义。

该函数plusNumbers必须返回一个Javascript支持的JS_Value抽象类型,这个结构被声明,在quickjs.h那里你会找到所有其他结构可用,在这种情况下我们返回JS_NewInt32表示数字的结构。

为了读取参数,我们使用以下行:

int a;
JS_ToInt32(ctx, &a, argv[0])

这里我们将JS数值转换为C数值,方法是将变量作为参考传递并从argv数组中读取即将到来的参数。

将C代码添加到Makefile

由于我们正在尝试扩展QuickJS编译器,我们必须将它作为依赖项添加到Makefile中: 在结尾添加一个新对象QJS_LIB_OBJS:

QJS_LIB_OBJS= ... $(OBJDIR)/my_module.o

因此,下次我们构建编译器时,它将确保目标./my_module.o存在,如果没有,它将被编译,已经有一个目标用于编译源目录中的所有C代码。

所以,最终的测试将是:

# build the compiler
make qjsc
# compile our example code
./qjsc -m -o my_module my_module.js
# run the program
./my_module
output => Result 5

至此,我们有一个3090416字节(3~MB)大小的独立可执行文件,它不是很大,但它可以通过flto标志进行优化,更小更快!

./qjsc -flto -m -o my_module my_module.js

现在我们有一个652~KB的二进制文件,程序使用了ES2020编码!

编码快乐!:)

原文:https://medium.com/@calbertts/writing-native-modules-in-c-for-quickjs-engine-49043587f2e2

翻译:https://github.com/quickjs-zh/QuickJS/