前言:随着 Node.js 的出现和不断发展,其他新的 JS 运行时也穷出不断,Deno、Just、Bun等等。本文简单介绍一下如何写一个 JS 运行时,相比操作系统、编译器来说,写一个 JS 运行时理论上并不是一个难的事情,但是写一个优秀且功能齐全的运行时并不是一个容易的事情。
写一个 JS 运行时,首先就必须需要一个 JS 引擎来处理 JS,大部分的 JS 运行时都是基于 V8的,当然你也可以使用其他的 JS 引擎。所以首先需要选择一个 JS 引擎,然后下载代码,编译成功。有了 JS 引擎,就可以通过它提供的一些 API 实现一个可以执行 JS 代码的软件。
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 拓展网络、文件这些功能。在之前代码的基础上增加以下代码。
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。
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,还是需要一个模块加载器。实现模块加载器之后,架子就搭建得差不多了。剩下的事情就是取决于需要支持什么功能。
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/js_runtime_loader
https://github.com/theanarkh/No.js