Open5

Denoを読む

nasanasa

ひとまずCargo.tomlを見て全体像を推測しておく。

Cargo.toml
[workspace]
resolver = "2"
members = [
  "bench_util",
  "cli",
  "cli/napi/sym",
  "core",
  "ops",
  "runtime",
  "serde_v8",
  "test_ffi",
  "test_napi",
  "test_util",
  "ext/broadcast_channel",
  "ext/cache",
  "ext/console",
  "ext/crypto",
  "ext/fetch",
  "ext/ffi",
  "ext/fs",
  "ext/http",
  "ext/io",
  "ext/kv",
  "ext/net",
  "ext/node",
  "ext/url",
  "ext/web",
  "ext/webidl",
  "ext/websocket",
  "ext/webstorage",
  "ext/napi",
]
...

ext/*が山ほどあるがこれは標準ライブラリに見える。

下記4つあたりをコードリーディング対象とするのが良さそう。

  • cli
  • core
  • ops
  • runtime
nasanasa

cli/main.rs

READMEにあるhello worldを実行した際に何が起こるかトップレベルから見ていくことにする。

$ deno run https://deno.land/std/examples/welcome.ts
Welcome to Deno!

denoコマンドのエントリーポイントを見ていく。

pub fn main() {
  let args: Vec<String> = env::args().collect();

  let future = async move {
    //...

    run_subcommand(flags).await
  };

  let exit_code = unwrap_or_exit(create_and_run_current_thread(future));

  std::process::exit(exit_code);
}

コマンドライン引数のパース、サブコマンドの定義はclapに則って行われている。
mainは基本的にrun_subcommandを呼ぶだけ。
run_subcommandでどのサブコマンドが呼ばれたか判断してサブコマンドのエントリーポイントを呼び出している。

今回読みたいのはrunなのでDenoSubcommand::Runを見ていく

cli/main.rs
    DenoSubcommand::Run(run_flags) => spawn_subcommand(async move {
      if run_flags.is_stdin() {
        tools::run::run_from_stdin(flags).await
      } else {
        tools::run::run_script(flags).await
      }
    }),
nasanasa

cli/tools/run.rs

前半ではflagのバリデーション、必要に応じてnpm installが行われていた。
プログラムの実行に関係ありそうなのは以下のコードかな。
workerを作り、main_moduleを指定されたpermissionで実行している。

cli/tools/run.rs
pub async fn run_script(flags: Flags) -> Result<i32, AnyError> {
  // ...

  let worker_factory = factory.create_cli_main_worker_factory().await?;
  let mut worker = worker_factory
    .create_main_worker(main_module, permissions)
    .await?;

  let exit_code = worker.run().await?;
  Ok(exit_code)
}

では、workerとは?というと、以下の構造体だった。
main_moduleはこれから実行するjsコードだろう、CliMainWorkerは何かしら状態を持つようで、しかも他と共有されるみたいだ。

cli/worker.rs
pub struct CliMainWorker {
  main_module: ModuleSpecifier,
  is_main_cjs: bool,
  worker: MainWorker,
  shared: Arc<SharedWorkerState>,
}

sharedは今は深ぼらずメインディッシュと思わしきworkerを見ていく

ちなみにここからcliクレートの外に出てruntimeクレートに入る

runtime/worker.rs
/// This worker is created and used by almost all
/// subcommands in Deno executable.
///
/// It provides ops available in the `Deno` namespace.
///
/// All `WebWorker`s created during program execution
/// are descendants of this worker.
pub struct MainWorker {
  pub js_runtime: JsRuntime,
  should_break_on_first_statement: bool,
  should_wait_for_inspector_session: bool,
  exit_code: ExitCode,
  bootstrap_fn_global: Option<v8::Global<v8::Function>>,
}

It provides ops available in the Deno namespace.

opsというのはRustで記述されたランタイムの拡張機能のことを指している。
例えば、ファイル操作やネットワークアクセスはopsとして実装されランタイムに組み込まれている。
詳細はまだ分かってないのだが、RustとJSでデータの受け渡し発生時に必要となるグルーコードがどこかにあるのだろう。

runtimeからcliに戻ってworkerの初期化を見てみる。
結構な量のオプションが有るみたい。

cli/worker.rs
let options = WorkerOptions {
  bootstrap: BootstrapOptions {
    args: shared.options.argv.clone(),
    cpu_count: std::thread::available_parallelism()
      .map(|p| p.get())
      .unwrap_or(1),
    log_level: shared.options.log_level,
    enable_testing_features: shared.options.enable_testing_features,
    locale: deno_core::v8::icu::get_language_tag(),
    location: shared.options.location.clone(),
    no_color: !colors::use_color(),
    is_tty: colors::is_tty(),
    runtime_version: version::deno().to_string(),
    ts_version: version::TYPESCRIPT.to_string(),
    unstable: shared.options.unstable,
    user_agent: version::get_user_agent().to_string(),
    inspect: shared.options.is_inspecting,
  },
  extensions,
  startup_snapshot: Some(crate::js::deno_isolate_init()),
  create_params: None,
  unsafely_ignore_certificate_errors: shared
    .options
    .unsafely_ignore_certificate_errors
    .clone(),
  root_cert_store_provider: Some(shared.root_cert_store_provider.clone()),
  seed: shared.options.seed,
  source_map_getter: maybe_source_map_getter,
  format_js_error_fn: Some(Arc::new(format_js_error)),
  create_web_worker_cb,
  web_worker_preload_module_cb,
  web_worker_pre_execute_module_cb,
  maybe_inspector_server,
  should_break_on_first_statement: shared.options.inspect_brk,
  should_wait_for_inspector_session: shared.options.inspect_wait,
  module_loader,
  fs: shared.fs.clone(),
  npm_resolver: Some(shared.npm_resolver.clone()),
  get_error_class_fn: Some(&errors::get_error_class_name),
  cache_storage_dir,
  origin_storage_dir,
  blob_store: shared.blob_store.clone(),
  broadcast_channel: shared.broadcast_channel.clone(),
  shared_array_buffer_store: Some(shared.shared_array_buffer_store.clone()),
  compiled_wasm_module_store: Some(
    shared.compiled_wasm_module_store.clone(),
  ),
  stdio,
};

let worker = MainWorker::bootstrap_from_options(
  main_module.clone(),
  permissions,
  options,
);

Ok(CliMainWorker {
  main_module,
  is_main_cjs,
  worker,
  shared: shared.clone(),
})

個人的にはmodule_loaderやstartup_snapshot, extensionsが気になっているが、深掘りせずにrunコマンドから呼ばれているCliMainWorker#runを見ていく

cli/worker.rs
  pub async fn run(&mut self) -> Result<i32, AnyError> {
    let mut maybe_coverage_collector =
      self.maybe_setup_coverage_collector().await?;
    log::debug!("main_module {}", self.main_module);

    if self.is_main_cjs {
      self.initialize_main_module_for_node()?;
      deno_node::load_cjs_module(
        &mut self.worker.js_runtime,
        &self.main_module.to_file_path().unwrap().to_string_lossy(),
        true,
        self.shared.options.inspect_brk,
      )?;
    } else {
      self.execute_main_module_possibly_with_npm().await?;
    }

    self.worker.dispatch_load_event(located_script_name!())?;

    loop {
      self
        .worker
        .run_event_loop(maybe_coverage_collector.is_none())
        .await?;
      if !self
        .worker
        .dispatch_beforeunload_event(located_script_name!())?
      {
        break;
      }
    }

    self.worker.dispatch_unload_event(located_script_name!())?;

    if let Some(coverage_collector) = maybe_coverage_collector.as_mut() {
      self
        .worker
        .with_event_loop(coverage_collector.stop_collecting().boxed_local())
        .await?;
    }

    Ok(self.worker.exit_code())
  }

jsの実行部分は下記になります。(その後のイベント送信は何してるのかよく分からなかった)

cjsだとかES Moduleだとかはよく分からないが、ログを仕込んでどちらの経路を取っているか確認したところcjsではなかった。
execute_main_module_possibly_with_npmが大通りだと思われるのでこちらも見ていく

if self.is_main_cjs {
  self.initialize_main_module_for_node()?;
  deno_node::load_cjs_module(
    &mut self.worker.js_runtime,
    &self.main_module.to_file_path().unwrap().to_string_lossy(),
    true,
    self.shared.options.inspect_brk,
  )?;
} else {
  self.execute_main_module_possibly_with_npm().await?;
}
nasanasa

ここでメインモジュールの評価が行われる。

引数のModuleSpecifierはUrlが渡ってくる。deno runでローカルのファイルを指定した場合はfile:///Users/lab/sandbox/deno_test/main.tsとなるし、https://deno.land/std/examples/welcome.tsを実行する場合はこれがそのまま引数として渡ってくる。

evaluate_moduleではモジュール情報を直接受け取るんじゃなくて、module idをやり取りする。まだ中身は読んでいないが、runtimeに含まれるmodule loaderやmodule mapから引いてくるんだろう。
tokio::selectは実行完了を待っているだけに見えるので略。

runtime/worker.rs
  /// Loads, instantiates and executes specified JavaScript module.
  ///
  /// This module will have "import.meta.main" equal to true.
  pub async fn execute_main_module(
    &mut self,
    module_specifier: &ModuleSpecifier,
  ) -> Result<(), AnyError> {
    let id = self.preload_main_module(module_specifier).await?;
    self.evaluate_module(id).await
  }

  // ...

  /// Executes specified JavaScript module.
  pub async fn evaluate_module(
    &mut self,
    id: ModuleId,
  ) -> Result<(), AnyError> {
    self.wait_for_inspector_session();
    let mut receiver = self.js_runtime.mod_evaluate(id);
    tokio::select! {
      // Not using biased mode leads to non-determinism for relatively simple
      // programs.
      biased;

      maybe_result = &mut receiver => {
        debug!("received module evaluate {:#?}", maybe_result);
        maybe_result.expect("Module evaluation result not provided.")
      }

      event_loop_result = self.run_event_loop(false) => {
        event_loop_result?;
        let maybe_result = receiver.await;
        maybe_result.expect("Module evaluation result not provided.")
      }
    }
  }

ちらほらv8が出てきた。大詰めっぽい。

core/runtime.rs
  // TODO(bartlomieju): make it return `ModuleEvaluationFuture`?
  /// Evaluates an already instantiated ES module.
  ///
  /// Returns a receiver handle that resolves when module promise resolves.
  /// Implementors must manually call [`JsRuntime::run_event_loop`] to drive
  /// module evaluation future.
  ///
  /// `Error` can usually be downcast to `JsError` and should be awaited and
  /// checked after [`JsRuntime::run_event_loop`] completion.
  ///
  /// This function panics if module has not been instantiated.
  pub fn mod_evaluate(
    &mut self,
    id: ModuleId,
  ) -> oneshot::Receiver<Result<(), Error>> {
    let global_realm = self.global_realm();
    let state_rc = self.inner.state.clone();
    let module_map_rc = self.module_map.clone();
    let scope = &mut self.handle_scope();
    let tc_scope = &mut v8::TryCatch::new(scope);

    let module = module_map_rc
      .borrow()
      .get_handle(id)
      .map(|handle| v8::Local::new(tc_scope, handle))
      .expect("ModuleInfo not found");
    let mut status = module.get_status();
    assert_eq!(
      status,
      v8::ModuleStatus::Instantiated,
      "Module not instantiated {id}"
    );

    let (sender, receiver) = oneshot::channel();

    // IMPORTANT: Top-level-await is enabled, which means that return value
    // of module evaluation is a promise.
    //
    // Because that promise is created internally by V8, when error occurs during
    // module evaluation the promise is rejected, and since the promise has no rejection
    // handler it will result in call to `bindings::promise_reject_callback` adding
    // the promise to pending promise rejection table - meaning JsRuntime will return
    // error on next poll().
    //
    // This situation is not desirable as we want to manually return error at the
    // end of this function to handle it further. It means we need to manually
    // remove this promise from pending promise rejection table.
    //
    // For more details see:
    // https://github.com/denoland/deno/issues/4908
    // https://v8.dev/features/top-level-await#module-execution-order
    {
      let mut state = state_rc.borrow_mut();
      assert!(
        state.pending_mod_evaluate.is_none(),
        "There is already pending top level module evaluation"
      );
      state.pending_mod_evaluate = Some(ModEvaluate {
        promise: None,
        has_evaluated: false,
        handled_promise_rejections: vec![],
        sender,
      });
    }

    let maybe_value = module.evaluate(tc_scope);
    {
      let mut state = state_rc.borrow_mut();
      let pending_mod_evaluate = state.pending_mod_evaluate.as_mut().unwrap();
      pending_mod_evaluate.has_evaluated = true;
    }

    // Update status after evaluating.
    status = module.get_status();

    let has_dispatched_exception =
      state_rc.borrow_mut().dispatched_exception.is_some();
    if has_dispatched_exception {
      // This will be overrided in `exception_to_err_result()`.
      let exception = v8::undefined(tc_scope).into();
      let pending_mod_evaluate = {
        let mut state = state_rc.borrow_mut();
        state.pending_mod_evaluate.take().unwrap()
      };
      pending_mod_evaluate
        .sender
        .send(exception_to_err_result(tc_scope, exception, false))
        .expect("Failed to send module evaluation error.");
    } else if let Some(value) = maybe_value {
      assert!(
        status == v8::ModuleStatus::Evaluated
          || status == v8::ModuleStatus::Errored
      );
      let promise = v8::Local::<v8::Promise>::try_from(value)
        .expect("Expected to get promise as module evaluation result");
      let promise_global = v8::Global::new(tc_scope, promise);
      let mut state = state_rc.borrow_mut();
      {
        let pending_mod_evaluate = state.pending_mod_evaluate.as_ref().unwrap();
        let pending_rejection_was_already_handled = pending_mod_evaluate
          .handled_promise_rejections
          .contains(&promise_global);
        if !pending_rejection_was_already_handled {
          global_realm
            .0
            .state()
            .borrow_mut()
            .pending_promise_rejections
            .retain(|(key, _)| key != &promise_global);
        }
      }
      let promise_global = v8::Global::new(tc_scope, promise);
      state.pending_mod_evaluate.as_mut().unwrap().promise =
        Some(promise_global);
      tc_scope.perform_microtask_checkpoint();
    } else if tc_scope.has_terminated() || tc_scope.is_execution_terminating() {
      let pending_mod_evaluate = {
        let mut state = state_rc.borrow_mut();
        state.pending_mod_evaluate.take().unwrap()
      };
      pending_mod_evaluate.sender.send(Err(
        generic_error("Cannot evaluate module, because JavaScript execution has been terminated.")
      )).expect("Failed to send module evaluation error.");
    } else {
      assert!(status == v8::ModuleStatus::Errored);
    }

    receiver
  }