Создание собственной среды выполнения JavaScript


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

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

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

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

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

Настройка

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

Чтобы начать, создайте новый проект с помощью cargo init. Затем добавьте некоторые зависимости в файл Cargo.toml. Пакет v8 содержит привязки к движку V8 JavaScript, а 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 sub-command, которая откроет наш REPL. Прежде чем мы реализуем REPL, нам сначала нужно создать наше окружение JavaScript.

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

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

Добавьте оператор use в верхней части файла, чтобы включить v8 crate. Далее, давайте вернемся к тому кусочку нереализованного кода для нашего 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 для лучшей обработки ошибок (которую мы реализуем в одном из следующих уроков). Далее мы инициализируем скрипт и создаем объект origin, который определяет, откуда был взят этот скрипт (из файла).

  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).

Ресурсы

  • Исходный код учебника
  • Полезные концепции — репозиторий Node.js GitHub
  • Документация по привязкам V8 Rust — Docs.rs
  • Примеры привязок V8 Rust — Rusty V8 GitHub

Оцените статью
devanswers.ru
Добавить комментарий