前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何写一个 JS 运行时

如何写一个 JS 运行时

作者头像
theanarkh
发布2022-12-06 09:37:41
1.7K0
发布2022-12-06 09:37:41
举报
文章被收录于专栏:原创分享

前言:随着 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] = '\0';
      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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-10-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JS 引擎
  • 拓展功能
  • 事件循环
  • 模块加载器
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档