JS运行时Just源码解读

时间:2022-08-27 23:53:01

JS运行时Just源码解读

  • 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++模块。

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

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

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

just.load是C++实现的。

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

1.2 内置JS模块

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

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

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

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

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

JS运行时Just源码解读

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

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

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

  1. struct builtin { 
  2.   unsigned int size
  3.   const char* source; 
  4. }; 
  5.  
  6. std::map<std::string, just::builtin*> just::builtins; 
  7.  
  8. // 注册JS模块 
  9. void just::builtins_add (const charname, const char* source,  unsigned int size) { 
  10.   struct builtin* b = new builtin(); 
  11.   b->size = size
  12.   b->source = source; 
  13.   builtins[name] = b; 

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

  1. function requireNative (path) { 
  2.       path = `lib/${path}.js` 
  3.       if (cache[path]) return cache[path].exports 
  4.       const { vm } = just 
  5.       const params = ['exports''require''module'
  6.       const exports = {} 
  7.       const module = { exports, type: 'native', dirName: appRoot } 
  8.       // 从数据结构中获得模块对应的源码 
  9.       module.text = just.builtin(path) 
  10.       // 编译 
  11.       const fun = vm.compile(module.text, path, params, []) 
  12.       module.function = fun 
  13.       cache[path] = module 
  14.       // 执行 
  15.       fun.call(exports, exports, p => just.require(p, module), module) 
  16.       return module.exports 

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

1.3 普通JS模块

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

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

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

1.4 Addon

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

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

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

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

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

2 事件循环

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

  1. // 创建一个事件循环 
  2. function create(nevents = 128) { 
  3.   const loop = createLoop(nevents) 
  4.   factory.loops.push(loop) 
  5.   return loop 
  6.  
  7. function createLoop (nevents = 128) { 
  8.   const evbuf = new ArrayBuffer(nevents * 12) 
  9.   const events = new Uint32Array(evbuf) 
  10.   // 创建一个epoll 
  11.   const loopfd = create(EPOLL_CLOEXEC) 
  12.   const handles = {} 
  13.   // 判断是否有事件触发 
  14.   function poll (timeout = -1, sigmask) { 
  15.     let r = 0 
  16.     // 对epoll_wait的封装 
  17.     if (sigmask) { 
  18.       r = wait(loopfd, evbuf, timeout, sigmask) 
  19.     } else { 
  20.       r = wait(loopfd, evbuf, timeout) 
  21.     } 
  22.     if (r > 0) { 
  23.       let off = 0 
  24.       for (let i = 0; i < r; i++) { 
  25.         const fd = events[off + 1] 
  26.         // 事件触发,执行回调 
  27.         handles[fd](fd, events[off]) 
  28.         off += 3 
  29.       } 
  30.     } 
  31.     return r 
  32.   } 
  33.   // 注册新的fd和事件 
  34.   function add (fd, callback, events = EPOLLIN) { 
  35.     const r = control(loopfd, EPOLL_CTL_ADD, fd, events) 
  36.     // 保存回调 
  37.     if (r === 0) { 
  38.       handles[fd] = callback 
  39.       instance.count++ 
  40.     } 
  41.     return r 
  42.   } 
  43.   // 删除之前注册的fd和事件 
  44.   function remove (fd) { 
  45.     const r = control(loopfd, EPOLL_CTL_DEL, fd) 
  46.     if (r === 0) { 
  47.       delete handles[fd] 
  48.       instance.count-- 
  49.     } 
  50.     return r 
  51.   } 
  52.   // 更新之前注册的fd和事件 
  53.   function update (fd, events = EPOLLIN) { 
  54.     const r = control(loopfd, EPOLL_CTL_MOD, fd, events) 
  55.     return r 
  56.   } 
  57.   const instance = { fd: loopfd, poll, add, remove, update, handles, count: 0 } 
  58.   return instance 

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

  1.         // 执行事件循环,即遍历每个事件循环 
  2.   run: (ms = -1) => { 
  3.     factory.paused = false 
  4.     let empty = 0 
  5.     while (!factory.paused) { 
  6.       let total = 0 
  7.       for (const loop of factory.loops) { 
  8.         if (loop.count > 0) loop.poll(ms) 
  9.         total += loop.count 
  10.       } 
  11.       // 执行微任务 
  12.       runMicroTasks() 
  13.       ... 
  14.   }, 
  15.  
  16.   stop: () => { 
  17.     factory.paused = true 
  18.   }, 

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

3 初始化

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

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

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

  1. int just::CreateIsolate(...) { 
  2.   Isolate::CreateParams create_params; 
  3.   int statusCode = 0; 
  4.   // 分配ArrayBuffer的内存分配器 
  5.   create_params.array_buffer_allocator =  ArrayBuffer::Allocator::NewDefaultAllocator(); 
  6.   Isolate *isolate = Isolate::New(create_params); 
  7.   { 
  8.     Isolate::Scope isolate_scope(isolate); 
  9.     HandleScope handle_scope(isolate); 
  10.  
  11.     // 新建一个对象为全局对象 
  12.     Local<ObjectTemplate> global = ObjectTemplate::New(isolate); 
  13.     // 新建一个对象为核心对象,也是个全局对象 
  14.     Local<ObjectTemplate> just = ObjectTemplate::New(isolate); 
  15.     // 设置一些属性到just对象 
  16.     just::Init(isolate, just); 
  17.     // 设置全局属性just 
  18.     global->Set(String::NewFromUtf8Literal(isolate, "just", NewStringType::kNormal), just); 
  19.     // 新建上下文,并且以global为全局对象 
  20.     Local<Context> context = Context::New(isolate, NULLglobal); 
  21.     Context::Scope context_scope(context); 
  22.     Local<Object> globalInstance = context->Global(); 
  23.     // 设置全局属性global指向全局对象 
  24.     globalInstance->Set(context, String::NewFromUtf8Literal(isolate,  
  25.       "global",  
  26.       NewStringType::kNormal), globalInstance).Check(); 
  27.  
  28.     // 编译执行just.js,just.js是核心的jS代码 
  29.     MaybeLocal<Value> maybe_result = script->Run(context); 
  30.   } 

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

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

4 总结

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

源码解析地址:

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

原文链接:https://mp.weixin.qq.com/s/S6bGtP-6-b8zMYa6BmtsDQ