Live2D

关于我对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的效率。甚至可以和原生媲美

posted @ 2020-09-14 01:00  二胖he鼠标  阅读(461)  评论(0编辑  收藏  举报