Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created January 8, 2019 19:33
Show Gist options
  • Select an option

  • Save mizchi/31e5628751330b624a0e8ada9e739b1e to your computer and use it in GitHub Desktop.

Select an option

Save mizchi/31e5628751330b624a0e8ada9e739b1e to your computer and use it in GitHub Desktop.

Revisions

  1. mizchi created this gist Jan 8, 2019.
    687 changes: 687 additions & 0 deletions deno_code_reading.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,687 @@
    deno のコードを読んだメモ。

    そこまで大きなプロジェクトでもないので、rust と cpp そこまで習熟してなくても読めるだろうという気持ち。

    ## ブートプロセス

    https://denolib.gitbook.io/guide/installing-deno

    起動プロセスっぽいところ。

    ```rs
    // 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 とはなんだろうか。

    ```rs
    // 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 とは

    ```rs
    // 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、 たぶんランタイム上のインメモリ状態だろうか。

    ```rs
    // 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 インスタンスを生成する。

    ```rs
    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` ってなんだ、と思ったらコメントに色々書いてある。

    ```rs
    // 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()` が呼ばれて起動してるわけで…

    ```rs
    // 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(); が何をしているかというと。

    ```cpp
    // 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 のここを追ってみる。

    ```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 実行してそう。

    と、ここで今までの流れを要約したようなテストコードを発見した。

    ```rs
    // 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 の実装を見る。

    ```rs
    // 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++ 側の実装を見ると…

    ```cpp
    // 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 の呼び出す実体を持ってそう。追ってみ
    る。
    ```cpp
    // 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 行っぽく見える。

    ```cpp
    auto script = v8::Script::Compile(context, source, &origin);
    // ...
    auto result = script.ToLocalChecked()->Run(context);
    ```
    というわけで、JS の `denoMain();` を呼んでるのはわかった。が、そもそもこれはどう
    定義されたのか。たぶんどこかでプレロードされたんだろうが…
    grep するとここっぽい。
    ```ts
    // 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 してもここぐらいしかみつからない

    ```rs
    // 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 のマイクロタスクキューが動き出すんでしょう。たぶん。

    ```rs
    // src/main.rs
    isolate.event_loop().unwrap_or_else(print_err_and_exit);
    ```

    ```rs
    // 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 するんだろうか。

    ```rs
    // 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)
    }
    }
    }
    ```

    よくわからないので飛ばす

    ## denoMain()

    要はここまでやって v8 が起動していることがわかった。じゃあどういうスクリプトが起
    動しているのか。

    ```ts
    // 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)` というわかりやすいメソッドが出て
    きた。

    ```ts
    // 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 に戻る

    ```js
    // 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 の実装はこれ

    ```ts
    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 というやつだろうか。何をしているんだろう

    ```ts
    const compiler = DenoCompiler.instance();
    ```

    ```ts
    // 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` というのを食ってコードを生成している。
    ```ts
    /** 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 の内部オブジェクトを継承してるっぽい。なんか色々やってコンパイルして
    キャッシュを作ってる。略。
    疲れてきたので、ここで終了。概略はわかった気がする。