Учебник о том, как создать собственную базовую среду выполнения 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
).
Ресурсы
- Исходный код учебника
- Полезные концепции — репозиторий Node.js на GitHub
- Документация V8 Rust Bindings — Docs.rs
- Примеры привязки V8 Rust — Rusty V8 GitHub
Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn и Discord.