用 Rust 构建你自己的 JavaScript 运行时(1)

简介: 在这篇文章中我们将创建一个自定义的 JavaScript 运行时,它能够执行本地 JavaScript 文件,与文件系统交互,并且有一个简化版的 console API。

原文链接: https://deno.com/blog/roll-your-own-javascript-runtime

原文作者:Bartek Iwańczuk

为了大家能够读的更爽,本文全部由本人手工翻译而成,没有任何机翻内容,如果大家有收获的话,还请多多点赞收藏支持~

在这篇文章中我们将创建一个自定义的 JavaScript 运行时,我们就叫它 runjs 吧!可以把它认为是一个简化版的 deno(一个类似于 node 的 JS 运行时)我们的目标是创建一个 CLI(命令行程序),它能够执行本地 JavaScript 文件,与文件系统交互(读取文件、写入文件、删除文件),并且有一个简化版的 console API。

本系列文章一共有两篇:

前置要求

这篇教程需要读者掌握以下内容:

  • 基本的 Rust 知识
  • 对 JavaScript 事件循环的基本了解

确保你的电脑上已经安装了 Rust(以及 cargo,Rust 的包管理器),其版本不能低于 1.62.0。访问 rust-lang.org 来安装 Rust 编译器和 cargo

运行下面这行命令来确保我们能够继续前进:

$ cargo --version
cargo 1.62.0 (a748cf5a3 2022-06-08)

Hello, Rust!

首先,我们来创建一个新的 Rust 项目,初始化一个名叫 runjs 的二进制包。

$ cargo init --bin runjs
     Created binary (application) package

进入 runjs 文件夹并在你的编辑器中打开它。运行 cargo run 来确认我们正确地初始化了项目。

$ cd runjs
$ cargo run
   Compiling runjs v0.1.0 (/Users/ib/dev/runjs)
    Finished dev [unoptimized + debuginfo] target(s) in 1.76s
     Running `target/debug/runjs`
Hello, world!

如果一切顺利的话,我们就可以开始创建我们自己的 JavaScript 运行时了!

依赖安装

接下来,我们添加 deno_coretokio 依赖包到项目中。

$ cargo add deno_core
    Updating crates.io index
      Adding deno_core v0.142.0 to dependencies.
$ cargo add tokio --features=full
    Updating crates.io index
      Adding tokio v1.19.2 to dependencies.

更新后的 Cargo.toml 文件应该长这样子:

[package]
name = "runjs"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
deno_core = "0.142.0"
tokio = { version = "1.19.2", features = ["full"] }

deno_core 由 Deno 团队创建,用于抽象简化与 V8 JavaScript 引擎之间的交互。V8 是一个包含了数千个 API 的复杂项目,为了让大家能够更方便地使用它,deno_core 提供了一个 JsRuntime 结构体,其中封装了一个 V8 引擎实例(它叫 Isolate)并提供了整合事件循环的能力。

tokio 是一个异步的 Rust 运行时,我们将用它来作为事件循环。 Tokio 将负责与操作系统的交互,如文件读写、网络访问等。tokiodeno_core 让我们能够轻松地将 JavaScript 的 Promise 映射为 Rust 的 Future

有了 JavaScript 引擎和事件循环,我们正式开始 JS 运行时的构建。

Hello, runjs!

我们首先编写一个异步的 Rust 函数来创建一个 JsRuntime 实例,它将负责 JS 代码的执行。

// main.rs
use std::rc::Rc;
use deno_core::error::AnyError;

async fn run_js(file_path: &str) -> Result<(), AnyError> {
   
  let main_module = deno_core::resolve_path(file_path)?;
  let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
   
      module_loader: Some(Rc::new(deno_core::FsModuleLoader)),
      ..Default::default()
  });

  let mod_id = js_runtime.load_main_module(&main_module, None).await?;
  let result = js_runtime.mod_evaluate(mod_id);
  js_runtime.run_event_loop(false).await?;
  result.await?
}

fn main() {
   
  println!("Hello, world!");
}

这段代码信息量很大:run_js 异步函数创建了一个 JsRuntime 实例,它使用了基于文件系统的模块加载器。随后,我们往 js_runtime 中导入了一个模块,运行它,最后启动了一个事件循环。

run_js 函数封装了整个 JavaScript 代码的运行生命周期。在我们开始正式运行代码之前,得先创建一个单线程的 tokio 运行时来运行我们的 run_js 函数。

// main.rs
fn main() {
   
  let runtime = tokio::runtime::Builder::new_current_thread()
    .enable_all()
    .build()
    .unwrap();
  if let Err(error) = runtime.block_on(run_js("./example.js")) {
   
    eprintln!("error: {}", error);
  }
}

现在我们可以来尝试着运行一些 JavaScript 代码了!创建一个 example.js 文件,其功能是打印 Hello runjs!

// example.js
Deno.core.print("Hello runjs!");

这里我们使用了 Deno.coreprint 函数 - 它是由 deno_core 提供的一个内置全局变量。

现在运行它:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/runjs`
  Hello runjs!⏎

成功了!仅仅用了 25 行 Rust 代码,我们就创建了一个简单的 JavaScript 运行时,它可以执行我们的本地 JS 文件。当然现在这个运行时并没有太多的功能(例如,现在 console.log 是没有效果的,不信你试试!),但我们已经把一个 V8 引擎和 tokio 集成到我们的项目中了。

添加 console API

现在我们来添加 console API。首先,创建 src/runtime.js 文件,这个文件用于实例化 console 对象,使其全局可用。

// runtime.js
((globalThis) => {
   
  const core = Deno.core;

  function argsToMessage(...args) {
   
    return args.map((arg) => JSON.stringify(arg)).join(" ");
  }

  globalThis.console = {
   
    log: (...args) => {
   
      core.print(`[out]: ${
     argsToMessage(...args)}\n`, false);
    },
    error: (...args) => {
   
      core.print(`[err]: ${
     argsToMessage(...args)}\n`, true);
    },
  };
})(globalThis);

console.logconsole.error 函数接收若干个参数,将这些参数交给 JSON.stringlify 处理(这样我们才能打印对象类型),然后在打印内容之前添加 outerror 前缀。这是一种老式的 JS 写法,就像我们在 ES module 出现之前会写的那样。

为了确保我们不会污染全局作用域,我们把代码放在一个立即执行函数(IIFE)中运行。如果我们没有这么做,argsToMessage 函数就会被暴露在全局作用域中。

现在我们把这份代码添加到我们的运行时中,在每次执行代码之前先执行它:

let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
   
  module_loader: Some(Rc::new(deno_core::FsModuleLoader)),
  ..Default::default()
});
// 添加这行
js_runtime.execute_script("[runjs:runtime.js]",  include_str!("./runtime.js")

现在,我们在 example.js 中使用我们的 console API:

console.log("Hello", "runjs!");
console.error("Boom!");

再次运行 example.js

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/runjs`
[out]: "Hello" "runjs!"
[err]: "Boom!"

成功了!现在我们来添加文件系统相关的 API。

添加基础文件系统 API

首先我们更新一下 runtime.js 文件:

((globalThis) => {
   
  // ...

  core.initializeAsyncOps();
  globalThis.runjs = {
   
    readFile: (path) => {
   
      return core.ops.op_read_file(path);
    },
    writeFile: (path, contents) => {
   
      return core.ops.op_write_file(path, contents);
    },
    removeFile: (path) => {
   
      return core.ops.op_remove_file(path);
    },
  };
})(globalThis);

我们添加了一个新的全局对象 runjs ,它有三个方法:readFilewriteFileremoveFile。前两个方法是异步的,最后一个方法是同步的。

你可能会疑惑那些 core.ops.[操作名称] 是什么 - 它们是 deno_core 包提供的绑定 JavaScript 和 Rust 函数的能力。当你调用这些的方法时,deno_core 会寻找拥有 #[op] 属性和对应名称的 Rust 函数进行调用。

我们更新 main.rs 文件来看看效果:

// ...
use deno_core::op;
use deno_core::Extension;
#[op]
async fn op_read_file(path: String) -> Result<String, AnyError> {
   
    let contents = tokio::fs::read_to_string(path).await?;
    Ok(contents)
}

#[op]
async fn op_write_file(path: String, contents: String) -> Result<(), AnyError> {
   
    tokio::fs::write(path, contents).await?;
    Ok(())
}

#[op]
fn op_remove_file(path: String) -> Result<(), AnyError> {
   
    std::fs::remove_file(path)?;
    Ok(())
}
// ...

我们添加了三个可以在 JavaScript 中调用的操作。但要想让这些操作能够被 JavaScript 调用,我们还需要通过注册一个 Extension 来让 deno_core 知道这些操作函数。

async fn run_js(file_path: &str) -> Result<(), AnyError> {
   
  let main_module = deno_core::resolve_path(file_path)?;
  let runjs_extension = Extension::builder()
      .ops(vec![
          op_read_file::decl(),
          op_write_file::decl(),
          op_remove_file::decl(),
      ])
      .build();
  let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
   
      module_loader: Some(Rc::new(deno_core::FsModuleLoader)),
      extensions: vec![runjs_extension],
      ..Default::default()
  });

  // ...
}

Extension 让你能够配置你的 JsRuntime 实例,将不同的 Rust 函数暴露给 JavaScript,同时你还能进行一些更高级的操作,例如加载额外的 JavaScript 代码。

让我们再更新一下 example.js

console.log("Hello", "runjs!");
console.error("Boom!");

const path = "./log.txt";
try {
   
  const contents = await runjs.readFile(path);
  console.log("Read from a file", contents);
} catch (err) {
   
  console.error("Unable to read file", path, err);
}

await runjs.writeFile(path, "I can write to a file.");
const contents = await runjs.readFile(path);
console.log("Read from a file", path, "contents:", contents);
console.log("Removing file", path);
runjs.removeFile(path);
console.log("File removed");

运行它:

$ cargo run
   Compiling runjs v0.1.0 (/Users/ib/dev/runjs)
    Finished dev [unoptimized + debuginfo] target(s) in 0.97s
     Running `target/debug/runjs`
[out]: "Hello" "runjs!"
[err]: "Boom!"
[err]: "Unable to read file" "./log.txt" {"code":"ENOENT"}
[out]: "Read from a file" "./log.txt" "contents:" "I can write to a file."
[out]: "Removing file" "./log.txt"
[out]: "File removed"

太棒了,我们的 runjs 运行时现在可以和文件系统进行交互了!我们只用了寥寥数行代码就让 JavaScript 拥有了调用 Rust 函数的能力 - deno_core 帮我们处理了两种编程语言之间数据转换和通信的问题,我们不需要自己手动处理这些麻烦。

总结

在这篇文章中,我们创建了一个 Rust 项目,将一个强大的 JavaScript 引擎(V8)和一个高性能的事件循环库(tokio)集成在一起,从而构建了一个简单的自定义 JavaScript 运行时。

在本系列文章的第二篇中,我们将为 runjs 添加 fetch API 以及 TypeScript 支持,并对我们的 CLI 进行改进

相关文章
|
16天前
|
JavaScript 前端开发 物联网
JavaScript:构建动态世界的引擎
JavaScript:构建动态世界的引擎
|
16天前
|
前端开发 JavaScript 开发者
JavaScript:构建动态网络的引擎
JavaScript:构建动态网络的引擎
|
16天前
|
前端开发 JavaScript 开发者
JavaScript:构建动态Web的核心力量
JavaScript:构建动态Web的核心力量
|
5月前
|
前端开发 算法 API
构建高性能图像处理Web应用:Next.js与TailwindCSS实践
本文分享了构建在线图像黑白转换工具的技术实践,涵盖技术栈选择、架构设计与性能优化。项目采用Next.js提供优秀的SSR性能和SEO支持,TailwindCSS加速UI开发,WebAssembly实现高性能图像处理算法。通过渐进式处理、WebWorker隔离及内存管理等策略,解决大图像处理性能瓶颈,并确保跨浏览器兼容性和移动设备优化。实际应用案例展示了其即时处理、高质量输出和客户端隐私保护等特点。未来计划引入WebGPU加速、AI增强等功能,进一步提升用户体验。此技术栈为Web图像处理应用提供了高效可行的解决方案。
|
6月前
|
前端开发 搜索推荐 JavaScript
如何通过DIY.JS快速构建出一个DIY手机壳、T恤的应用?
DIY.JS 是一款基于原生 Canvas 的业务级图形库,专注于商品定制的图形交互功能,帮助开发者轻松实现个性化设计。适用于 T 恤、手机壳等多种商品场景。它自带丰富功能,无需从零构建,快速集成到项目中。通过创建舞台、添加模型、定义 DIY 区域和添加素材四个步骤即可完成基础用法。支持在线演示体验,文档详细,易上手。
206 57
|
6月前
|
前端开发 JavaScript NoSQL
使用 Node.js、Express 和 React 构建强大的 API
本文详细介绍如何使用 Node.js、Express 和 React 构建强大且动态的 API。从开发环境搭建到集成 React 前端,再到利用 APIPost 高效测试 API,适合各水平开发者。内容涵盖 Node.js 运行时、Express 框架与 React 库的基础知识及协同工作方式,还涉及数据库连接和前后端数据交互。通过实际代码示例,助你快速上手并优化应用性能。
|
7月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
11月前
|
Rust 安全 Java
探索Rust语言的并发编程模型
探索Rust语言的并发编程模型
275 2
|
11月前
|
Rust 安全 区块链
探索Rust语言:系统编程的新选择
【10月更文挑战第27天】Rust语言以其安全性、性能和并发性在系统编程领域受到广泛关注。本文介绍了Rust的核心特性,如内存安全、高性能和强大的并发模型,以及开发技巧和实用工具,展示了Rust如何改变系统编程的面貌,并展望了其在WebAssembly、区块链和嵌入式系统等领域的未来应用。
|
11月前
|
Rust 安全 编译器
编程语言新宠:Rust语言的特性、优势与实战入门
【10月更文挑战第26天】Rust语言诞生于2006年,由Mozilla公司的Graydon Hoare发起。作为一门系统编程语言,Rust专注于安全和高性能。通过所有权系统和生命周期管理,Rust在编译期就能消除内存泄漏等问题,适用于操作系统、嵌入式系统等高可靠性场景。
562 2

热门文章

最新文章