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++模塊。
- 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