-
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;
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
- }
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 内置JS模块
为了提升加载性能,Node.js的内置JS模块是保存到内存里的,加载的时候,通过模块名获取对应的JS模块源码编译执行,而不需要从硬盘加。比如net模块在内存里表示为。
- static const uint16_t net_raw[] = {
- 47, 47, 32, 67,111,112,121,114...
- };
以上的数字转成字符是["/", "/", " ", "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:
- ...
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);
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 char* name, const char* source, unsigned int size) {
- struct builtin* b = new builtin();
- b->size = size;
- b->source = source;
- builtins[name] = b;
- }
注册模块的逻辑很简单,就是建立模块名和内容信息的关系,接着看如何加载内置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
- }
加载的逻辑也很简单,根据模块名从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
- }
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
- }
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());
- }
因为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
- }
事件循环本质是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
- },
- }
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;
- }
继续看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, NULL, global);
- 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);
- }
- }
初始化的时候设置了全局对象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()
- }
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