Этот пост изначально был размещен в JavaScript на простом английском на Медиуме. Вы можете найти будущие уроки на моем Средняя серия

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


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

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

Как я упоминал ранее, V8 — это движок JavaScript, то есть он обрабатывает и выполняет анализ исходного кода JavaScript. Node.js и Chrome (оба на базе версии 8) предоставляют объекты и 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 call с некоторым кодом, определяющим наши подкоманды и входные параметры. Если подкоманда не указана, мы делаем то же самое, что и 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. Завершенный код из этого руководства можно найти на Гитхаби я перечислил некоторые замечательные ресурсы, которые сделали возможным создание этого руководства ниже.

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


Ресурсы