deno のコードを読んだメモ。
そこまで大きなプロジェクトでもないので、rust と cpp そこまで習熟してなくても読めるだろうという気持ち。
https://denolib.gitbook.io/guide/installing-deno
起動プロセスっぽいところ。
// src/main.rs
fn main() {
// ... ロガーやフラグのの設定とか
let state = Arc::new(isolate::IsolateState::new(flags, rest_argv));
let snapshot = snapshot::deno_snapshot();
let isolate = isolate::Isolate::new(snapshot, state, ops::dispatch);
tokio_util::init(|| {
isolate
.execute("denoMain();")
.unwrap_or_else(print_err_and_exit);
isolate.event_loop().unwrap_or_else(print_err_and_exit);
});
}
isolate.event_loop
というのがメインループかな。
IsolateState とはなんだろうか。
// src/isolate.rs
pub struct IsolateState {
pub dir: deno_dir::DenoDir,
pub argv: Vec<String>,
pub permissions: DenoPermissions,
pub flags: flags::DenoFlags,
pub metrics: Metrics,
}
impl IsolateState {
pub fn new(flags: flags::DenoFlags, argv_rest: Vec<String>) -> Self {
let custom_root = env::var("DENO_DIR").map(|s| s.into()).ok();
Self {
dir: deno_dir::DenoDir::new(flags.reload, custom_root).unwrap(),
argv: argv_rest,
permissions: DenoPermissions::new(&flags),
flags,
metrics: Metrics::default(),
}
}
//...
パーミッションや起動フラグを渡している。
snapshot とは
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
use libdeno::deno_buf;
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()) }
}
deno_buf、 たぶんランタイム上のインメモリ状態だろうか。
// src/libdeno.rs
/// If "alloc_ptr" is not null, this type represents a buffer which is created
/// in C side, and then passed to Rust side by `deno_recv_cb`. Finally it should
/// be moved back to C side by `deno_respond`. If it is not passed to
/// `deno_respond` in the end, it will be leaked.
///
/// If "alloc_ptr" is null, this type represents a borrowed slice.
#[repr(C)]
pub struct deno_buf {
alloc_ptr: *const u8,
alloc_len: usize,
data_ptr: *const u8,
data_len: usize,
}
メモリ確保状態を持ってるっぽい。
で、この state と snapshot を使って、isolate インスタンスを生成する。
impl Isolate {
pub fn new(
snapshot: libdeno::deno_buf,
state: Arc<IsolateState>,
dispatch: Dispatch,
) -> Self {
DENO_INIT.call_once(|| {
unsafe { libdeno::deno_init() };
});
let config = libdeno::deno_config {
will_snapshot: 0,
load_snapshot: snapshot,
shared: libdeno::deno_buf::empty(), // TODO Use for message passing.
recv_cb: pre_dispatch,
resolve_cb,
};
let libdeno_isolate = unsafe { libdeno::deno_new(config) };
// This channel handles sending async messages back to the runtime.
let (tx, rx) = mpsc::channel::<(i32, Buf)>();
Self {
libdeno_isolate,
dispatch,
rx,
tx,
ntasks: Cell::new(0),
timeout_due: Cell::new(None),
state,
}
}
第三引数の dispatch
ってなんだ、と思ったらコメントに色々書いてある。
// src/ops.rs
/// Processes raw messages from JavaScript.
/// This functions invoked every time libdeno.send() is called.
/// control corresponds to the first argument of libdeno.send().
/// data corresponds to the second argument of libdeno.send().
pub fn dispatch(
isolate: &Isolate,
control: libdeno::deno_buf,
data: libdeno::deno_buf,
) -> (bool, Box<Op>) {
let base = msg::get_root_as_base(&control);
let is_sync = base.sync();
let inner_type = base.inner_type();
let cmd_id = base.cmd_id();
let op: Box<Op> = if inner_type == msg::Any::SetTimeout {
// SetTimeout is an exceptional op: the global timeout field is part of the
// Isolate state (not the IsolateState state) and it must be updated on the
// main thread.
assert_eq!(is_sync, true);
op_set_timeout(isolate, &base, data)
} else {
// Handle regular ops.
let op_creator: OpCreator = match inner_type {
msg::Any::Accept => op_accept,
msg::Any::Chdir => op_chdir,
msg::Any::Chmod => op_chmod,
msg::Any::Close => op_close,
msg::Any::CodeCache => op_code_cache,
js からの来る諸々を Rust で捌いてる部分っぽく見える。
で、 libdeno::deno_init()
が呼ばれて起動してるわけで…
// 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);
C バインディング。 cpp 側にも同じメソッド定義があるはずなので grep すると、
libdeno/deno.h
と libdeno/api.cc
が引っかかる。
deno_init(); が何をしているかというと。
// libdeno/api.cc
void deno_init() {
// v8::V8::InitializeICUDefaultLocation(argv[0]);
// v8::V8::InitializeExternalStartupData(argv[0]);
auto* p = v8::platform::CreateDefaultPlatform();
v8::V8::InitializePlatform(p);
v8::V8::Initialize();
}
なるほど、ここで V8 が起動するわけか。あとは IsolateState のオプションを渡したり 色々している。
次は src/main.rs のここを追ってみる。
tokio_util::init(|| {
isolate
.execute("denoMain();")
.unwrap_or_else(print_err_and_exit);
isolate.event_loop().unwrap_or_else(print_err_and_exit);
});
tokio、 正直ちゃんと理解してないんだけど…先に isolate.execute("denoMain();");
を追う。JS 実行してそう。
と、ここで今までの流れを要約したようなテストコードを発見した。
// src/isolate.rs
fn test_dispatch_sync() {
let argv = vec![String::from("./deno"), String::from("hello.js")];
let (flags, rest_argv, _) = flags::set_flags(argv).unwrap();
let state = Arc::new(IsolateState::new(flags, rest_argv));
let snapshot = libdeno::deno_buf::empty();
let isolate = Isolate::new(snapshot, state, dispatch_sync);
tokio_util::init(|| {
isolate
.execute(
r#"
const m = new Uint8Array([4, 5, 6]);
let n = libdeno.send(m);
if (!(n.byteLength === 3 &&
n[0] === 1 &&
n[1] === 2 &&
n[2] === 3)) {
throw Error("assert error");
}
"#,
).expect("execute error");
isolate.event_loop().ok();
});
}
で、これを踏まえた上で execute の実装を見る。
// src/isolate.rs
/// Same as execute2() but the filename defaults to "<anonymous>".
pub fn execute(&self, js_source: &str) -> Result<(), JSError> {
self.execute2("<anonymous>", js_source)
}
/// Executes the provided JavaScript source code. The js_filename argument is
/// provided only for debugging purposes.
pub fn execute2(
&self,
js_filename: &str,
js_source: &str,
) -> Result<(), JSError> {
let filename = CString::new(js_filename).unwrap();
let source = CString::new(js_source).unwrap();
let r = unsafe {
libdeno::deno_execute(
self.libdeno_isolate,
self.as_raw_ptr(),
filename.as_ptr(),
source.as_ptr(),
)
};
if r == 0 {
let js_error = self.last_exception().unwrap();
return Err(js_error);
}
Ok(())
}
CString って C にわたす FFI 呼ぶときによく見るやつだ。 実質
libdeno::deno_execute()
へのファサードになっている。
この C++ 側の実装を見ると…
// libdeno/api.cc
int deno_execute(Deno* d_, void* user_data, const char* js_filename,
const char* js_source) {
auto* d = unwrap(d_);
deno::UserDataScope user_data_scope(d, user_data);
auto* isolate = d->isolate_;
v8::Locker locker(isolate);
v8::Isolate::Scope isolate_scope(isolate);
v8::HandleScope handle_scope(isolate);
auto context = d->context_.Get(d->isolate_);
CHECK(!context.IsEmpty());
return deno::Execute(context, js_filename, js_source) ? 1 : 0;
}
v8::Locker がなんなのかよくわからないが、名前と使われ方みると、実行コンテキスト の排他制御とかそんな感じだろうか。
色々飛ばして、 return deno::Execute
が v8 の呼び出す実体を持ってそう。追ってみ
る。
// libdeno/binding.cc
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;
}
v8 の呼び出し自体は、単にこの 2 行っぽく見える。
auto script = v8::Script::Compile(context, source, &origin);
// ...
auto result = script.ToLocalChecked()->Run(context);
というわけで、JS の denoMain();
を呼んでるのはわかった。が、そもそもこれはどう
定義されたのか。たぶんどこかでプレロードされたんだろうが…
grep するとここっぽい。
// js/main.ts
export default function denoMain() {
libdeno.recv(handleAsyncMsgFromRust);
// First we send an empty "Start" message to let the privileged side know we
// are ready. The response should be a "StartRes" message containing the CLI
// args and other info.
const startResMsg = sendStart();
// ...
これがどう扱われているか。
BUILD.gn
に main.js
にビルドしてそうなタスクを見つけた。
run_node("bundle") {
out_dir = "$target_gen_dir/bundle/"
outputs = [
out_dir + "main.js",
out_dir + "main.js.map",
]
depfile = out_dir + "main.d"
deps = [
":deno_runtime_declaration",
":msg_ts",
]
args = [
rebase_path("third_party/node_modules/rollup/bin/rollup", root_build_dir),
"-c",
rebase_path("rollup.config.js", root_build_dir),
"-i",
rebase_path("js/main.ts", root_build_dir),
"-o",
rebase_path(out_dir + "main.js", root_build_dir),
"--sourcemapFile",
rebase_path("."),
"--silent",
]
}
なるほど、つまり rollup
で js にビルドしている。 追っていくと
target/debug/gen/bundle/main.js
に sourcemap とともに出力されていた。(本番ビルドだと release)
ここで、この main.js をどこかで評価してるはずだ、と思って追ったが、main.js で grep してもここぐらいしかみつからない
// src/js_errors.rs
fn parse_map_string(
script_name: &str,
getter: &SourceMapGetter,
) -> Option<SourceMap> {
match script_name {
// The bundle does not get built for 'cargo check', so we don't embed the
// bundle source map.
#[cfg(not(feature = "check-only"))]
"gen/bundle/main.js" => {
let s =
include_str!(concat!(env!("GN_OUT_DIR"), "/gen/bundle/main.js.map"));
SourceMap::from_json(s)
}
_ => match getter.get_source_map(script_name) {
None => None,
Some(raw_source_map) => SourceMap::from_json(&raw_source_map),
},
}
}
これはエラーが起きたときに そのエラーの出本がこの main.js かどうかを判定している だけで、読み込んでる箇所ではない。
追ったがわからなかったので飛ばす。なんか知らんが読み込まれてるんだろうという雑な 理解で逃げる。
まあなんだかんだで、ここまで来たので、 src/main.rs
で、イベントループを開始し
、 setTimeout や Promise のマイクロタスクキューが動き出すんでしょう。たぶん。
// src/main.rs
isolate.event_loop().unwrap_or_else(print_err_and_exit);
// src/isolate.rs
pub fn event_loop(&self) -> Result<(), JSError> {
// Main thread event loop.
while !self.is_idle() {
match recv_deadline(&self.rx, self.get_timeout_due()) {
Ok((req_id, buf)) => self.complete_op(req_id, buf),
Err(mpsc::RecvTimeoutError::Timeout) => self.timeout(),
Err(e) => panic!("recv_deadline() failed: {:?}", e),
}
self.check_promise_errors();
if let Some(err) = self.last_exception() {
return Err(err);
}
}
// Check on done
self.check_promise_errors();
if let Some(err) = self.last_exception() {
return Err(err);
}
Ok(())
}
アイドル状態でなければ、メインループ回して promise エラーとか収集してあったらエ
ラーを吐く。 recv_deadline
というのが Timeout などを扱っている?のかな。JS って 60 fps で制御されてるという理解だったんだけど、これどこかで sleep するんだろうか。
// src/isolate.rs
fn recv_deadline<T>(
rx: &mpsc::Receiver<T>,
maybe_due: Option<Instant>,
) -> Result<T, mpsc::RecvTimeoutError> {
match maybe_due {
None => rx.recv().map_err(|e| e.into()),
Some(due) => {
// Subtracting two Instants causes a panic if the resulting duration
// would become negative. Avoid this.
let now = Instant::now();
let timeout = if due > now {
due - now
} else {
Duration::new(0, 0)
};
// TODO: use recv_deadline() instead of recv_timeout() when this
// feature becomes stable/available.
rx.recv_timeout(timeout)
}
}
}
よくわからないので飛ばす
要はここまでやって v8 が起動していることがわかった。じゃあどういうスクリプトが起 動しているのか。
// js/main.ts
export default function denoMain() {
libdeno.recv(handleAsyncMsgFromRust);
// First we send an empty "Start" message to let the privileged side know we
// are ready. The response should be a "StartRes" message containing the CLI
// args and other info.
const startResMsg = sendStart();
setLogDebug(startResMsg.debugFlag());
// handle `--types`
if (startResMsg.typesFlag()) {
const defaultLibFileName = compiler.getDefaultLibFileName();
const defaultLibModule = compiler.resolveModule(defaultLibFileName, "");
console.log(defaultLibModule.sourceCode);
os.exit(0);
}
// handle `--version`
if (startResMsg.versionFlag()) {
console.log("deno:", startResMsg.denoVersion());
console.log("v8:", startResMsg.v8Version());
console.log("typescript:", version);
os.exit(0);
}
os.setPid(startResMsg.pid());
const cwd = startResMsg.cwd();
log("cwd", cwd);
for (let i = 1; i < startResMsg.argvLength(); i++) {
args.push(startResMsg.argv(i));
}
log("args", args);
Object.freeze(args);
const inputFn = args[0];
compiler.recompile = startResMsg.recompileFlag();
if (inputFn) {
compiler.run(inputFn, `${cwd}/`);
} else {
replLoop();
}
}
さっそく libdeno.recv(handleAsyncMsgFromRust)
というわかりやすいメソッドが出て
きた。
// src/libdeno.ts
interface Libdeno {
recv(cb: MessageCallback): void;
send(control: ArrayBufferView, data?: ArrayBufferView): null | Uint8Array;
print(x: string, isErr?: boolean): void;
shared: ArrayBuffer;
builtinModules: { [s: string]: object };
setGlobalErrorHandler: (
handler: (
message: string,
source: string,
line: number,
col: number,
error: Error
) => void
) => void;
setPromiseRejectHandler: (
handler: (
error: Error | string,
event: PromiseRejectEvent,
/* tslint:disable-next-line:no-any */
promise: Promise<any>
) => void
) => void;
setPromiseErrorExaminer: (handler: () => boolean) => void;
}
const window = globalEval("this");
export const libdeno = window.libdeno as Libdeno;
ここに libdeno.recv の実装はなく、キャストされている。トップレベルでの
globalEval("this")
はなんか不穏な気配がするが、要はどこかで this
というカス
タマイズされた global コンテキストに対し、 recv
というメソッドを定義しているや
つがいる、ような気がする。
たぶんこれは v8 バインディングではないか。つまり libdeno は rust 側から呼ぶ経路
と、JS から呼ぶ経路がありそう。js から呼んだものが、 ops::dispatch
などでハン
ドルされるんだろう。
src/main.ts に戻る
// First we send an empty "Start" message to let the privileged side know we
// are ready. The response should be a "StartRes" message containing the CLI
// args and other info.
const startResMsg = sendStart();
sendStart の実装はこれ
function sendStart(): msg.StartRes {
const builder = flatbuffers.createBuilder();
msg.Start.startStart(builder);
const startOffset = msg.Start.endStart(builder);
const baseRes = sendSync(builder, msg.Any.Start, startOffset);
assert(baseRes != null);
assert(msg.Any.StartRes === baseRes!.innerType());
const startRes = new msg.StartRes();
assert(baseRes!.inner(startRes) != null);
return startRes;
}
よくわからないが flatbuffers の RPC 使う準備してそう。
あと、気になるのは、 compiler というやつだろうか。何をしているんだろう
const compiler = DenoCompiler.instance();
// src/compiler.ts
/** A singleton class that combines the TypeScript Language Service host API
* with Deno specific APIs to provide an interface for compiling and running
* TypeScript and JavaScript modules.
*/
export class DenoCompiler
implements ts.LanguageServiceHost, ts.FormatDiagnosticsHost {
// Modules are usually referenced by their ModuleSpecifier and ContainingFile,
// and keeping a map of the resolved module file name allows more efficient
// future resolution
private readonly _fileNamesMap = new Map<
ContainingFile,
Map<ModuleSpecifier, ModuleFileName>
>();
// ...
compile(moduleMetaData: ModuleMetaData): OutputCode {
const recompile = !!this.recompile;
if (!recompile && moduleMetaData.outputCode) {
return moduleMetaData.outputCode;
}
const { fileName, sourceCode, mediaType, moduleId } = moduleMetaData;
console.warn("Compiling", moduleId);
moduleMetaData
というのを食ってコードを生成している。
/** A simple object structure for caching resolved modules and their contents.
*
* Named `ModuleMetaData` to clarify it is just a representation of meta data of
* the module, not the actual module instance.
*/
export class ModuleMetaData implements ts.IScriptSnapshot {
public deps?: ModuleFileName[];
public exports = {};
public factory?: AmdFactory;
public gatheringDeps = false;
public hasRun = false;
public scriptVersion = "";
typescript の内部オブジェクトを継承してるっぽい。なんか色々やってコンパイルして キャッシュを作ってる。略。
疲れてきたので、ここで終了。概略はわかった気がする。
色々勉強になりましたー 👏👏
以下、思ったことコメントします。
で、あってると思います。これがあるおかげで ts を丸ごと含んでいる main.js の (v8 的な) compile とか JIT 最適化がある程度終わった状態で起動できるので速いという話だと思っています。
( snapshot で startup time を早くする話が出てくる issue: denoland/deno#45
chrome で下のようなコードを実行すると 60 以上の数字が出てくるので、60fps という縛りは持っていないような気がしますが、どうでしょう?
ですね、libdeno は v8 バインディングであってると思います。ソースは /libdeno 以下に隔離されています
ですね、DenoCompiler = 「typescript の API をラップして、deno 的な ts コンパイルするやつ」 だと思います。この辺は @ry よりも @kitsonk という人が中心になって開発してるぽいです。
イベントループについての余談ですが、以前 event_loop を JS (と libdeno) 側に移動する(したい)という PR がありましたが、全てのベンチマークがよくなるわけではないらしく、一旦ペンディングになったみたいです。
denoland/deno#1003