Deno の コードリーディング
cloudflare worker, deno deploy 等で v8 isolates を前提にしたホスティングプラットフォームが増えている。これを自分で実装してみたいと思った。
denoland/rusty_v8 を読んだが、SnapshotCreator の使い方がよくわからなかった。これを deno 本体を読んで使い方を調べた。
ついでに、rusty_v8、というか v8 にはメインループやスケジューラが含まれていないので、 Tokio のメインループで、どのように deno が非同期メインループを実装しているか調べた。
まず公式の説明は rusty_v8 導入以前のもので、現状とやや乖離している。
前提として、v8 自体ではイベントループが回らない。それを deno がどのように実行しているか。
v8 へのバインディング。deno 本体からリポジトリごと切り出されている。
本当はこれを使いたいだけだったのだが、example が少なく、その実例として deno の大部分を読む羽目になった。
v8 の内部状態を serde でシリアライズできるようにするやつらしい。実験的なモジュールで、まだ全部で使われてるわけではないので存在を知っておけば十分。
これで http-server の実装がメチャクチャ速くなったみたいなのは読んだ。
Deno 1.9 previews native HTTP/2 server | InfoWorld
rusty_v8 で V8 Isolate (軽量プロセスのようなもの) を管理して、V8 Snapshot の生成、読み込み、実行状態(JsRuntimeState
)の管理をしている crate。 runtime::JsRuntime
がメイン。
V8 には Isolate の実行状態をシリアライズしてバイナリとして吐ける機能がある。これを使って、 Deno
のグローバル変数を読み込み終わった状態を作って吐き出しておく。deno 起動時はこの snapshot から起動するので高速に立ち上がる。
core/bindings.rs
で V8 と Rust のブリッジが実装されている。これらは Deno.core.opSync("op_print", "Hello World\n");
のように、JS 側から直接叩くことができる。Deno で実装されてる部分は v8 isolate としてシリアライズされないが、rusty_v8 の snapshot を起動する際に、 ExternalReferences
という参照を渡すことで、rust <=> js 間をブリッジしている。(rusty_v8 からみると、ここが全くドキュメントになってない)
実際に起動用 snapshot を作るのは runtime/build.rs
なのだが、 runtime/extensions/*
や extensions/*
も一緒に実行されている。これらは ↑ の ops の API を使って、 js で console, fetch, webgpu といった API をウェブ標準に合わせて実装している。つまり、 console.log(...)
の実体は op_print
のラッパーになっている。
ESM の import や dynamic import のモジュール解決もここで扱う。
注意するべきは deno/core にイベントループは含まれておらず、イベントループに実行キューを詰む処理はあるが、そのループを実際に動かす処理は deno/runtime にある。
Tokio のイベントループで deno/core の JsRuntime を回す。OS のプロセス管理を WebWorker の API として実装している。
メインプロセスが、 deno/runtime/worker.rs
で、 サブプロセスが deno/runtime/web_worker.rs
。
deno 特有の permissions 管理もここ。
CLI として、TypeScript 周りを処理して、その結果を runtime::MainWorker
に渡す。Language Server Protocol も cli の中で実装してる。
基本的に、TypeScript 周りを扱う処理は全部 CLI 層にある。他のは v8 を叩くのが主なので、特に出てこない。
以下これらを理解するためのコードリーディングのログ。
最初に読んだのは runtime/examples/hello_runtime.rs
Worker を作って event_loop を回している
let mut worker =
MainWorker::from_options(main_module.clone(), permissions, &options);
worker.bootstrap(&options);
worker.execute_module(&main_module).await?;
worker.run_event_loop().await?;
Ok(())
runtime/worker.rs
pub fn bootstrap(&mut self, options: &WorkerOptions) {
let runtime_options = json!({
"args": options.args,
"applySourceMaps": options.apply_source_maps,
"debugFlag": options.debug_flag,
"denoVersion": options.runtime_version,
"noColor": options.no_color,
"pid": std::process::id(),
"ppid": ops::runtime::ppid(),
"target": env!("TARGET"),
"tsVersion": options.ts_version,
"unstableFlag": options.unstable,
"v8Version": deno_core::v8_version(),
"location": options.location,
});
let script = format!(
"bootstrap.mainRuntime({})",
serde_json::to_string_pretty(&runtime_options).unwrap()
);
self
.execute(&script)
.expect("Failed to execute bootstrap script");
}
/// Same as execute2() but the filename defaults to "$CWD/__anonymous__".
pub fn execute(&mut self, js_source: &str) -> Result<(), AnyError> {
let path = env::current_dir()
.context("Failed to get current working directory")?
.join("__anonymous__");
let url = Url::from_file_path(path).unwrap();
self.js_runtime.execute(url.as_str(), js_source)
}
bootstrap.mainRuntime
というメソッドを引数をシリアライズしながら呼んでいる。
/// Loads, instantiates and executes specified JavaScript module.
pub async fn execute_module(
&mut self,
module_specifier: &ModuleSpecifier,
) -> Result<(), AnyError> {
let id = self.preload_module(module_specifier).await?;
self.wait_for_inspector_session();
let mut receiver = self.js_runtime.mod_evaluate(id);
tokio::select! {
maybe_result = receiver.next() => {
debug!("received module evaluate {:#?}", maybe_result);
let result = maybe_result.expect("Module evaluation result not provided.");
return result;
}
event_loop_result = self.run_event_loop() => {
event_loop_result?;
let maybe_result = receiver.next().await;
let result = maybe_result.expect("Module evaluation result not provided.");
return result;
}
}
}
まず preload_modules する
/// Loads and instantiates specified JavaScript module.
pub async fn preload_module(
&mut self,
module_specifier: &ModuleSpecifier,
) -> Result<ModuleId, AnyError> {
self.js_runtime.load_module(module_specifier, None).await
}
specifier を読みだした tokio の id を返している?
読む順番が違う気がしてきた。先に js_runtime.mod_evaluate(id)
というやつを読んだほうが良さそう。
先に run_event_loop
が何をやってるかだけみていく
pub async fn run_event_loop(&mut self) -> Result<(), AnyError> {
poll_fn(|cx| self.poll_event_loop(cx)).await
}
この poll_fn
は futures_util::future::poll_fn
で、 rust の非同期周りエコシステムこの辺ややこしそうなイメージあるけど、 tokio でも future の API を使うようになったのかな?
(オンマウスで表示される rust_analyzer が超絶便利…あと examples があるのは最高…)
Example
use futures::future::poll_fn;
use futures::task::{Context, Poll};
fn read_line(_cx: &mut Context<'_>) -> Poll<String> {
Poll::Ready("Hello, World!".into())
}
let read_future = poll_fn(read_line);
assert_eq!(read_future.await, "Hello, World!".to_owned());
関数を投げるとスケジューラが実行するみたいな雑な理解をする。
で、 その結果実行される poll_event_loop
pub fn poll_event_loop(
&mut self,
cx: &mut Context,
) -> Poll<Result<(), AnyError>> {
// We always poll the inspector if it exists.
let _ = self.inspector.as_mut().map(|i| i.poll_unpin(cx));
self.js_runtime.poll_event_loop(cx)
}
js_runtime::poll_event_loop(cx)
で結局こいつを読む必要がある。
runtime とはいっても、こいつは deno/runtime ではなく、 deno/core/runtime.rs にある。というわけで deno::core をみていく。
とりあえず README.md に deepl 翻訳を掛けて読む。
この Rust クレートには、Deno のコマンドラインインタフェース(Deno CLI)に必要な V8 バインディングが含まれています。 インターフェース(Deno CLI)に必要な V8 バインディングが含まれています。ここでの主な抽象化は、JavaScript を実行する方法を提供する JsRuntime です。 JavaScript を実行する方法を提供する JsRuntime です。
JsRuntime は、実行されたコードのためのイベントループの抽象化を実装しています。 すべての保留中のタスク(非同期処理、動的モジュールのロード)を追跡します。ユーザーの責任は JsRuntime::run_event_loop`メソッドを使用してそのループを駆動するのはユーザーの責任です。 このループは、Rust のフューチャーエグゼキュータ(tokio, smol など)のコンテキストで実行されなければなりません。
Rust の関数を JavaScript にバインドするには、
Deno.core.dispatch()
という関数を使います。 関数を使って、Rust の "dispatch "コールバックをトリガーしてください。ユーザーは以下のことに責任があります。 リクエストとレスポンスの両方を Uint8Array にエンコードする責任があります。
あと TypeScript は core に含まれてない。CLI 層で処理されるので Runtime には出てこない、みたいなことが書いてある。
JsRuntime がコアっぽい。
js_runtime を読む前に、 deno/runtime/worker
の MainWorker がどのように js_runtime を初期化するか確認。
impl MainWorker {
pub fn from_options(
main_module: ModuleSpecifier,
permissions: Permissions,
options: &WorkerOptions,
) -> Self {
// Permissions: many ops depend on this
let unstable = options.unstable;
let perm_ext = Extension::builder()
.state(move |state| {
state.put::<Permissions>(permissions.clone());
state.put(ops::UnstableChecker { unstable });
Ok(())
})
.build();
// Internal modules
let extensions: Vec<Extension> = vec![
// Web APIs
deno_webidl::init(),
deno_console::init(),
deno_url::init(),
deno_web::init(),
deno_file::init(options.blob_url_store.clone(), options.location.clone()),
deno_fetch::init::<Permissions>(
options.user_agent.clone(),
options.ca_data.clone(),
),
deno_websocket::init::<Permissions>(
options.user_agent.clone(),
options.ca_data.clone(),
),
deno_webstorage::init(options.location_data_dir.clone()),
deno_crypto::init(options.seed),
deno_webgpu::init(options.unstable),
deno_timers::init::<Permissions>(),
// Metrics
metrics::init(),
// Runtime ops
ops::runtime::init(main_module),
ops::worker_host::init(options.create_web_worker_cb.clone()),
ops::fs_events::init(),
ops::fs::init(),
ops::http::init(),
ops::io::init(),
ops::io::init_stdio(),
ops::net::init(),
ops::os::init(),
ops::permissions::init(),
ops::plugin::init(),
ops::process::init(),
ops::signal::init(),
ops::tls::init(),
ops::tty::init(),
// Permissions ext (worker specific state)
perm_ext,
];
let mut js_runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(options.module_loader.clone()),
startup_snapshot: Some(js::deno_isolate_init()),
js_error_create_fn: options.js_error_create_fn.clone(),
get_error_class_fn: options.get_error_class_fn,
extensions,
..Default::default()
});
let inspector = if options.attach_inspector {
Some(DenoInspector::new(
&mut js_runtime,
options.maybe_inspector_server.clone(),
))
} else {
None
};
let should_break_on_first_statement =
inspector.is_some() && options.should_break_on_first_statement;
Self {
inspector,
js_runtime,
should_break_on_first_statement,
}
}
// ...
ops::*::init
は飛ばす。
知りたかったのはここ。
let mut js_runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(options.module_loader.clone()),
startup_snapshot: Some(js::deno_isolate_init()),
js_error_create_fn: options.js_error_create_fn.clone(),
get_error_class_fn: options.get_error_class_fn,
extensions,
..Default::default()
});
初期化された snapshot、モジュールローダー等を与えている。
本筋として、 snapshot をどう作っているか知りたかったので、 js::deno_isolate_init()
を読んでいく。
結局 runtime に戻ってきてしまった。snapshot を読み込んでいる部分を追う。
pub fn deno_isolate_init() -> Snapshot {
debug!("Deno isolate init with snapshots.");
let data = CLI_SNAPSHOT;
Snapshot::Static(data)
}
pub static CLI_SNAPSHOT: &[u8] =
include_bytes!(concat!(env!("OUT_DIR"), "/CLI_SNAPSHOT.bin"));
pub fn deno_isolate_init() -> Snapshot {
debug!("Deno isolate init with snapshots.");
let data = CLI_SNAPSHOT;
Snapshot::Static(data)
}
この CLI_SNAPSHOT.bin の置き場所を知りたいので、 print デバッグしてみる。
pub fn deno_isolate_init() -> Snapshot {
debug!("Deno isolate init with snapshots.");
let data = CLI_SNAPSHOT;
println!("{}", concat!(env!("OUT_DIR"), "/CLI_SNAPSHOT.bin"));
Snapshot::Static(data)
}
$ cargo run --example hello_runtime
Blocking waiting for file lock on build directory
Compiling deno_runtime v0.14.0 (/Users/mizchi/gh/github.com/denoland/deno/runtime)
Finished dev [unoptimized + debuginfo] target(s) in 28.45s
Running `/Users/mizchi/gh/github.com/denoland/deno/target/debug/examples/hello_runtime`
/Users/mizchi/gh/github.com/denoland/deno/target/debug/build/deno_runtime-4b7d49413e4a5776/out/CLI_SNAPSHOT.bin
CLI_SNAPSHOT.bin
で grep すると、 runtime/build.rs が引っかかる。
fn main() {
// Skip building from docs.rs.
if env::var_os("DOCS_RS").is_some() {
return;
}
// To debug snapshot issues uncomment:
// op_fetch_asset::trace_serializer();
println!("cargo:rustc-env=TARGET={}", env::var("TARGET").unwrap());
println!("cargo:rustc-env=PROFILE={}", env::var("PROFILE").unwrap());
let o = PathBuf::from(env::var_os("OUT_DIR").unwrap());
// Main snapshot
let runtime_snapshot_path = o.join("CLI_SNAPSHOT.bin");
let js_files = get_js_files("js");
create_runtime_snapshot(&runtime_snapshot_path, js_files);
}
その create_runtime_snapshot
fn create_runtime_snapshot(snapshot_path: &Path, files: Vec<PathBuf>) {
let extensions: Vec<Extension> = vec![
deno_webidl::init(),
deno_console::init(),
deno_url::init(),
deno_web::init(),
deno_file::init(Default::default(), Default::default()),
deno_fetch::init::<deno_fetch::NoFetchPermissions>("".to_owned(), None),
deno_websocket::init::<deno_websocket::NoWebSocketPermissions>(
"".to_owned(),
None,
),
deno_webstorage::init(None),
deno_crypto::init(None),
deno_webgpu::init(false),
deno_timers::init::<deno_timers::NoTimersPermission>(),
];
let js_runtime = JsRuntime::new(RuntimeOptions {
will_snapshot: true,
extensions,
..Default::default()
});
create_snapshot(js_runtime, snapshot_path, files);
}
JsRuntime に will_snapshot フラグを付けて runtime を作っている。
// TODO(bartlomieju): this module contains a lot of duplicated
// logic with `cli/build.rs`, factor out to `deno_core`.
fn create_snapshot(
mut js_runtime: JsRuntime,
snapshot_path: &Path,
files: Vec<PathBuf>,
) {
// TODO(nayeemrmn): https://github.com/rust-lang/cargo/issues/3946 to get the
// workspace root.
let display_root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
for file in files {
println!("cargo:rerun-if-changed={}", file.display());
let display_path = file.strip_prefix(display_root).unwrap();
let display_path_str = display_path.display().to_string();
js_runtime
.execute(
&("deno:".to_string() + &display_path_str.replace('\\', "/")),
&std::fs::read_to_string(&file).unwrap(),
)
.unwrap();
}
let snapshot = js_runtime.snapshot();
let snapshot_slice: &[u8] = &*snapshot;
println!("Snapshot size: {}", snapshot_slice.len());
std::fs::write(&snapshot_path, snapshot_slice).unwrap();
println!("Snapshot written to: {} ", snapshot_path.display());
}
これらの runtime/js/*.js
が順番に execute される。
$ ls js
01_build.js 30_os.js 40_process.js
01_errors.js 40_compiler_api.js 40_read_file.js
01_version.js 40_diagnostics.js 40_signals.js
01_web_util.js 40_error_stack.js 40_testing.js
06_util.js 40_files.js 40_tls.js
11_workers.js 40_fs_events.js 40_tty.js
12_io.js 40_http.js 40_write_file.js
13_buffer.js 40_net_unstable.js 41_prompt.js
30_fs.js 40_performance.js 90_deno_ns.js
30_metrics.js 40_permissions.js 99_main.js
30_net.js 40_plugins.js README.md
js_runtime::snapshot
でバイナリが吐き出されて、それを先程確認したパスに書き込んでいる。
let snapshot = js_runtime.snapshot();
let snapshot_slice: &[u8] = &*snapshot;
println!("Snapshot size: {}", snapshot_slice.len());
std::fs::write(&snapshot_path, snapshot_slice).unwrap();
println!("Snapshot written to: {} ", snapshot_path.display());
結局後回しにした js_runtime.snapshot()
を読むことになる。
本腰いれて読んでいく。
impl JsRuntime {
/// Only constructor, configuration is done through `options`.
pub fn new(mut options: RuntimeOptions) -> Self {
let v8_platform = options.v8_platform.take();
static DENO_INIT: Once = Once::new();
DENO_INIT.call_once(move || v8_init(v8_platform));
// ...
全体初期化を一回だけやる。
fn v8_init(v8_platform: Option<v8::UniquePtr<v8::Platform>>) {
// Include 10MB ICU data file.
#[repr(C, align(16))]
struct IcuData([u8; 10413584]);
static ICU_DATA: IcuData = IcuData(*include_bytes!("icudtl.dat"));
v8::icu::set_common_data(&ICU_DATA.0).unwrap();
let v8_platform = v8_platform
.unwrap_or_else(v8::new_default_platform)
.unwrap();
v8::V8::initialize_platform(v8_platform);
v8::V8::initialize();
let flags = concat!(
// TODO(ry) This makes WASM compile synchronously. Eventually we should
// remove this to make it work asynchronously too. But that requires getting
// PumpMessageLoop and RunMicrotasks setup correctly.
// See https://github.com/denoland/deno/issues/2544
" --experimental-wasm-threads",
" --no-wasm-async-compilation",
" --harmony-top-level-await",
" --harmony-import-assertions",
" --no-validate-asm",
);
v8::V8::set_flags_from_string(flags);
}
ICU という概念があるらしいが、Chrome と v8 のプロセス間通信のファイルソケットかなにかだっけ?ググっても引っかからないが自明なものとして扱われてる…
https://chromium.googlesource.com/chromium/deps/icu46/
ry が wasm の初期化は async にしたいねみたいなコメントを残してる。
ICU のことは一旦忘れて、 v8 が起動した前提で読みすすめる
let has_startup_snapshot = options.startup_snapshot.is_some();
will_snapshot: true
のときの処理
let (mut isolate, maybe_snapshot_creator) = if options.will_snapshot {
// TODO(ry) Support loading snapshots before snapshotting.
assert!(options.startup_snapshot.is_none());
let mut creator =
v8::SnapshotCreator::new(Some(&bindings::EXTERNAL_REFERENCES));
let isolate = unsafe { creator.get_owned_isolate() };
let mut isolate = JsRuntime::setup_isolate(isolate);
{
let scope = &mut v8::HandleScope::new(&mut isolate);
let context = bindings::initialize_context(scope);
global_context = v8::Global::new(scope, context);
creator.set_default_context(context);
}
(isolate, Some(creator))
} else {
bindings::EXTERNAL_REFERENCES
を引数に、v8::SnapshotCreator を初期化。
これは snapshot 外の API を予約するみたいなやつかな。
lazy_static::lazy_static! {
pub static ref EXTERNAL_REFERENCES: v8::ExternalReferences =
v8::ExternalReferences::new(&[
v8::ExternalReference {
function: opcall.map_fn_to()
},
v8::ExternalReference {
function: set_macrotask_callback.map_fn_to()
},
v8::ExternalReference {
function: eval_context.map_fn_to()
},
v8::ExternalReference {
function: queue_microtask.map_fn_to()
},
v8::ExternalReference {
function: encode.map_fn_to()
},
v8::ExternalReference {
function: decode.map_fn_to()
},
v8::ExternalReference {
function: serialize.map_fn_to()
},
v8::ExternalReference {
function: deserialize.map_fn_to()
},
v8::ExternalReference {
function: get_promise_details.map_fn_to()
},
v8::ExternalReference {
function: get_proxy_details.map_fn_to()
},
v8::ExternalReference {
function: memory_usage.map_fn_to(),
},
]);
}
(この後色々やったがログに残ってない)
これで動いた。
use rusty_v8 as v8;
use std::mem::forget;
fn main() {
let platform = v8::new_default_platform().unwrap();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
let snapshot;
// --- create snapshot ---
{
let mut creator = v8::SnapshotCreator::new(None);
let mut isolate = unsafe { creator.get_owned_isolate() };
{
let scope = &mut v8::HandleScope::new(&mut isolate);
let context = v8::Context::new(scope);
let src = r#"
function add1(n) { return n + 1; }
x = 1;
y = 2;
add1(x + y);
"#;
{
let scope = &mut v8::ContextScope::new(scope, context);
let code = v8::String::new(scope, src).unwrap();
let script = v8::Script::compile(scope, code, None).unwrap();
let result = script.run(scope).unwrap();
assert_eq!(result.to_rust_string_lossy(scope), "4");
}
creator.set_default_context(context);
}
snapshot = creator.create_blob(v8::FunctionCodeHandling::Keep).unwrap();
let snapshot_slice: &[u8] = &*snapshot;
println!("Snapshot size: {} kb", snapshot_slice.len() / 1024);
forget(isolate);
}
// --- restore snapshot ---
{
let mut isolate = v8::Isolate::new(v8::Isolate::create_params().snapshot_blob(snapshot));
let scope = &mut v8::HandleScope::new(&mut isolate);
let context = v8::Context::new(scope);
{
let scope = &mut v8::ContextScope::new(scope, context);
let code = v8::String::new(scope, "add1(x + y)").unwrap();
let script = v8::Script::compile(scope, code, None).unwrap();
let result = script.run(scope).unwrap();
assert_eq!(result.to_rust_string_lossy(scope), "4");
}
}
}