Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created January 8, 2019 19:33
Show Gist options
  • Save mizchi/31e5628751330b624a0e8ada9e739b1e to your computer and use it in GitHub Desktop.
Save mizchi/31e5628751330b624a0e8ada9e739b1e to your computer and use it in GitHub Desktop.

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.hlibdeno/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.gnmain.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)
    }
  }
}

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

denoMain()

要はここまでやって 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 の内部オブジェクトを継承してるっぽい。なんか色々やってコンパイルして キャッシュを作ってる。略。

疲れてきたので、ここで終了。概略はわかった気がする。

@kt3k
Copy link

kt3k commented Jan 9, 2019

色々勉強になりましたー 👏👏

以下、思ったことコメントします。

snapshot とは deno_buf、 たぶんランタイム上のインメモリ状態だろうか。

で、あってると思います。これがあるおかげで ts を丸ごと含んでいる main.js の (v8 的な) compile とか JIT 最適化がある程度終わった状態で起動できるので速いという話だと思っています。
( snapshot で startup time を早くする話が出てくる issue: denoland/deno#45

JS って 60 fps で制御されてるという理解だったんだけど

chrome で下のようなコードを実行すると 60 以上の数字が出てくるので、60fps という縛りは持っていないような気がしますが、どうでしょう?

i = 0; setInterval(() => { console.log(i); i = 0; }, 1000); a = () => {i += 1; setTimeout(a);}; a();

たぶんこれは v8 バインディングではないか。つまり libdeno は rust 側から呼ぶ経路 と、JS から呼ぶ経路がありそう。

ですね、libdeno は v8 バインディングであってると思います。ソースは /libdeno 以下に隔離されています

typescript の内部オブジェクトを継承してるっぽい。なんか色々やってコンパイルして キャッシュを作ってる。略。

ですね、DenoCompiler = 「typescript の API をラップして、deno 的な ts コンパイルするやつ」 だと思います。この辺は @ry よりも @kitsonk という人が中心になって開発してるぽいです。

イベントループについての余談ですが、以前 event_loop を JS (と libdeno) 側に移動する(したい)という PR がありましたが、全てのベンチマークがよくなるわけではないらしく、一旦ペンディングになったみたいです。
denoland/deno#1003

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment