深圳幻海软件技术有限公司 欢迎您!

JS运行时Just源码解读

2023-02-27

1模块的设计1.1C++模块1.2内置JS模块1.3普通JS模块1.4Addon2事件循环3初始化4总结1模块的设计像Node.js一样,Just也分为内置JS和C++模块,同样是在运行时初始化时会处理相关的逻辑。1.1C++模块Node.js在初始化时,会把C++模块组织成一个链表,然后加载的时候
  • 1 模块的设计
    • 1.1 C++模块
    • 1.2 内置JS模块
    • 1.3 普通JS模块
    • 1.4 Addon
  • 2 事件循环
  • 3 初始化
  • 4 总结

1 模块的设计

像Node.js一样,Just也分为内置JS和C++模块,同样是在运行时初始化时会处理相关的逻辑。

1.1 C++模块

Node.js在初始化时,会把C++模块组织成一个链表,然后加载的时候通过模块名找到对应的模块配置,然后执行对应的钩子函数。Just则是用C++的map来管理C++模块。目前只有五个C++模块。

just::modules["sys"] = &_register_sys; 
just::modules["fs"] = &_register_fs; 
just::modules["net"] = &_register_net; 
just::modules["vm"] = &_register_vm; 
just::modules["epoll"] = &_register_epoll; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

Just在初始化时就会执行以上代码建立模块名称到注册函数地址的关系。我们看一下C++模块加载器时如何实现C++模块加载的。

// 加载C++模块 
function library (name, path) { 
  // 有缓存则直接返回 
  if (cache[name]) return cache[name
  // 调用 
  const lib = just.load(name
  lib.type = 'module' 
  // 缓存起来 
  cache[name] = lib 
  return lib 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

just.load是C++实现的。

void just::Load(const FunctionCallbackInfo<Value> &args) { 
  Isolate *isolate = args.GetIsolate(); 
  Local<Context> context = isolate->GetCurrentContext(); 
  // C++模块导出的信息 
  Local<ObjectTemplate> exports = ObjectTemplate::New(isolate); 
  // 加载某个模块 
  if (args[0]->IsString()) { 
    String::Utf8Value name(isolate, args[0]); 
    auto iter = just::modules.find(*name); 
    register_plugin _init = (*iter->second); 
    // 执行_init拿到函数地址 
    auto _register = reinterpret_cast<InitializerCallback>(_init()); 
    // 执行C++模块提供的注册函数,见C++模块,导出的属性在exports对象中 
    _register(isolate, exports); 
  } 
  // 返回导出的信息 
  args.GetReturnValue().Set(exports->NewInstance(context).ToLocalChecked()); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

1.2 内置JS模块

为了提升加载性能,Node.js的内置JS模块是保存到内存里的,加载的时候,通过模块名获取对应的JS模块源码编译执行,而不需要从硬盘加。比如net模块在内存里表示为。

static const uint16_t net_raw[] = { 
 47, 47, 32, 67,111,112,121,114... 
}; 
  • 1.
  • 2.
  • 3.

以上的数字转成字符是["/", "/", " ", "C", "o", "p", "y", "r"],我们发现这些字符是net模块开始的一些注释。Just同样使用了类似的理念,不过Just是通过汇编来处理的。

.global _binary_lib_fs_js_start 
_binary_lib_fs_js_start: 
        .incbin "lib/fs.js" 
        .global _binary_lib_fs_js_end 
_binary_lib_fs_js_end: 
... 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

Just定义里一系列的全局变量 ,比如以上的binary_lib_fs_js_start变量,它对应的值是lib/fs.js的内容,binary_lib_fs_js_end表示结束地址。

值得一提的是,以上的内容是在代码段的,所以是不能被修改的。接着我们看看如何注册内置JS模块,以fs模块为例。

// builtins.S汇编文件里定义 
extern char _binary_lib_fs_js_start[]; 
extern char _binary_lib_fs_js_end[]; 
 
just::builtins_add("lib/fs.js", _binary_lib_fs_js_start, _binary_lib_fs_js_end - _binary_lib_fs_js_start); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

builtins_add三个参数分别是模块名,模块内容的虚拟开始地址,模块内容大小。来看一下builtins_add的逻辑。

struct builtin { 
  unsigned int size
  const char* source; 
}; 
 
std::map<std::string, just::builtin*> just::builtins; 
 
// 注册JS模块 
void just::builtins_add (const charname, const char* source,  unsigned int size) { 
  struct builtin* b = new builtin(); 
  b->size = size
  b->source = source; 
  builtins[name] = b; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

注册模块的逻辑很简单,就是建立模块名和内容信息的关系,接着看如何加载内置JS模块。

function requireNative (path) { 
      path = `lib/${path}.js` 
      if (cache[path]) return cache[path].exports 
      const { vm } = just 
      const params = ['exports''require''module'
      const exports = {} 
      const module = { exports, type: 'native', dirName: appRoot } 
      // 从数据结构中获得模块对应的源码 
      module.text = just.builtin(path) 
      // 编译 
      const fun = vm.compile(module.text, path, params, []) 
      module.function = fun 
      cache[path] = module 
      // 执行 
      fun.call(exports, exports, p => just.require(p, module), module) 
      return module.exports 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

加载的逻辑也很简单,根据模块名从map里获取源码编译执行,从而拿到导出的属性。

1.3 普通JS模块

普通JS模块就是用户自定义的模块。用户自定义的模块首次加载时都是需要从硬盘实时加载的,所以只需要看加载的逻辑。

// 一般JS模块加载器 
  function require (path, parent = { dirName: appRoot }) { 
    const { join, baseName, fileName } = just.path 
    if (path[0] === '@') path = `${appRoot}/lib/${path.slice(1)}/${fileName(path.slice(1))}.js` 
    const ext = path.split('.').slice(-1)[0] 
    // js或json文件 
    if (ext === 'js' || ext === 'json') { 
      let dirName = parent.dirName 
      const fileName = join(dirName, path) 
      // 有缓存则返回 
      if (cache[fileName]) return cache[fileName].exports 
      dirName = baseName(fileName) 
      const params = ['exports''require''module'
      const exports = {} 
      const module = { exports, dirName, fileName, type: ext } 
      // 文件存在则直接加载 
      if (just.fs.isFile(fileName)) { 
        module.text = just.fs.readFile(fileName) 
      } else { 
        // 否则尝试加载内置JS模块 
        path = fileName.replace(appRoot, ''
        if (path[0] === '/') path = path.slice(1) 
           module.text = just.builtin(path) 
        } 
      } 
      cache[fileName] = module 
      // js文件则编译执行,json则直接parse 
      if (ext === 'js') { 
        const fun = just.vm.compile(module.text, fileName, params, []) 
        fun.call(exports, exports, p => require(p, module), module) 
      } else { 
        // 是json文件则直接parse 
        module.exports = JSON.parse(module.text) 
      } 
      return module.exports 
    } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.

Just里,普通JS模块的加载原理和Node.js类似,但是也有些区别,Node.js加载JS模块时,会优先判断是不是内置JS模块,Just则相反。

1.4 Addon

Node.js里的Addon是动态库,Just里同样是,原理也类似。

function loadLibrary (path, name) { 
      if (cache[name]) return cache[name
      // 打开动态库 
      const handle = just.sys.dlopen(path, just.sys.RTLD_LAZY) 
      // 找到动态库里约定格式的函数的虚拟地址 
      const ptr = just.sys.dlsym(handle, `_register_${name}`) 
      // 以该虚拟地址为入口执行函数 
      const lib = just.load(ptr) 
      lib.close = () => just.sys.dlclose(handle) 
      lib.type = 'module-external' 
      cache[name] = lib 
      return lib 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

just.load是C++实现的函数。

void just::Load(const FunctionCallbackInfo<Value> &args) { 
  Isolate *isolate = args.GetIsolate(); 
  Local<Context> context = isolate->GetCurrentContext(); 
  // C++模块导出的信息 
  Local<ObjectTemplate> exports = ObjectTemplate::New(isolate); 
  // 传入的是注册函数的虚拟地址(动态库) 
   Local<BigInt> address64 = Local<BigInt>::Cast(args[0]); 
   void* ptr = reinterpret_cast<void*>(address64->Uint64Value()); 
   register_plugin _init = reinterpret_cast<register_plugin>(ptr); 
   auto _register = reinterpret_cast<InitializerCallback>(_init()); 
   _register(isolate, exports); 
  // 返回导出的信息 
  args.GetReturnValue().Set(exports->NewInstance(context).ToLocalChecked()); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

因为Addon是动态库,所以底层原理都是对系统API的封装,再通过V8暴露给JS层使用。

2 事件循环

Just的事件循环是基于epoll的,所有生产者生产的任务都是基于文件描述符的,相比Node.js清晰且简洁了很多,也没有了各种阶段。Just支持多个事件循环,不过目前只有内置的一个。我们看看如何创建一个事件循环。

// 创建一个事件循环 
function create(nevents = 128) { 
  const loop = createLoop(nevents) 
  factory.loops.push(loop) 
  return loop 

 
function createLoop (nevents = 128) { 
  const evbuf = new ArrayBuffer(nevents * 12) 
  const events = new Uint32Array(evbuf) 
  // 创建一个epoll 
  const loopfd = create(EPOLL_CLOEXEC) 
  const handles = {} 
  // 判断是否有事件触发 
  function poll (timeout = -1, sigmask) { 
    let r = 0 
    // 对epoll_wait的封装 
    if (sigmask) { 
      r = wait(loopfd, evbuf, timeout, sigmask) 
    } else { 
      r = wait(loopfd, evbuf, timeout) 
    } 
    if (r > 0) { 
      let off = 0 
      for (let i = 0; i < r; i++) { 
        const fd = events[off + 1] 
        // 事件触发,执行回调 
        handles[fd](fd, events[off]) 
        off += 3 
      } 
    } 
    return r 
  } 
  // 注册新的fd和事件 
  function add (fd, callback, events = EPOLLIN) { 
    const r = control(loopfd, EPOLL_CTL_ADD, fd, events) 
    // 保存回调 
    if (r === 0) { 
      handles[fd] = callback 
      instance.count++ 
    } 
    return r 
  } 
  // 删除之前注册的fd和事件 
  function remove (fd) { 
    const r = control(loopfd, EPOLL_CTL_DEL, fd) 
    if (r === 0) { 
      delete handles[fd] 
      instance.count-- 
    } 
    return r 
  } 
  // 更新之前注册的fd和事件 
  function update (fd, events = EPOLLIN) { 
    const r = control(loopfd, EPOLL_CTL_MOD, fd, events) 
    return r 
  } 
  const instance = { fd: loopfd, poll, add, remove, update, handles, count: 0 } 
  return instance 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.

事件循环本质是epoll的封装,一个事件循环对应一个epoll fd,后续生产任务的时候,就通过操作epoll fd,进行增删改查,比如注册一个新的fd和事件到epoll中,并保存对应的回调。然后通过wait进入事件循环,有事件触发后,就执行对应的回调。接着看一下事件循环的执行。


        // 执行事件循环,即遍历每个事件循环 
  run: (ms = -1) => { 
    factory.paused = false 
    let empty = 0 
    while (!factory.paused) { 
      let total = 0 
      for (const loop of factory.loops) { 
        if (loop.count > 0) loop.poll(ms) 
        total += loop.count 
      } 
      // 执行微任务 
      runMicroTasks() 
      ... 
  }, 
 
  stop: () => { 
    factory.paused = true 
  }, 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

Just初始化完毕后就会通过run进入事件循环,这个和Node.js是类似的。

3 初始化

了解了一些核心的实现后,来看一下Just的初始化。

int main(int argc, char** argv) { 
  // 忽略V8的一些逻辑 
  // 注册内置模块 
  register_builtins(); 
  // 初始化isolate 
  just::CreateIsolate(argc, argv, just_js, just_js_len); 
  return 0; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

继续看CreateIsolate(只列出核心代码)

int just::CreateIsolate(...) { 
  Isolate::CreateParams create_params; 
  int statusCode = 0; 
  // 分配ArrayBuffer的内存分配器 
  create_params.array_buffer_allocator =  ArrayBuffer::Allocator::NewDefaultAllocator(); 
  Isolate *isolate = Isolate::New(create_params); 
  { 
    Isolate::Scope isolate_scope(isolate); 
    HandleScope handle_scope(isolate); 
 
    // 新建一个对象为全局对象 
    Local<ObjectTemplate> global = ObjectTemplate::New(isolate); 
    // 新建一个对象为核心对象,也是个全局对象 
    Local<ObjectTemplate> just = ObjectTemplate::New(isolate); 
    // 设置一些属性到just对象 
    just::Init(isolate, just); 
    // 设置全局属性just 
    global->Set(String::NewFromUtf8Literal(isolate, "just", NewStringType::kNormal), just); 
    // 新建上下文,并且以global为全局对象 
    Local<Context> context = Context::New(isolate, NULLglobal); 
    Context::Scope context_scope(context); 
    Local<Object> globalInstance = context->Global(); 
    // 设置全局属性global指向全局对象 
    globalInstance->Set(context, String::NewFromUtf8Literal(isolate,  
      "global",  
      NewStringType::kNormal), globalInstance).Check(); 
 
    // 编译执行just.js,just.js是核心的jS代码 
    MaybeLocal<Value> maybe_result = script->Run(context); 
  } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.

初始化的时候设置了全局对象global和just,所以在JS里可以直接访问,然后再给just对象设置各种属性,接着看just.js的逻辑。

function main (opts) { 
    // 获得C++模块加载器和缓存 
    const { library, cache } = wrapLibrary() 
 
    // 挂载C++模块到JS 
    just.vm = library('vm').vm 
    just.loop = library('epoll').epoll 
    just.fs = library('fs').fs 
    just.net = library('net').net 
    just.sys = library('sys').sys 
    // 环境变量 
    just.env = wrapEnv(just.sys.env) 
    // JS模块加载器 
    const { requireNative, require } = wrapRequire(cache) 
 
    Object.assign(just.fs, requireNative('fs')) 
 
    just.path = requireNative('path'
    just.factory = requireNative('loop').factory 
    just.factory.loop = just.factory.create(128) 
    just.process = requireNative('process'
    just.setTimeout = setTimeout 
    just.library = library 
    just.requireNative = requireNative 
    just.net.setNonBlocking = setNonBlocking 
    just.require = global.require = require 
    just.require.cache = cache 
    // 执行用户js 
    just.vm.runScript(just.fs.readFile(just.args[1]), scriptName) 
    // 进入时间循环 
    just.factory.run() 
  } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

4 总结

Just的底层实现在modules里,里面的实现非常清晰,里面对大量系统API和开源库进行了封装。另外使用了timerfd支持定时器,而不是自己去维护相关逻辑。核心模块代码非常值得学习,有兴趣的可以直接去看对应模块的源码。Just的代码整体很清晰,而且目前的代码量不大,通过阅读里面的代码,对系统、网络、V8的学习都有帮助,另外里面用到了很多开源库,也可以学到如何使用一些优秀的开源库,甚至阅读库的源码。

源码解析地址:

https://github.com/theanarkh/read-just-0.1.4-code