Учебник о том, как создать собственную базовую среду выполнения JavaScript, используя движок JavaScript V8.

Будь то среда выполнения браузера или среда выполнения на стороне сервера, такая как Node.js, мы все используем какую-то среду выполнения для запуска нашего кода JavaScript. Сегодня мы создадим собственную базовую среду выполнения JavaScript, используя движок JavaScript V8.

Что такое среда выполнения JavaScript?

Среда выполнения JavaScript — это просто среда, которая расширяет движок JavaScript, предоставляя полезные API и позволяя программе взаимодействовать с миром за пределами своего контейнера. Это отличается от движка, который просто анализирует код и выполняет его в изолированной среде.

Как я упоминал ранее, V8 — это движок JavaScript, то есть он обрабатывает и выполняет анализ исходного кода JavaScript. Node.js и Chrome (оба работают на v8) предоставляют объекты и API, которые позволяют коду взаимодействовать с такими вещами, как файловая система (через node:fs) или объект окна (в Chrome).

Настраивать

В этом уроке мы будем использовать Rust для создания среды выполнения. Мы будем использовать привязки V8, поддерживаемые командой Deno. Поскольку создание среды выполнения — сложный процесс, сегодня мы начнем с простого, реализуя REPL (цикл чтения-оценки-печати). Подсказка, которая запускает JavaScript по одной строке ввода за раз.



Для начала создайте новый проект с помощью cargo init. Затем добавьте некоторые зависимости в файл Cargo.toml. Пакет v8 содержит привязки к движку JavaScript V8, а clap — популярная библиотека для обработки аргументов командной строки.

[dependencies]
v8 = "0.48.0"
clap = "3.2.16"

Управление вводом команд

При использовании нашей среды выполнения мы, вероятно, захотим предоставить ей некоторые аргументы командной строки, например, какой файл запускать, или любые флаги, которые изменяют поведение. Откройте src/main.rs и в нашей функции main замените вызов println некоторым кодом, определяющим наши подкоманды и входные параметры. Если подкоманда не указана, мы делаем то же самое, что и Node.js, и бросаем пользователя в REPL. Мы также создадим одну подкоманду `run`, которую мы реализуем в следующем уроке. run после реализации позволит пользователю запустить файл JavaScript (с любыми другими параметрами, которые мы определяем).

use clap::{Command, arg};
fn main() {
  let cmd = clap::Command::new("myruntime")
  .bin_name("myruntime")
  .subcommand_required(false)
  .subcommand(
    Command::new("run")
      .about("Run a file")
      .arg(arg!(<FILE> "The file to run"))
      .arg_required_else_help(true),
  );
}

Теперь мы сопоставим аргументы с этой схемой и соответствующим образом обработаем ответы.

  ...
  let matches = cmd.get_matches();
  match matches.subcommand() {
    Some(("run", _matches)) => unimplemented!(),
    _ => {
      unimplemented!("Implement this in the next step")
    },
  };

У нас пока только две возможности. Первая — это run, которую мы сегодня реализовывать не будем, а вторая — это подкоманда no, которая откроет наш REPL. Прежде чем мы реализуем REPL, нам сначала нужно создать нашу среду JavaScript.

Инициализация V8 и создание экземпляра ядра

Прежде чем мы сможем что-то сделать с V8, мы должны сначала его инициализировать. Затем нам нужно создать изоляцию. Всеохватывающий объект, представляющий один экземпляр механизма JavaScript.

Добавьте оператор use вверху файла, чтобы включить ящик v8. Далее давайте вернемся к этому фрагменту нереализованного кода для нашего REPL и инициализируем V8, а также создадим изолят и завернем его в файл HandleScope.

use v8;
...
    _ => {
      let platform = v8::new_default_platform(0, false).make_shared();
      v8::V8::initialize_platform(platform);
      v8::V8::initialize();
      let isolate = &mut v8::Isolate::new(v8::CreateParams::default());
      let handle_scope = &mut v8::HandleScope::new(isolate);
    },

Создание REPL

Чтобы упростить управление нашим кодом, мы создадим среду выполнения внутри файла struct. Когда будет создан новый экземпляр, мы создадим файл Context. Context позволяет набору глобальных и встроенных объектов существовать внутри «контекста». Говоря о глобальных объектах, мы создадим шаблон объекта с именем global для использования в следующем руководстве. Этот объект позволяет нам привязывать наши собственные глобальные функции, но пока мы просто используем его для создания контекста.

struct Runtime<'s, 'i> {
  context_scope: v8::ContextScope<'i, v8::HandleScope<'s>>,
}
impl<'s, 'i> Runtime<'s, 'i>
where
  's: 'i,
{
  pub fn new(
    isolate_scope: &'i mut v8::HandleScope<'s, ()>,
  ) -> Self {
    let global = v8::ObjectTemplate::new(isolate_scope);
    let context = v8::Context::new_from_template(isolate_scope, global);
    let context_scope = v8::ContextScope::new(isolate_scope, context);
Runtime { context_scope }
  }
}

Далее давайте определим внутри Runtime метод, отвечающий за обработку REPL, и только REPL. Используя loop, мы будем получать входные данные на каждой итерации, а затем запускать их в случае успеха. Нам также нужно будет импортировать некоторые вещи из std::io в верхней части файла.

use std::io::{self, Write};
...
pub fn repl(&mut self) {
    println!("My Runtime REPL (V8 {})", v8::V8::get_version());
    loop {
      print!("> ");
      io::stdout().flush().unwrap();
  
      let mut buf = String::new();
      match io::stdin().read_line(&mut buf) {
        Ok(n) => {
          if n == 0 {
            println!();
            return;
          }
  
          // prints the input (you'll replace this in the next step)
          println!("input: {}", &buf);
        }
        Err(error) => println!("error: {}", error),
      }
    }
  }

Теперь давайте вернем нашу команду REPL в main, создадим экземпляр среды выполнения и инициализируем REPL.

      ...
      let mut runtime = Runtime::new(handle_scope);
      runtime.repl();

Запуск кода

Наш метод run примет код, а также имя файла (которое для REPL мы будем использовать просто (shell)) для обработки ошибок. Мы создаем новую область для обработки выполнения скрипта и заключаем ее в область TryCatch для лучшей обработки ошибок (которую мы реализуем в будущем руководстве). Затем мы инициализируем скрипт и создаем объект-источник, который определяет, откуда этот скрипт возник (в файле).

  fn run(
    &mut self,
    script: &str,
    filename: &str,
  ) -> Option<String> {
    let scope = &mut v8::HandleScope::new(&mut self.context_scope);
    let mut scope = v8::TryCatch::new(scope);
    let filename = v8::String::new(&mut scope, filename).unwrap();
    let undefined = v8::undefined(&mut scope);
    let script = v8::String::new(&mut scope, script).unwrap();
    let origin = v8::ScriptOrigin::new(
      &mut scope,
      filename.into(),
      0,
      0,
      false,
      0,
      undefined.into(),
      false,
      false,
      false,
    );
}

Теперь, продолжая run, компилируем скрипт, отлавливаем все ошибки и выводим, что произошла ошибка. Затем мы запускаем скрипт, снова перехватывая любые ошибки и записывая их в журнал, если ошибка произошла. Затем мы возвращаем результат работы скрипта (или None, если произошла ошибка).

    ...
    let script = if let Some(script) = v8::Script::compile(&mut scope, script, Some(&origin)) {
      script
    } else {
      assert!(scope.has_caught());
      eprintln!("An error occurred when compiling the JavaScript!");
      return None;
    };
    if let Some(result) = script.run(&mut scope) {
      return Some(result.to_string(&mut scope).unwrap().to_rust_string_lossy(&mut scope));
    } else {
      assert!(scope.has_caught());
      eprintln!("An error occurred when running the JavaScript!");
      return None;
    }

Вернитесь к этим двум строкам в нашем методе repl.

          // prints the input (you'll replace this in the next step)
          println!("input: {}", &buf);

Теперь мы можем реализовать наш метод run. Замените println оператором if, чтобы запустить сценарий, и распечатайте результат.

          if let Some(result) = self.run(&buf, "(shell)") {
            println!("{}", result);
          }

Заключение

Поздравляем! Вы сделали первый шаг в создании собственной среды выполнения JavaScript с использованием движка V8. Завершенный код из этого руководства можно найти на GitHub, и ниже я перечислил несколько замечательных ресурсов, благодаря которым стало возможным создание этого руководства.

В следующий раз мы займемся обработкой ошибок, используя уже готовый код (например, область действия TryCatch).

Ресурсы

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn и Discord.