前言:本文介绍一下新的JS运行时Just的一些设计和实现。
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++模块加载的。
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();
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);
auto _register = reinterpret_cast<InitializerCallback>(_init());
_register(isolate, exports);
}
args.GetReturnValue().Set(exports->NewInstance(context).ToLocalChecked());
}
load和Node.js的internalBinding类似。
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模块为例。
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;
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模块就是用户自定义的模块。用户自定义的模块首次加载时都是需要从硬盘实时加载的,所以只需要看加载的逻辑。
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]
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 {
path = fileName.replace(appRoot, '')
if (path[0] === '/') path = path.slice(1)
module.text = just.builtin(path)
}
}
cache[fileName] = module
if (ext === 'js') {
const fun = just.vm.compile(module.text, fileName, params, [])
fun.call(exports, exports, p => require(p, module), module)
} else {
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();
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)
const loopfd = create(EPOLL_CLOEXEC)
const handles = {}
function poll (timeout = -1, sigmask) {
let r = 0
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
}
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
}
function remove (fd) {
const r = control(loopfd, EPOLL_CTL_DEL, fd)
if (r === 0) {
delete handles[fd]
instance.count--
}
return r
}
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) {
register_builtins();
just::CreateIsolate(argc, argv, just_js, just_js_len);
return 0;
}
继续看CreateIsolate(只列出核心代码)
int just::CreateIsolate(...) {
Isolate::CreateParams create_params;
int statusCode = 0;
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::Init(isolate, just);
global->Set(String::NewFromUtf8Literal(isolate, "just", NewStringType::kNormal), just);
Local<Context> context = Context::New(isolate, NULL, global);
Context::Scope context_scope(context);
Local<Object> globalInstance = context->Global();
globalInstance->Set(context, String::NewFromUtf8Literal(isolate,
"global",
NewStringType::kNormal), globalInstance).Check();
MaybeLocal<Value> maybe_result = script->Run(context);
}
}
初始化的时候设置了全局对象global和just,所以在JS里可以直接访问,然后再给just对象设置各种属性,接着看just.js的逻辑。
function main (opts) {
const { library, cache } = wrapLibrary()
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)
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
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
|