如何写一个 JS 运行时

2022-12-06 09:37:41 浏览数 (1)

前言:随着 Node.js 的出现和不断发展,其他新的 JS 运行时也穷出不断,Deno、Just、Bun等等。本文简单介绍一下如何写一个 JS 运行时,相比操作系统、编译器来说,写一个 JS 运行时理论上并不是一个难的事情,但是写一个优秀且功能齐全的运行时并不是一个容易的事情。

JS 引擎

写一个 JS 运行时,首先就必须需要一个 JS 引擎来处理 JS,大部分的 JS 运行时都是基于 V8的,当然你也可以使用其他的 JS 引擎。所以首先需要选择一个 JS 引擎,然后下载代码,编译成功。有了 JS 引擎,就可以通过它提供的一些 API 实现一个可以执行 JS 代码的软件。

代码语言:javascript复制
int main(int argc, char* argv[]) {
  setvbuf(stdout, nullptr, _IONBF, 0);
  setvbuf(stderr, nullptr, _IONBF, 0);
  v8::V8::InitializeICUDefaultLocation(argv[0]);
  v8::V8::InitializeExternalStartupData(argv[0]);
  std::unique_ptr<Platform> platform = platform::NewDefaultPlatform();
  v8::V8::InitializePlatform(platform.get());
  v8::V8::Initialize();
  Isolate::CreateParams create_params;
  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<Context> context = Context::New(isolate, nullptr, global);
    Context::Scope context_scope(context);
    Local<Object> globalInstance = context->Global();
    globalInstance->Set(context, String::NewFromUtf8Literal(isolate, "No", 
    NewStringType::kNormal), No);
    // 设置全局属性global指向全局对象
    globalInstance->Set(context, String::NewFromUtf8Literal(isolate, 
      "global", 
      NewStringType::kNormal), globalInstance).Check();
    {
      // 打开文件
      int fd = open(argv[1], 0, O_RDONLY);
      struct stat info;
      // 取得文件信息
      fstat(fd, &info);
      // 分配内存保存文件内容
      char *ptr = (char *)malloc(info.st_size   1);
      // ptr[info.st_size] = '';
      read(fd, (void *)ptr, info.st_size);
      // 要执行的js代码
      Local<String> source = String::NewFromUtf8(isolate, ptr,
                          NewStringType::kNormal,
                          info.st_size).ToLocalChecked();

      // 编译
      Local<Script> script = Script::Compile(context, source).ToLocalChecked();
      // 解析完应该没用了,释放内存
      free(ptr);
      // 执行
      Local<Value> result = script->Run(context).ToLocalChecked();
    }
  }

  // Dispose the isolate and tear down V8.
  isolate->Dispose();
  v8::V8::Dispose();
  v8::V8::ShutdownPlatform();
  delete create_params.array_buffer_allocator;
  return 0;
}

拓展功能

有了 JS 引擎,我们只能使用 JS 语言本身提供的一些能力,可以做的事情不多,比如网络、文件、进程能力都没有。但是幸运的是,JS 引擎提供了拓展能力,我们可以使用 JS 引擎提供的 API 拓展网络、文件这些功能。在之前代码的基础上增加以下代码。

代码语言:javascript复制
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
Local<Context> context = Context::New(isolate, nullptr, global);
Context::Scope context_scope(context);
// 所有拓展功能挂到这个对象中
Local<Object> No = Object::New(isolate);
No::Console::Init(isolate, No);
Local<Object> globalInstance = context->Global();
// 再把这个对象挂载到全局变量
globalInstance->Set(context, String::NewFromUtf8Literal(isolate, "No", 
NewStringType::kNormal), No);


void No::Console::log(V8_ARGS) {
    V8_ISOLATE
    String::Utf8Value str(isolate, args[0]);
    Log(*str);
}

void No::Console::Init(Isolate* isolate, Local<Object> target) {
  Local<ObjectTemplate> console = ObjectTemplate::New(isolate);
  setMethod(isolate, console, "log", No::Console::log);
  setObjectValue(isolate, target, "console", console->NewInstance(isolate->GetCurrentContext()).ToLocalChecked());
}

以上代码在 JS 的全局变量上挂载了一个变量 No,然后在 No 变量上挂载我们需要拓展的功能,比如上面的 console.log。这样我们就可以直接在 JS 里使用 console.log 了。

事件循环

有了之前的基础后,接下来我们就需要实现一个事件循环,因为有些拓展功能的 API,是同步执行的,但是有些是不能同步执行的,比如文件、网络。所以我们需要一个事件循环来处理异步的任务。事件循环本质上是一个生产者 / 消费者模型,在这个模型中,最重要的是当没有任务消费的时候,如何处理。通常使用的是阻塞 / 唤醒的机制,通常是使用事件驱动模块实现这种机制。如果我们只支持 Linux,那么就可以选择 epoll,如何是 Mac,那么就可以选择 kqueue,基本上,大多数操作系统都提供了这种机制,如果我们支持多操作系统,那么就需要封装好各个操作系统提供的 API,当然如果为了方便,我们可以直接使用 Libuv。如果你只想支持比较新版本的 Linux,可以使用真正的异步 IO 框架 io_uring。

代码语言:javascript复制
void No::io_uring::RunIOUring(struct io_uring_info *io_uring_data) {
    struct io_uring* ring = &io_uring_data->ring;
    struct io_uring_cqe* cqe;
    struct request* req;
    while(io_uring_data->stop != 1 && io_uring_data->pending != 0) {
        // 提交请求给内核
        int count = io_uring_submit_and_wait(ring, 1);
        // 处理每一个完成的请求
        while (1) { 
            io_uring_peek_cqe(ring, &cqe);
            if (cqe == NULL)
                break;
            --io_uring_data->pending;
            // 拿到请求上下文
            req = (struct request*) (uintptr_t) cqe->user_data;
            req->res = cqe->res;
            io_uring_cq_advance(ring, 1);
            // 执行回调
            if (req->cb != nullptr) {
                req->cb((void *)req);
            }
        }
    }
}

模块加载器

有了上面的基础后,基本上实现了一个 JS 运行时了。可以在 JS 里使用到各种各样的拓展功能,比如建立 TCP 连接,读写文件。但是还有一个重要的部分需要实现,那就是模块加载器,内置的功能可以通过挂载到全局变量的方式来实现,这样用户就不需要通过模块加载器的方式来使用拓展功能,但是用户的 JS,还是需要一个模块加载器。实现模块加载器之后,架子就搭建得差不多了。剩下的事情就是取决于需要支持什么功能。

代码语言:javascript复制
void No::Loader::Compile(V8_ARGS) {
    V8_ISOLATE
    V8_CONTEXT
    String::Utf8Value filename(isolate, args[0].As<String>());
    int fd = open(*filename, 0 , O_RDONLY);
    std::string content;
    char buffer[4096];
    while (1)
    {
      memset(buffer, 0, 4096);
      int ret = read(fd, buffer, 4096);
      if (ret == -1) {
        return args.GetReturnValue().Set(newStringToLcal(isolate, "read file error"));
      }
      if (ret == 0) {
        break;
      }
      content.append(buffer, ret);
    }
    close(fd);
    ScriptCompiler::Source script_source(newStringToLcal(isolate, content.c_str()));
    Local<String> params[] = {
      newStringToLcal(isolate, "require"),
      newStringToLcal(isolate, "exports"),
      newStringToLcal(isolate, "module"),
    };
    MaybeLocal<Function> fun =
    ScriptCompiler::CompileFunctionInContext(context, &script_source, 3, params, 0, nullptr);
    if (fun.IsEmpty()) {
      args.GetReturnValue().Set(Undefined(isolate));
    } else {
      args.GetReturnValue().Set(fun.ToLocalChecked());
    }
}

如果你有兴趣,可以参考我之前的一些实践。

https://github.com/theanarkh/No.js

https://github.com/theanarkh/js_runtime_loader

0 人点赞