JS运行时Just源码解读

前言:本文介绍一下新的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++模块加载的。

// 加载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());
}

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模块为例。

// 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

猜你喜欢

转载自blog.csdn.net/THEANARKH/article/details/119937832