Denoのコードを読んでみました。
Rust に入門したばかりで基礎知識が足らず四苦八苦していますが、Deno のプロセスが起動してから TypeScript のコードが実行されるまでの仕組みについて愚直に読んでみたメモです。
コア内部を理解するには非公式ガイド(以下ガイド)がとても参考になります。
Deno のディレクトリ構成やレイヤー分けについてはRepo StructureとInfrastructureを一読し、リポジトリの構造をざっくり把握してからコードを読み始めるとより捗ると思います。
また、すでにmizchiさんがDeno のコードを読んだメモを上げてました。そちらもとても参考になりました。
まず Deno のビルドツールについて軽く触れます。
Deno ではgnという Node.js でおなじみの gyp の次期バージョンをビルドツールに用いています。
What is GN? GN is a meta-build system that generates NinjaBuild files. It’s meant to be faster and simpler than GYP. It outputs only Ninja build files.
コードを読む上でビルド設定のすべてを理解する必要はありませんが、これを読まないと次に実行されるコードがわからない、このコードがどこから出現したかわからないってことが結構あるので、必要に応じてビルド設定も併せて読むと読み進められると思います。
deno
コマンドを実行したときに真っ先に実行されるファイルはsrc/main.rsです。ビルドツールでdeno
コマンドを生成する際のエントリポイントとしてsrc/main.rs
が指定されています。
rust_executable("deno") {
source_root = "src/main.rs"
main.rs の詳細に入る前にmain
関数のアウトラインを整理し、用語の補足をしてから次に進みます。またガイドのRust main() Entry Pointにも説明があるので併せてご覧ください。
--help
オプションをチェックし、あればヘルプを表示して終了Arc<IsolateState>
インスタンスを生成snapshot::deno_snapshot
で V8 スナップショットを取得tokio_util::init
で Tokio を初期化denoMain();
を評価するという流れになっています。用語を補足してから詳細を読み進めます。すでに知ってる方は先へお進み下さい。
ロガーや--help
周りはコード量も少ないしシンプルなので解説は割愛します。またイベントループに関してはかなり長くなると判断したので本記事では割愛します。イベントループについてはガイドのisolate.event_loop()にて詳しく説明されているのでそちらを参照して下さい。
Isolate は V8 が提供している API の1つです。
Deno にこれをラップした isolate.rs などがありますが、末端までコードを読むと結局は V8 の Isolate を操作しています。
Isolate An isolate is a concept of an instance in V8. In Blink, isolates and threads are in 1:1 relationship. One isolate is associated with the main thread. One isolate is associated with one worker thread. An exception is a compositor worker where one isolate is shared by multiple compositor workers.
— VM よりコンテナよりもさらに軽量な分離技術、V8 の Isolate を用いてサーバレスコンピューティングを提供する Cloudflare Workers - Publickey
V8 には生成された Isolate Context の状態をシリアライズしておき、それをデシリアライズして利用することでオーバーヘッドを削減し Isolate Context の生成を高速化する スナップショットの機能があります。
Node.js でも V8 スナップショットを使用してプロセスの起動を高速化する RFC が#17058にて議論されています。
その Issue のより詳細な資料Speeding up Node.js startup using V8 snapshotによると V8 スナップショットを使うことで、プロセスの起動時間を最大で2桁倍高速化できる見立てがあるようです。
「このタイプの callback 関数があるから、この Object を用意して、この値を設定して」とやっていくのは効率が良くありません。「一通り定義したらこれだけのメモリが必要で、こんなレイアウトになってるから」という感じにできないでしょうか? そこで我々は V8 が用意してくれていた Snapshot の機能を使って効率化することにしました。ちなみに純粋な V8 でも同様に Context を作る度に Math などの Built-in object を作るのが非効率ということで、この Snapshot 機能を作ったという背景があります。
Arc って名前から全くピンとこなかったのですが、Atomically Reference Counted の略だそうです。
Atomic がついてないシングルスレッド版のrcという crate もあるようです。
A thread-safe reference-counting pointer. ‘Arc’ stands for ‘Atomically Reference Counted’.
スレッドをまたいで参照を共有するために、Rust は
Arc<T>
というラッパ型を提供しています。— 並行性
私もきちんと理解できていないので引用だけにとどめます。
tokio 周りは読み飛ばしてもコードの流れは追えます。どの処理で・どんな単位で並列化/多重化してるかについてしっかり読む場合は tokio と Futures の理解が必要になります。
Tokio is an event-driven, non-blocking I/O platform for writing asynchronous applications with the Rust programming language.
futures::{Future, Stream} で実装された非同期かつゼロコストなグリーンスレッドを使ってネットワークプログラミングするためのフレームワーク
それでは main のアウトラインに沿って詳細を読んでいきます。
まずコマンドライン引数のパース周りの処理はsrc/flags.rsにあります。
パース処理は、まず V8 用のオプションのパースから実行されます。v8_set_flags
関数です。この関数で V8 用のオプションとそれ以外(Deno の引数)を分離し、V8 用の引数はlibdeno/api.cc のdeno_set_v8_flags
に渡し、Deno 用の引数は後続の処理に引き継ぎます。
libdeno::deno_set_v8_flags
が定義されているのはlibdeno/api.ccです。
void deno_set_v8_flags(int* argc, char** argv) {
v8::V8::SetFlagsFromCommandLine(argc, argv, true);
}
C++で定義された関数を Rust から呼び出すための FFI の定義がsrc/libdeno.rsに書かれています。
extern "C" {
pub fn deno_init();
pub fn deno_v8_version() -> *const c_char;
pub fn deno_set_v8_flags(argc: *mut c_int, argv: *mut *mut c_char);
V8 のオプションか否かを判定する方法はコメントによるとlibdeno::deno_set_v8_flags
内部で呼び出されているv8::V8::SetFlagsFromCommandLine
の内部に書かれており、引数で渡したコマンドラインオプションのうち、V8 が解釈できたオプションだけ取り除かれるという破壊的操作になってるようですようです。
// deno_set_v8_flags(int* argc, char** argv) mutates argc and argv to remove
// flags that v8 understands.
// First parse core args, then convert to a vector of C strings.
V8 が解釈できなかった残りのオプションのパース処理は Rust のgetoptsという crate をラップした独自関数を用いています getopts は予期しないオプションが渡されたときにエラー扱いになるのですが、そのエラーを自前でハンドルするためにラップしたset_recognized_flags
という関数を使用しています。“better solution welcome!”だそうです。なお Deno に指定可能なオプションはset_flags
関数を読めばわかります。
// getopts doesn't allow parsing unknown options so we check them
// one-by-one and handle unrecognized ones manually
// better solution welcome!
Arc<IsolateState>
インスタンスを生成isolate::IsolateState::new
はsrc/isolate.rsに定義されていますが、これ自体はただの構造体を初期化してるだけだったので詳細を割愛。
snapshot::deno_snapshot
はsrc/snapshot.rsに定義されています。
pub fn deno_snapshot() -> deno_buf {
#[cfg(not(feature = "check-only"))]
let data =
include_bytes!(concat!(env!("GN_OUT_DIR"), "/gen/snapshot_deno.bin"));
// The snapshot blob is not available when the Rust Language Server runs
// 'cargo check'.
#[cfg(feature = "check-only")]
let data = vec![];
unsafe { deno_buf::from_raw_parts(data.as_ptr(), data.len()) }
}
{GN_OUT_DIR}/gen/snapshot_deno.bin
というファイルから V8 スナップショットを取得しています。このファイルは gn でビルドする時に生成されているようです。環境変数GN_OUT_DIR
の値はデバッグビルドした環境ではtarget/debug
になってました。
スナップショットを生成するビルドタスクは
BUILD.gn のsnapshot("snapshot_deno")
に定義があります。snapshot
というテンプレートはlibdeno/deno.gniに定義されていました。.gni
は GN のビルド設定を別ファイルに定義して import できるようにしたものだそうです。
You can import .gni files into the current scope with the import function.
libdeno/deno.gni
の中にtool = "//libdeno:snapshot_creator"
と指定があるように、V8 スナップショットを生成するファイルはlibdeno/snapshot_create.ccです。main 関数に渡される引数はビルド定義を読んでみると第一引数が出力先(gen/snapshot_deno.bin
)のパス、第二引数が スナップショットを取りたい js(bundle/main.js
)のパスになっていました。
main 関数の内部処理としては、deno_execute
で与えられた js(bundle/main.js
)を実行した結果の Isolate Context の スナップショットをdeno_get_snapshot
で取得し、指定されたパス(gen/snapshot_deno.bin
)に保存するという感じです。
inputs = [
invoker.source_root,
]
outputs = [
snapshot_out_bin,
]
args = rebase_path(outputs, root_build_dir) +
rebase_path(inputs, root_build_dir)
なお前触れなく登場したbundle/main.js
とはrollupで生成された Deno の TypeScript のコード bundle した生成物です。
$ head -n5 target/debug/gen/bundle/main.js
var denoMain = (function () {
'use strict';
var runner = /*#__PURE__*/Object.freeze({
get Runner () { return Runner; }
ここからさらにビルド周りを深追いしていくと submodule になっているdenoland/chromium_buildなどの別リポジトリにたどり着くのですが、ビルドプロセスを深堀りしすぎると本筋から逸脱するのでこれぐらいにします。
isolate::Isolate::new
もIsolateState
と同じくsrc/isolate.rs
に定義されています。
渡している引数は上の行で生成した V8 snapshot、2つ上の行で生成した IsolateState、opt::dispatch
の3つです。opt::dispatch
に関してはひとまずこちらを参照。後々denoMain
関数を実行するあたりでまた出てきます。
第三引数の dispatch ってなんだ、と思ったらコメントに色々書いてある。
js からの来る諸々を Rust で捌いてる部分っぽく見える。
isolate::Isolate::new
のアウトラインは、
libdeno::deno_init()
で初期化libdeno::deno_config
のインスタンスを生成deno_config
をdeno_new
に渡してlibdeno_isolate
のインスタンスを生成という感じになっています。
libdeno::deno_init()
は V8 の初期化をしています。
deno_new
はlibdeno/api.ccに定義されています。
先程のスナップショットを生成する処理でも登場しており、そのときはwill_snapshot
が 1、load_snapshot
がdeno::empty_buf
になっていました。今回はdeno_new
にwill_snapshot
を 0、load_snapshot
に生成された スナップショットを指定しています。それぞれの値の違いによる処理の分岐はこのあたりを見れば明らかかと思います。
Deno* deno_new(deno_config config) {
if (config.will_snapshot) {
return deno_new_snapshotter(config);
}
deno::DenoIsolate* d = new deno::DenoIsolate(config);
v8::Isolate::CreateParams params;
params.array_buffer_allocator = d->array_buffer_allocator_;
params.external_references = deno::external_references;
if (config.load_snapshot.data_ptr) {
params.snapshot_blob = &d->snapshot_;
}
スナップショットを用いてV8::Isolate
のインスタンスを作ってスコープ設定して…とわちゃわちゃやってますが、このあたりは V8 自体の説明になってしまうので割愛します。この記事とガイドにて丁寧な解説されているので理解の助けになると思います。
There are 2 important functions/constructors used in deno_new that might not be immediately clear: DenoIsolate and InitializeContext. It turns out DenoIsolate serves more or less as a collection of Isolate information. Instead, InitializeContext is the more interesting one.
denoMain
関数を実行
// Setup runtime.
isolate
.execute("denoMain();")
.unwrap_or_else(print_err_and_exit);
ここですね。isolate.execute
自体はごくシンプルで、内部でibdeno::deno_execute
を読んでるだけです。
CString って C にわたす FFI 呼ぶときによく見るやつだ。 実質
libdeno::deno_execute()
へのファサードになっている。
The only interesting function we care in this section is libdeno::deno_execute. We can find its actual definition in libdeno/api.cc again:
deno_execute
は スナップショット作るところで1度登場していますが、そこでは読み飛ばしたので詳しく読んでみます。V8 Context を作ってdeno::Execute
に渡してます。
deno::Execute
はlibdeno/binding.ccに定義されています。
おおまかな流れとしては渡された文字列をv8::Script::Compile
でコードをコンパイルし、エラーがなければLocal<Script>->Run(context)
でコードを実行して、エラーチェックして終わりです。
bool Execute(v8::Local<v8::Context> context, const char* js_filename,
const char* js_source) {
auto* isolate = context->GetIsolate();
v8::Isolate::Scope isolate_scope(isolate);
v8::HandleScope handle_scope(isolate);
v8::Context::Scope context_scope(context);
auto source = v8_str(js_source, true);
auto name = v8_str(js_filename, true);
v8::TryCatch try_catch(isolate);
v8::ScriptOrigin origin(name);
auto script = v8::Script::Compile(context, source, &origin);
if (script.IsEmpty()) {
DCHECK(try_catch.HasCaught());
HandleException(context, try_catch.Exception());
return false;
}
auto result = script.ToLocalChecked()->Run(context);
if (result.IsEmpty()) {
DCHECK(try_catch.HasCaught());
HandleException(context, try_catch.Exception());
return false;
}
return true;
}
まとめると、isolate.execute("denoMain();")
はdenoMain();
という文字列(JavaScript のコード)を V8 でコンパイルして実行しています。
いきなりdenoMain
関数を呼び出してますがdenoMain
とはどこから来たのでしょうか。これまでのコードでは denoMain 関数を定義するようなコードは登場しませんでした。おそらく V8 スナップショットから復元されています。
あらかじめdenoMain
の関数定義を含んだ bundle/main.js を実行した結果をスナップショットとして保存し、それを使って Isolate を復元しているのでdenoMain
関数が呼べるようになっています。
前触れなく登場している
bundle/main.js
とは、js の bundler の1つであるrollupで生成されたdenoMain
関数を含む deno の js レイヤのコードです。
引用したこのあたりです。demoMain 関数を呼び出せる仕組みがわかったので denoMain について詳しく読んでみます。
export default function denoMain() {
要はここまでやって v8 が起動していることがわかった。じゃあどういうスクリプトが起動しているのか。
やっと Deno の TypeScript のレイヤにたどり着けました。といっても実際には rollup で bundle された JavaScript が実行されており TypeScript を直接実行しているわけではありません。
denoMain
関数が定義されているjs/main.tsは大きく分けて3パートに分かれています。
denoMain
は(コマンドライン)引数を処理して、sendStart
をコールするなどの然るべき初期化を行います。
sendStart
はsendSync
という関数を用いて js レイヤの初期化が開始したことを C++レイヤへ通知しています。
compilerMain
はここでは呼び出されていません。関数定義だけして window オブジェクトのプロパティにセットしています。この関数は後々 ts をコンパイルするところで再び登場します。
libdeno.recv(handleAsyncMsgFromRust);
denoMain 関数の冒頭にあるlibdeno.recv
とhandleAsyncMsgFromRust
は、イベントリスナとイベントハンドラのようなものです。
libdeno
オブジェクトFirst, we tell libdeno, which is the exposed API from the middle-end, that whenever there is a message sent from the Rust side, please forward the buffer to a function called handleAsyncMsgFromRust. Then, a Start message is sent to Rust side, signaling that we are starting and receiving information including the current working directory, or cwd. We then decide whether the user is running a script, or using the REPL. If we have an input file name, Deno would then try to let the runner, which contains the TypeScript compiler, to try running the file (going deep into its definition, you’ll eventually find a secret eval/globalEval called somewhere). denoMain only exits when the runner finish running the file.
denoMain の中にlibdeno
という名前空間的なオブジェクトが登場しますが、このオブジェクトの定義はjs/libdeno.tsには存在しません。どこかで事前に作られています。
libdeno オブジェクトはlibdeno/api.cc
のdeno_new
関数の内部処理で読み飛ばしたInitializeContext
にて C++から V8 を直接操作して生成されています。
auto deno_val = v8::Object::New(isolate);
CHECK(global->Set(context, deno::v8_str("libdeno"), deno_val).FromJust());
これでlibdeno
という空オブジェクトを作成し、その中にprint
, recv
, send
メソッドの実装をセットしています。
auto print_tmpl = v8::FunctionTemplate::New(isolate, Print);
auto print_val = print_tmpl->GetFunction(context).ToLocalChecked();
CHECK(deno_val->Set(context, deno::v8_str("print"), print_val).FromJust());
auto recv_tmpl = v8::FunctionTemplate::New(isolate, Recv);
auto recv_val = recv_tmpl->GetFunction(context).ToLocalChecked();
CHECK(deno_val->Set(context, deno::v8_str("recv"), recv_val).FromJust());
auto send_tmpl = v8::FunctionTemplate::New(isolate, Send);
auto send_val = send_tmpl->GetFunction(context).ToLocalChecked();
CHECK(deno_val->Set(context, deno::v8_str("send"), send_val).FromJust());
見ての通りこの3つの実装は C++で定義されており、V8 を操作して C++の関数ポインタを js 側に露出しています。
print
はここで定義されており、ざっくりまとめるとSTDOUT
もしくはSTDERR
のどちらかに渡された引数を fwrite している。要は printf 的なものです。
recv
はここに定義されており、ざっくりまとめると渡された引数を関数としてキャストし、DenoIsolate のrecv_
プロパティにコールバック関数としてセットしています。
send
はここに定義されています。src/isolate.rs
の中でdeno_new
関数の中で渡しているdeno_config
のrecv_cb
がDenoIsolateに渡っており、結果的にpre_dispatchという Rust の関数が呼び出されます。
という C++(Rust)レイヤとやりとりするためのlibdeno
オブジェクトが定義されていました。
Send に関してはガイドの説明も併せて参照してください。
The Send function get args and invoke the recvcb . Notice that the recvcb is defined in the Rust code.
denoMain
が実行され、Deno ランタイムの準備が整いました。
再び Rust の main 関数に話題を戻し、次はファイルパスがコマンドライン引数として与えられてるときisolate.execute_mod
が呼び出されるあたりを読みます。
これまでも FFI で C++, Rust, js と複数レイヤをまたぐコードが多かったですが、ここから先も複雑かつ長いので、休憩しつつ読まれるといいと思います。
// Execute input file.
if isolate.state.argv.len() > 1 {
let input_filename = &isolate.state.argv[1];
isolate
.execute_mod(input_filename, should_prefetch)
.unwrap_or_else(print_err_and_exit);
}
isolate.execute_mod
はsrc/isolate.rsに定義されています。
isolate.execute
と同じ要領でdeno_execute(filename, ファイルの中身)
と実行するのかと思いましたが、違いました。内部で呼び出されているcode_fetch_and_maybe_compile
という関数とlibdeno::deno_execute_mod
が気になります。まずcode_fetch_and_maybe_compile
から読んでみます。
/// Executes the provided JavaScript module.
pub fn execute_mod(
&self,
js_filename: &str,
is_prefetch: bool,
) -> Result<(), JSError> {
let out =
code_fetch_and_maybe_compile(&self.state, js_filename, ".").unwrap();
let filename = CString::new(out.filename.clone()).unwrap();
let filename_ptr = filename.as_ptr() as *const i8;
let js_source = CString::new(out.js_source().clone()).unwrap();
let js_source = CString::new(js_source).unwrap();
let js_source_ptr = js_source.as_ptr() as *const i8;
let r = unsafe {
libdeno::deno_execute_mod(
self.libdeno_isolate,
self.as_raw_ptr(),
filename_ptr,
js_source_ptr,
if is_prefetch { 1 } else { 0 },
)
};
if r == 0 {
let js_error = self.last_exception().unwrap();
return Err(js_error);
}
Ok(())
}
code_fetch_and_maybe_compile
はsrc/isolate.rsに定義されています。
fn code_fetch_and_maybe_compile(
state: &Arc<IsolateState>,
specifier: &str,
referrer: &str,
) -> Result<CodeFetchOutput, DenoError> {
let mut out = state.dir.code_fetch(specifier, referrer)?;
1行目のstate.dir.code_fetch
は型定義を読んでみるとdeno_dir::DenoDir
というメソッドです。src/deno_dir.rsに定義されています。DenoDir#code_fetch
のアウトラインとしては、
resolve_module
でモジュールのファイルパス(か URL)を解決get_source_code
で/https?/
で始まるならfetch_remote_source
、それ以外ならfetch_local_source
を使用して TypeScript のコードを取得filter_shebang
で shebang を取り除いてload_cache
でコンパイル後の JavaScript とその Source Map を手に入れるとなっております。
ここから先resolve_module
やそれ以外の箇所でspecifier
、referrer
という変数がよく登場します。specifier
はモジュールの名前・パス解決の処理全般で、import from
に記載した文字列(もしくはコマンドライン引数で与えたファイルパス)を現しています。referrer
は import が記載されているファイルの絶対パス(もしくは URL)と念頭に置いておくとコードが読みやすいと思います。
get_source_code
にてローカルかリモートかにかかわらず返却されているCodeFetchOutput
型の構造は以下のとおりです。
module_name
: specifierfilename
: 解決できたファイル名(もしくは URL)media_type
: msg::MediaType::TypeScript
、msg::MediaType::JavaScript
、msg::MediaType::Json
、msg::MediaType::Unknown
のいずれかsource_code
: ファイル内容maybe_output_code
: (fetch_local_source
の場合常に None)maybe_source_map
: (fetch_local_source
の場合常に None)ローカルのファイルパスを解決するfetch_local_source
の面白い挙動としては、ファイル名.mime
というファイルがもし置かれていれば、そのメディアタイプとして解釈する挙動になっています。ローカルファイルでこれをわざわざやる必要は無さそうですが、リモートのファイルとを取ってくるときに Content-Type ヘッダの値を格納しておくという使い方をしています。
なお.mime
ファイルがなければファイルの拡張子ごとにあらかじめ定義されているタイプとして解釈します。例えば.ts
は TypeScript として解釈します。
リモートの URL を解決するfetch_remote_source
ではソースを HTTP GET してきて、URL 末尾の拡張子に対応するメディアタイプと、レスポンスヘッダのContent-Type
ヘッダを比較し、もし食い違っていたら前述の.mime
タイプを作成し拡張子よりもContent-Type
の値をリスペクトするという処理になってます。
eprint!("Downloading {}...", &module_name); // no newline
let maybe_source = http_util::fetch_sync_string(&module_name);
if let Ok((source, content_type)) = maybe_source {
eprintln!(""); // next line
match p.parent() {
Some(ref parent) => fs::create_dir_all(parent),
None => Ok(()),
}?;
deno_fs::write_file(&p, &source, 0o666)?;
// Remove possibly existing stale .mime file
// may not exist. DON'T unwrap
let _ = std::fs::remove_file(&media_type_filename);
// Create .mime file only when content type different from extension
let resolved_content_type = map_content_type(&p, Some(&content_type));
let ext = p
.extension()
.map(|x| x.to_str().unwrap_or(""))
.unwrap_or("");
let media_type = extmap(&ext);
if media_type == msg::MediaType::Unknown
|| media_type != resolved_content_type
{
deno_fs::write_file(&mt, content_type.as_bytes(), 0o666)?
}
コードを取得するための HTTP クライアントはhyperというものを利用していました。低レベルだけど高速に動作する HTTP クライアントのようです。例えばリダイレクトのフォローを自前で書かないといけないくらいシンプルなクライアントです。コードにも TODO が書かれているんですが、現在のコードでは最大リダイレクト数が設定されておらず、リダイレクトループが発生するとハングするコードになってます。
// TODO(kevinkassimo): consider set a max redirection counter
// to avoid bouncing between 2 or more urls
TypeScript のコードの取得は完了しました。次に V8 で実行できるように JavaScript にコンパイルする必要があります。
code_fetch_and_maybe_compile
のDenoDir::code_fetch
を読みきったので続きを読んでいきます。
fn code_fetch_and_maybe_compile(
state: &Arc<IsolateState>,
specifier: &str,
referrer: &str,
) -> Result<CodeFetchOutput, DenoError> {
let mut out = state.dir.code_fetch(specifier, referrer)?;
if (out.media_type == msg::MediaType::TypeScript
&& out.maybe_output_code.is_none())
|| state.flags.recompile
{
debug!(">>>>> compile_sync START");
out = compile_sync(state, specifier, &referrer).unwrap();
debug!(">>>>> compile_sync END");
}
Ok(out)
}
わかりやすくログが出てますが、compile_sync
が ts を js にコンパイルする関数ですね。compile_sync
の中身も追います。
pub fn compile_sync(
parent_state: &Arc<IsolateState>,
specifier: &str,
referrer: &str,
) -> Option<CodeFetchOutput> {
let req_msg = req(specifier, referrer);
let compiler = lazy_start(parent_state);
let send_future = resources::worker_post_message(compiler.rid, req_msg);
send_future.wait().unwrap();
let recv_future = resources::worker_recv_message(compiler.rid);
let res_msg = recv_future.wait().unwrap().unwrap();
let res_json = std::str::from_utf8(&res_msg).unwrap();
CodeFetchOutput::from_json(res_json)
}
内部で呼び出されているlazy_start
と、更にその中でコールされているworker::spawn
も追います。
fn lazy_start(parent_state: &Arc<IsolateState>) -> Resource {
let mut cell = C_RID.lock().unwrap();
let rid = cell.get_or_insert_with(|| {
let resource =
workers::spawn(parent_state.clone(), "compilerMain()".to_string());
resource.rid
});
Resource { rid: *rid }
}
pub fn spawn(
state: Arc<IsolateState>,
js_source: String,
) -> resources::Resource {
// TODO This function should return a Future, so that the caller can retrieve
// the JSError if one is thrown. Currently it just prints to stderr and calls
// exit(1).
// let (js_error_tx, js_error_rx) = oneshot::channel::<JSError>();
let (p, c) = oneshot::channel::<resources::Resource>();
let builder = thread::Builder::new().name("worker".to_string());
let _tid = builder
.spawn(move || {
let (worker, external_channels) = Worker::new(&state);
let resource = resources::add_worker(external_channels);
p.send(resource.clone()).unwrap();
tokio_util::init(|| {
(|| -> Result<(), JSError> {
worker.execute("denoMain()")?;
worker.execute("workerMain()")?;
worker.execute(&js_source)?;
worker.event_loop()?;
Ok(())
})().or_else(|err: JSError| -> Result<(), JSError> {
eprintln!("{}", err.to_string());
std::process::exit(1)
}).unwrap();
});
resource.close();
}).unwrap();
c.wait().unwrap()
}
worker::spawn の中worker.execute
にてdenoMain()
, workerMain()
, 引数で渡されたコード
を実行するようになっています。引数で渡されたコードは何かというとlazy_start
を見ての通りcompilerMain()
です。
Worker は main 関数で生成された V8 Isolate とは隔離された別の Isolate を生成しています。この Isolate もまたスナップショットから復元されているので高速です。
これら3つの関数が実行されたあとに、req 関数で生成したspecifier
とreferrer
を worker に送信し、TypeScript のコンパイルを実行します。
次にcompilerMain
の定義を追ってみます。
// provide the "main" function that will be called by the privilaged side when
// lazy instantiating the compiler web worker
window.compilerMain = function compilerMain() {
// workerMain should have already been called since a compiler is a worker.
const encoder = new TextEncoder();
const decoder = new TextDecoder();
compiler.recompile = startResMsg.recompileFlag();
log(`recompile ${compiler.recompile}`);
window.onmessage = ({ data }: { data: Uint8Array }) => {
const json = decoder.decode(data);
const lookup = JSON.parse(json) as CompilerLookup;
const moduleMetaData = compiler.compile(lookup.specifier, lookup.referrer);
const responseJson = JSON.stringify(moduleMetaData);
const response = encoder.encode(responseJson);
postMessage(response);
};
};
Compiler の compile メソッドはjs/compiler.tsに定義されています。コードがやや長いので引用はしません。
大まかな処理内容としては、typescript
モジュールを使ってコンパイルしたり型検査したりしてます。JSON に関してはjsonEsmTemplate
という薄いラッパーを噛ませて独自コンパイルしています。それ以外(TypeScript, JavaScript)に関しては、LanguageService のgetEmitOutput
を用いてソースマップとコンパイル後の JavaScript を入手し、その後に型検査 etc を実行しもしエラーがあれば異常終了、エラーがなければ JavaScript にソースマップをくっつけてキャッシュに書き込んで完了という処理になっています。
検査する項目は独自に絞って処理の高速化を図っています。このやり方のほうが 3 倍ほど通常の検査より高速だそうです。
// Get the relevant diagnostics - this is 3x faster than
// `getPreEmitDiagnostics`.
const diagnostics = [
// TypeScript is overly opinionated that only CommonJS modules kinds can
// support JSON imports. Allegedly this was fixed in
// Microsoft/TypeScript#26825 but that doesn't seem to be working here,
// so we will ignore complaints about this compiler setting.
...service
.getCompilerOptionsDiagnostics()
.filter(diagnostic => diagnostic.code !== 5070),
...service.getSyntacticDiagnostics(fileName),
...service.getSemanticDiagnostics(fileName)
];
なお TypeScript の LanguageService の詳しい説明は公式の wikiにあったのでそちらを参照して下さい。
JavaScript へのコンパイルが完了し、ようやく指定されたモジュールを実行できるようになりました。最後にlibdeno::deno_execute_mod
を読みます。libdeno::deno_execute_mod
はlibdeno/api.ccに定義されています。実質的な処理はdeno::ExecuteMod
に書いてあります。deno::ExecuteMod
はlibdeno/binding.ccに定義されています。V8 で JavaScript のコードを実行するくだりはdeno::Execute
のときに書いた説明を参照して下さい。
auto maybe_module = CompileModule(context, js_filename, source);
if (maybe_module.IsEmpty()) {
DCHECK(try_catch.HasCaught());
HandleException(context, try_catch.Exception());
return false;
}
DCHECK(!try_catch.HasCaught());
auto module = maybe_module.ToLocalChecked();
auto maybe_ok = module->InstantiateModule(context, ResolveCallback);
if (maybe_ok.IsNothing()) {
return false;
}
CHECK_EQ(v8::Module::kInstantiated, module->GetStatus());
if (resolve_only) {
return true;
}
auto result = module->Evaluate(context);
ここまでの内容で、deno コマンドを実行し、TypeScript が実行されるまでの処理が追えたと思います。
$ cat tests/002_hello.ts
console.log("Hello World");
$ ./target/debug/deno tests/002_hello.ts
Hello World
実行されるすべての行を網羅したわけではありませんが、ビルドプロセスも含めて一連の流れを追えたと思います。Rust、C++、TypeScript とレイヤをまたぐ処理がかなり多いので、わかってしまえばすんなり読めますが、初見はかなり大変でした。
C++は V8 以外の処理をなるべく書かず意図的に Rust 側に寄せているように感じました。Rust で書いたほうが自分の足を撃ちにくいプログラムになると思うので、これが功を奏すのかどうか、1~2 年後が楽しみです。
記事内ではあまり触れてませんが、コードを読んでる過程で TODO や FIXME コメントが大量に見つかったので、Deno コアに対するコントリビュートのネタは、コード読むとたくさん見つかると思います。
他にも Worker の実装、セキュリティモデルの実装、TypeScript 製の外部モジュールの依存解決、イベントループや TaskQueue 関連の Node.js との差異、ビルドプロセスの詳細など、まだまだ読みたい Deno のコードがあるので、読んで理解できたら記事書きます。