关于我对js运行时的一些浅薄的理解:(1,实现简单的原生编译)
运行时是什么?
runtime(运行时)在计算机世界中并不少见,或者说基本上所有的语言都需要一个运行时。计算机的能力,比如操作系统的网络,io操作,文件系统等能力,单纯的使用js是无法运用的。js本声就是一门很简单的解释型脚本而已。他并没有能力触碰操作系统。这也就是我们常常把js比作一把无比华丽的宝剑,但是却找不到剑柄。
而js目前的主要运行时有两个,浏览器和nodejs
怎么去实现简单的运行时
这里有一个著名的轮子叫quickjs
怎么样可以触碰到底层呢?这个问题和怎么把大象装到冰箱里一样。其实很简单,js的解释器,也就是我们所说的引擎,对于c++(c)开发者来说和其他的库没有什么不同
简单的说就是
- 将引擎的源码编译成库文件
- 编到头文件include一下
- 编译自己的c源码,经过编译,连接,装载。。。(开始背书)
那么quickjs就是做的这件事,只需要一行make&&sudo make install就可以生成可执行的c代码,就这么简单,而quickjs做的事也很简单,就是我们上面说的,把js代码转换成机器码喂给js引擎而已,然后把他们的文件结构整理成unix规范
比如我们用quickjs把下面的js代码转换成c
js console.log("Hello World");
#include <quickjs/quickjs-libc.h>
const uint32_t qjsc_hello_size = 87;
const uint8_t qjsc_hello[87] = {
0x02, 0x04, 0x0e, 0x63, 0x6f, 0x6e, 0x73, 0x6f,
0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x67, 0x16, 0x48,
0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72,
0x6c, 0x64, 0x22, 0x65, 0x78, 0x61, 0x6d, 0x70,
0x6c, 0x65, 0x73, 0x2f, 0x68, 0x65, 0x6c, 0x6c,
0x6f, 0x2e, 0x6a, 0x73, 0x0e, 0x00, 0x06, 0x00,
0x9e, 0x01, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00,
0x14, 0x01, 0xa0, 0x01, 0x00, 0x00, 0x00, 0x39,
0xf1, 0x00, 0x00, 0x00, 0x43, 0xf2, 0x00, 0x00,
0x00, 0x04, 0xf3, 0x00, 0x00, 0x00, 0x24, 0x01,
0x00, 0xd1, 0x28, 0xe8, 0x03, 0x01, 0x00,
};
int main(int argc, char **argv)
{
JSRuntime *rt;
JSContext *ctx;
rt = JS_NewRuntime();
ctx = JS_NewContextRaw(rt);
JS_AddIntrinsicBaseObjects(ctx);
js_std_add_helpers(ctx, argc, argv);
js_std_eval_binary(ctx, qjsc_hello, qjsc_hello_size, 0);
js_std_loop(ctx);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
所以并没有什么黑魔法,就只是拿胶水站在一起而已。。。。
现在我们已经实现了一个可以在原生环境运行js的这么一个东西了。当然他现在只认识原生js,任何触摸到操作系统的事情都做不了,settimeout也不行(要用到时钟)。
我们把上面生成的c代码编译运行之后就可以在控制台看到“hello world”
有了这个工具我们可以做什么呢?比如说一些计算密集型的任务,就可以直接交给c去做
要想在 QuickJS 引擎中使用上面这个 C 函数,大致要做这么几件事:
把 C 函数包一层,处理它与 JS 引擎之间的类型转换。
将包好的函数挂载到 JS 模块下。
将整个原生模块对外提供出来。
这一共只要约 30 行胶水代码就够了,相应的 fib.c 源码如下所示:
#include <quickjs/quickjs.h>
#define countof(x) (sizeof(x) / sizeof((x)[0]))
// 原始的 C 函数
static int fib(int n) {
if (n <= 0) return 0;
else if (n == 1) return 1;
else return fib(n - 1) + fib(n - 2);
}
// 包一层,处理类型转换
static JSValue js_fib(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) {
int n, res;
if (JS_ToInt32(ctx, &n, argv[0])) return JS_EXCEPTION;
res = fib(n);
return JS_NewInt32(ctx, res);
}
// 将包好的函数定义为 JS 模块下的 fib 方法
static const JSCFunctionListEntry js_fib_funcs[] = {
JS_CFUNC_DEF("fib", 1, js_fib ),
};
// 模块初始化时的回调
static int js_fib_init(JSContext *ctx, JSModuleDef *m) {
return JS_SetModuleExportList(ctx, m, js_fib_funcs, countof(js_fib_funcs));
}
// 最终对外的 JS 模块定义
JSModuleDef *js_init_module_fib(JSContext *ctx, const char *module_name) {
JSModuleDef *m;
m = JS_NewCModule(ctx, module_name, js_fib_init);
if (!m) return NULL;
JS_AddModuleExportList(ctx, m, js_fib_funcs, countof(js_fib_funcs));
return m;
}
上面这个 fib.c 文件只要加入 CMakeLists.txt 中的 add_executable 项中,就可以被编译进来使用了。这样在原本的 main.c 入口里,只要在 eval JS 代码前多加两行初始化代码,就能准备好带有原生模块的 JS 引擎环境了:
// ...
int main(int argc, char **argv)
{
// ...
// 在 eval 前注册上名为 fib.so 的原生模块
extern JSModuleDef *js_init_module_fib(JSContext *ctx, const char *name);
js_init_module_fib(ctx, "fib.so");
// eval JS 字节码
js_std_eval_binary(ctx, qjsc_hello, qjsc_hello_size, 0);
// ...
}
这样,我们就能用这种方式在 JS 中使用 C 模块了:
import { fib } from "fib.so";
fib(42);
运行时间:
js | c | v8 |
---|---|---|
42ms | 2 ms | 3.5ms |
(这个实验我没做,看的别人的 |
所以jit是一个很伟大的事,他大幅提高了js的效率。甚至可以和原生媲美