Deno源码简析(二)启动流程

开始

这里直接开始从Deno启动开始分析:TS代码是如何加载和跑起来的,每一步都做了些什么;希望能够给到大家一点启发。

Deno启动

直接来到cli/main.rs的main方法:

pub fn main() {
  ...

  log::set_logger(&LOGGER).unwrap(); //设置日志等级
  let args: Vec<String> = env::args().collect();
  let flags = flags::flags_from_vec(args);
  
  //设置v8参数
  if let Some(ref v8_flags) = flags.v8_flags {
    let mut v8_flags_ = v8_flags.clone();
    v8_flags_.insert(0, "UNUSED_BUT_NECESSARY_ARG0".to_string());
    v8_set_flags(v8_flags_);
  }

  let log_level = match flags.log_level {
    Some(level) => level,
    None => Level::Info, // Default log level
  };
  log::set_max_level(log_level.to_level_filter());
   
  //根据命令行参数生成future
  let fut = match flags.clone().subcommand {
    DenoSubcommand::Bundle ...,
    DenoSubcommand::Doc ...,
    DenoSubcommand::Eval ...,
    DenoSubcommand::Cache ...
    DenoSubcommand::Fmt ...
    DenoSubcommand::Info ...,
    DenoSubcommand::Install ...
    DenoSubcommand::Repl ...,
    DenoSubcommand::Run { script } => run_command(flags, script).boxed_local(),
    DenoSubcommand::Test ...,
    DenoSubcommand::Completions ...
    DenoSubcommand::Types ...
    DenoSubcommand::Upgrade ...;
  //然后把future作为第一个任务传入Tokio调度器执行
  let result = tokio_util::run_basic(fut);
   ...
}

一开始的流程主要有两步:

  • 解析命令行参数然后创建对应的future(任务)
  • 启动Tokio执行future(任务)

这里就不得说一说rust的异步的概念,rust的future也可以大致理解为js的promise;它们都是存在类似的状态和状态转移:
截屏2020-06-07 下午3.07.37.png
不过promise靠的是push,也就可以认为它本身就是一个事件源,能够主动去改变自身状态;而future靠的是poll,需要一个excutor去轮询获取其状态,也就是说excutor会在适当的时候执行future的poll方法,当返回状态为Poll::Pending也就意味需要excutor等待进行下一次轮询,当返回Poll::Ready也就是意味future已经完成,然后根据返回的值去判断是否出错还是顺利完成执行下一步代码逻辑。
当然了,excutor如果不间断去轮询只会白白浪费性能,所以future的poll方法里面是可以设置一个waker,去通知excutor什么时候该来轮询的。
所以future的poll方法就是核心所在,Deno的核心逻辑也是在这个方法里面完成的。

接着我们直接跳到run_command方法:

async fn run_command(flags: Flags, script: String) -> Result<(), ErrBox> {
  let global_state = GlobalState::new(flags.clone())?; //第一步
  let main_module = ModuleSpecifier::resolve_url_or_path(&script).unwrap();
  let mut worker =
    create_main_worker(global_state.clone(), main_module.clone())?; //第二步
  debug!("main_module {}", main_module);
  worker.execute_module(&main_module).await?;
  worker.execute("window.dispatchEvent(new Event('load'))")?;
  (&mut *worker).await?;
  worker.execute("window.dispatchEvent(new Event('unload'))")?;
  if global_state.flags.lock_write {
    if let Some(ref lockfile) = global_state.lockfile {
      let g = lockfile.lock().unwrap();
      g.write()?;
    } else {
      eprintln!("--lock flag must be specified when using --lock-write");
      std::process::exit(11);
    }
  }
  Ok(())
}

感觉这里每一步都很关键,所以得需要一步一步来分析了

第一步:创建GlobalState
先看看GlobalState的结构是怎么样的:

/// This structure represents state of single "deno" program.
///
/// It is shared by all created workers (thus V8 isolates).
pub struct GlobalStateInner {
  /// Flags parsed from `argv` contents.
  pub flags: flags::Flags,
  /// Permissions parsed from `flags`.
  pub permissions: Permissions,
  pub dir: deno_dir::DenoDir,
  pub file_fetcher: SourceFileFetcher,
  pub ts_compiler: TsCompiler,
  pub lockfile: Option<Mutex<Lockfile>>,
  pub compiler_starts: AtomicUsize,
  compile_lock: AsyncMutex<()>,
}

根据注释GlobalStateInner就是代表整个deno程序的状态,并且所有的worker都共享这个状态。
而它关键的属性就是flags(命令行参数),permissions(权限),file_fetcher(负责加载本地或者远程文件),ts_compiler(ts编译器实例)
而它也只有一个方法:fetch_compiled_module,负责加载编译模块。
所以GlobalState其实主要功能职责可以总结为:共享参数和权限还有去加载编译模块代码。

第二步:创建main_worker
目前deno存在三种worker: MainWorker,CompilerWorker和WebWorker,而它们最终都依赖于Worker(另外一个独立的类),它们的关系如下:
截屏2020-06-09 上午11.48.49.png

Worker会建立一个独立的Isolate,为js提供全新的执行环境,MainWorker和WebWorker都是在Worker的基础上进行封装,提供自身的独有的接口。

所以继续回到创建main_worker的逻辑上:

fn create_main_worker(
  global_state: GlobalState,
  main_module: ModuleSpecifier,
) -> Result<MainWorker, ErrBox> {
  let state = State::new(global_state, None, main_module, false)?;

  let mut worker = MainWorker::new(
    "main".to_string(),
    startup_data::deno_isolate_init(),
    state,
  );

  {
    let (stdin, stdout, stderr) = get_stdio();
    let mut t = worker.resource_table.borrow_mut();
    t.add("stdin", Box::new(stdin));
    t.add("stdout", Box::new(stdout));
    t.add("stderr", Box::new(stderr));
  }

  worker.execute("bootstrap.mainRuntime()")?;
  Ok(worker)
}

创建main_worker的第一步就是要创建一个State对象,其实deno里面每个worker都有一个State对象相对应的,而刚说的deno程序本身也会有一个GlobalState相对应,最终各个woker会共享同一个GlobalState,还是画个图比较直观:
截屏2020-06-09 下午3.11.58.png

woker的State结构如下:

pub struct StateInner {
  pub global_state: GlobalState,
  pub permissions: Permissions,
  pub main_module: ModuleSpecifier,
  /// When flags contains a `.import_map_path` option, the content of the
  /// import map file will be resolved and set.
  pub import_map: Option<ImportMap>,
  pub metrics: Metrics,
  pub global_timer: GlobalTimer,
  pub workers: HashMap<u32, (JoinHandle<()>, WebWorkerHandle)>,
  pub next_worker_id: u32,
  pub start_time: Instant,
  pub seeded_rng: Option<StdRng>,
  pub target_lib: TargetLib,
  pub is_main: bool,
  pub is_internal: bool,
  pub inspector: Option<Box<DenoInspector>>,
}

可以看到State主要就是记录woker运行时的一些状态(有哪些权限,启动时间,审计等等),另外这个State还实现了ModuleLoader这个trait,所以State也需要承担加载模块的工作。

创建完State对象,现在正式要创建主worker了,但是创建前首先要调起StartupData::deno_isolate_init方法准备Isolate初始化要用到的StartupData,根据参数可以选择snapshot初始化或者直接代码初始化(代码位置:${deno构建输出目录}/gen/cli/bundle/main.js),继续下一步就是MainWorker的new方法:

pub fn new(name: String, startup_data: StartupData, state: State) -> Self {
    let state_ = state.clone();
    let mut worker = Worker::new(name, startup_data, state_);
    {
      let isolate = &mut worker.isolate;
      ops::runtime::init(isolate, &state);
      ops::runtime_compiler::init(isolate, &state);
      ops::errors::init(isolate, &state);
      ops::fetch::init(isolate, &state);
      ops::fs::init(isolate, &state);
      ops::fs_events::init(isolate, &state);
      ops::io::init(isolate, &state);
      ops::plugin::init(isolate, &state);
      ops::net::init(isolate, &state);
      ops::tls::init(isolate, &state);
      ops::os::init(isolate, &state);
      ops::permissions::init(isolate, &state);
      ops::process::init(isolate, &state);
      ops::random::init(isolate, &state);
      ops::repl::init(isolate, &state);
      ops::resources::init(isolate, &state);
      ops::signal::init(isolate, &state);
      ops::timers::init(isolate, &state);
      ops::tty::init(isolate, &state);
      ops::worker_host::init(isolate, &state);
    }
    Self(worker)
  }

MainWorker的创建也是比较简单:
第一步: 创建实际的Worker
第二步: 就是把各种op(系统调用)注册到CoreIsolate上。

继续深入Worker的创建:

pub fn new(name: String, startup_data: StartupData, state: State) -> Self {
    let loader = Rc::new(state.clone());
    let mut isolate = deno_core::EsIsolate::new(loader, startup_data, false);

    state.maybe_init_inspector(&mut isolate);

    let global_state = state.borrow().global_state.clone();
    isolate.set_js_error_create_fn(move |core_js_error| {
      JSError::create(core_js_error, &global_state.ts_compiler)
    });

    let (internal_channels, external_channels) = create_channels();

    Self {
      name,
      isolate,
      state,
      waker: AtomicWaker::new(),
      internal_channels,
      external_channels,
    }
  }

其实Worker的创建也比较简单,因为它的属性本身就比较少,而其中重点是isolate属性,Worker就是要依赖isolate对外提供执行代码模块的能力;而internal_channels和external_channel这两个属性都是基于rust的channel,目的就是为了提供woker在外部对其postMessage和内部自身postMessage的能力。

接着再看ESIsolate,这个时候我们已经从cli目录进入到core目录,意味着我们已经进入deno的心脏。
首先提前说明一下,ESIsolate是在CoreIsolate基础上做了一层封装,主要负责ES Module的导入-创建-加载-实例化-执行,所以名称开头其实就暴露它是跟ES Module相关的。
ESIsolate的new方法如下:

pub fn new(
    loader: Rc<dyn ModuleLoader>,
    startup_data: StartupData,
    will_snapshot: bool,
  ) -> Box<Self> {
    //第一步
    let mut core_isolate = CoreIsolate::new(startup_data, will_snapshot); 
    {
      //第二步
      let v8_isolate = core_isolate.v8_isolate.as_mut().unwrap();
      v8_isolate.set_host_initialize_import_meta_object_callback(
        bindings::host_initialize_import_meta_object_callback,
      );
      v8_isolate.set_host_import_module_dynamically_callback(
        bindings::host_import_module_dynamically_callback,
      );
    }

    let es_isolate = Self {
      modules: Modules::new(),
      loader,
      core_isolate,
      dyn_import_map: HashMap::new(),
      preparing_dyn_imports: FuturesUnordered::new(),
      pending_dyn_imports: FuturesUnordered::new(),
      waker: AtomicWaker::new(),
    };

    let mut boxed_es_isolate = Box::new(es_isolate);
    {
      //第三步
      let es_isolate_ptr: *mut Self = Box::into_raw(boxed_es_isolate);
      boxed_es_isolate = unsafe { Box::from_raw(es_isolate_ptr) };
      unsafe {
        let v8_isolate = boxed_es_isolate.v8_isolate.as_mut().unwrap();
        v8_isolate.set_data(1, es_isolate_ptr as *mut c_void);
      };
    }
    boxed_es_isolate
  }

代码也不算特别多,主要得益于deno设计整体职责分明,各司其职。
第一步:创建CoreIsolate
第二步:在v8 isolate上注册导入模块的回调
第三步:建立v8 isolate与rust之间的联系

最后来到CoreIsolate,穿过层层封装,终于见到这个大Boss了(感叹一下),简单分析一下创建步骤(因为CoreIsolate的new方法实在太长就不贴了):
第一步:初始化v8引擎
第二步:根据startup_data来判断是用script还是snapshot来初始化v8的Isolate(如果使用script,就会在第一次执行代码或者第一次事件循环之前才去执行初始化的script)
第三步:如果是使用script初始化就调用bindings::initialize_context 来初始化上下文(主要定义send,recv,queueMicrotask等这些核心的方法属性)
第四步:构建CoreIsolate实例

到此从MainWorker到CoreIsolate结束。

那么最后的重点:MainWorker,Worker,ESIsolate和CoreIsolate都是实现了Future Trait的(回想一下开头的就说的Future),当Tokio开始调度任务的时候:
截屏2020-06-17 下午8.22.07.png

上图可以理解为是从另外的一个角度去看待deno的事件循环,后面再深入去拆解deno的事件循环每一步做了些啥。

继续回到创建MainWorker的流程:

fn create_main_worker(
  global_state: GlobalState,
  main_module: ModuleSpecifier,
) -> Result<MainWorker, ErrBox> {
  let state = State::new(global_state, None, main_module, false)?;

  let mut worker = MainWorker::new(
    "main".to_string(),
    startup_data::deno_isolate_init(),
    state,
  );

  {
    let (stdin, stdout, stderr) = get_stdio();
    let mut t = worker.resource_table.borrow_mut();
    t.add("stdin", Box::new(stdin));
    t.add("stdout", Box::new(stdout));
    t.add("stderr", Box::new(stderr));
  }

  worker.execute("bootstrap.mainRuntime()")?;
  Ok(worker)
}

现在MainWorker已经构建好了,然后把标准的输入输出添加到worker的resource表上,接着就执行bootstrap.mainRuntime(),但是执行代码之前,还是需要进行一些初始化的工作,直接来到CoreIsolate::shared_init方法:

pub(crate) fn shared_init(&mut self) {
    if self.needs_init {
      self.needs_init = false;
      js_check(self.execute("core.js", include_str!("core.js")));
      // Maybe execute the startup script.
      if let Some(s) = self.startup_script.take() {
        self.execute(&s.filename, &s.source).unwrap()
      }
    }
  }

可以看到deno是先会执行core.js,然后再尝试执行之前所说的初始化script,那么core.js是什么来的尼?现在先不展开,因为这个是跟js与rust交互相关的,现在先留个坑下一篇文章再分析,我们只需要知道在执行任何脚本之前deno就会初始化好js与rust的交互环境就行了。

再回到执行bootstrap.mainRuntime(),现在我们终于可以回到熟悉的js世界了,mainRuntime方法主要工作就是是定义了全局的一些属性方法(包括兼容浏览器window对象和Deno对象下的属性和方法),然后mainRuntim方法后面会调用runtime.start()方法,在这里还有一个很重要的点就是获取deno上注册的op,返回的是name->opId的一个map,后面使用send调起rust的方法都需要用到这个opeId;还有一个点就是Deno.core.ops()这个方法并不是在前面bindings::initialize_context 去定义,而是在core.js上定义的。

export function initOps(): void {
  OPS_CACHE = core.ops();
  for (const [name, opId] of Object.entries(OPS_CACHE)) {
    core.setAsyncHandler(opId, getAsyncHandler(name));
  }
  core.setMacrotaskCallback(handleTimerMacrotask);
}

export function start(source?: string): Start {
  initOps();
  // 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 s = opStart();
  setVersions(s.denoVersion, s.v8Version, s.tsVersion);
  setBuildInfo(s.target);
  util.setLogDebug(s.debugFlag, source);
  setPrepareStackTrace(Error);
  return s;
}

接着下一步就是加载代码,交给TS Compiler编译,然后执行代码,这个流程里面关键是要递归去解决用户导入的其他代码:
worker.execute_module(&main_module).await?;

再下一步触发load事件,deno跟node的其中一个不同点就是兼容浏览器的一些事件,方法和属性,这样的一个好处明显就是降低了我们前端的学习成本,让我们编写的代码更容易符合我们的预期,那么为什么在开发node的时候不去兼容浏览器尼,我想大概当时的js和浏览器很多特性都是空白要么就是实验性质(node是2009年诞生),得益于这几年疯狂填坑,现在这个想法才有的可行性:
worker.execute("window.dispatchEvent(new Event('load'))")?;

再下一步:
(&mut *worker).await?;
这里就是deno事件循环开始的地方,只要deno程序不退出,这里的逻辑就会一直停顿在这里。

一旦我们结束了事件循环,那么下一步就触发unload事件,跟浏览器页面卸载一样(dispatchEvent是同步的):
worker.execute("window.dispatchEvent(new Event('unload'))")?;

结束

deno的启动流程暂时就分析到这里,下次再来分析事件循环和js与rust是如何交互的。

阅读 326

推荐阅读