- 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 char* name, 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, 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);
}
}
- 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