-
Notifications
You must be signed in to change notification settings - Fork 301
使用C语言为QuickJS开发一个原生模块
为QuickJS编写原生模块的基础介绍
不久前,QEMU和FFmpeg的创建者Fabrice Bellard发布了一个全新的Javascript引擎。
这引起了我的注意,因为我是一名Javascript开发人员而且我对NodeJS内核开发很感兴趣,所以我觉得这是一个了解JS更为底层执行方式的好机会。
我知道这个引擎是在为嵌入式系统开发场景下设计创建的,非常小巧、轻便,并且实际情况是它依赖库也非常小,我试图了解它是如何工作的,主要如何扩展它。
在这里我需要跟大家解释,这是一个有C++基础的人,而不是一个专家的解决方案。
我已经看到了很多混淆,例如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数组中读取即将到来的参数。
由于我们正在尝试扩展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