5.15 Register / compile module

So far, all the modules including the main module have been fetched and compiled. They are still not loaded into v8 where they would get executed. It's time to get them into v8. The process is called registration and instantiation. This is only about loading, not running them.

Overview

By now all the modules have been fetched, whether from local or remote. A module graph has been built. All the modules have been checked or transpiled. At this point, it's all JS code. There is no TS code.
All the modules are now ready to get into v8. This is done in three steps:
  • Registering root module
    • This is specific to Deno
    • In v8 terms, this is called compiling module
  • Register imports
    • Go through all the imports and register/compile them
  • Instantiating root module into v8
    • This instantiates the root module in v8
Registration is a recursive process as it requires to loop through all the imports. In our hello world example, there are no imports. So we won't be going through imports in detail. We'll visit them in the next chapter.
let load = RecursiveModuleLoad::main(
self.op_state(),
&specifier.to_string(),
code,
loader,
);
let (_load_id, prepare_result) = load.prepare().await;
let mut load = prepare_result?;
while let Some(info_result) = load.next().await {
let info = info_result?;
self.register_during_load(info, &mut load)?;
}
let root_id = load.root_module_id.expect("Root module id empty");
self.mod_instantiate(root_id).map(|_| root_id)

Register / compile module

When the prepare() completes, the next step is to register the module into v8. This is done via a function called register_during_load(). This function is called in a loop till all the modules have been registered.
Recall the code present in JS runtime's load_module function. After recursive loading of modules is done, there is a loop that would register and compile all the modules. For each module, there is a call to register_during_load function.
fn register_during_load(
&mut self,
info: ModuleSource,
load: &mut RecursiveModuleLoad,
) -> Result<(), AnyError> {
let ModuleSource {
code,
module_url_specified,
module_url_found,
} = info;
let is_main =
load.state == LoadState::LoadingRoot && !load.is_dynamic_import();
let referrer_specifier =
ModuleSpecifier::resolve_url(&module_url_found).unwrap();
let state_rc = Self::state(self.v8_isolate());
if module_url_specified != module_url_found {
let mut state = state_rc.borrow_mut();
state
.modules
.alias(&module_url_specified, &module_url_found);
}
let maybe_mod_id = {
let state = state_rc.borrow();
state.modules.get_id(&module_url_found)
};
let module_id = match maybe_mod_id {
Some(id) => {
// Module has already been registered.
debug!(
"Already-registered module fetched again: {}",
module_url_found
);
id
}
// Module not registered yet, do it now.
None => self.mod_new(is_main, &module_url_found, &code)?,
};
let imports = {
let state_rc = Self::state(self.v8_isolate());
let state = state_rc.borrow();
state.modules.get_children(module_id).unwrap().clone()
};
for module_specifier in imports {
let is_registered = {
let state_rc = Self::state(self.v8_isolate());
let state = state_rc.borrow();
state.modules.is_registered(&module_specifier)
};
if !is_registered {
load
.add_import(module_specifier.to_owned(), referrer_specifier.clone());
}
}
// If we just finished loading the root module, store the root module id.
if load.state == LoadState::LoadingRoot {
load.root_module_id = Some(module_id);
load.state = LoadState::LoadingImports;
}
if load.pending.is_empty() {
load.state = LoadState::Done;
}
Ok(())
}
The function is big. There are a lot of steps in registration.

Step 1 - check if already registered

A module is registered only one time regardless of how many places it is imported. There is a unique module id for each module.
The first check is if it's a root module that's getting registered. In our example, it is the root and the only module that would get registered. There are no imports.
The second check is if the module is already registered. This is to ensure that a module gets registered exactly once. There is a unique module id associated with every module.
In our example, the module is the root module, and it's not registered.

Step 2 - Register module

Registration is a term used by Deno. However, in the v8 world, the equivalent is to compile a module. If the module is not registered, mod_new() is called to compile the module with v8. This is a compilation only.
fn mod_new(
&mut self,
main: bool,
name: &str,
source: &str,
) -> Result<ModuleId, AnyError> {
let state_rc = Self::state(self.v8_isolate());
let context = self.global_context();
let scope = &mut v8::HandleScope::with_context(self.v8_isolate(), context);
let name_str = v8::String::new(scope, name).unwrap();
let source_str = v8::String::new(scope, source).unwrap();
let origin = bindings::module_origin(scope, name_str);
let source = v8::script_compiler::Source::new(source_str, &origin);
let tc_scope = &mut v8::TryCatch::new(scope);
let maybe_module = v8::script_compiler::compile_module(tc_scope, source);
if tc_scope.has_caught() {
assert!(maybe_module.is_none());
let e = tc_scope.exception().unwrap();
return exception_to_err_result(tc_scope, e, false);
}
let module = maybe_module.unwrap();
let mut import_specifiers: Vec<ModuleSpecifier> = vec![];
for i in 0..module.get_module_requests_length() {
let import_specifier =
module.get_module_request(i).to_rust_string_lossy(tc_scope);
let state = state_rc.borrow();
let module_specifier = state.loader.resolve(
state.op_state.clone(),
&import_specifier,
name,
false,
)?;
import_specifiers.push(module_specifier);
}
let id = state_rc.borrow_mut().modules.register(
name,
main,
v8::Global::<v8::Module>::new(tc_scope, module),
import_specifiers,
);
Ok(id)
}
The steps are quite straightforward:
  • Do v8 specific initialization work like context, scope, error handler, etc.
  • Compile module source with v8
  • Loop through imports and push them in a list
  • Save module and import specifiers (last register call in above method)
  • Return module id
V8 assigns a module handle for each of the modules. This module handle needs to be saved for the module id. The call to register does that. It saves everything:
pub fn register(
&mut self,
name: &str,
main: bool,
handle: v8::Global<v8::Module>,
import_specifiers: Vec<ModuleSpecifier>,
) -> ModuleId {
let name = String::from(name);
let id = self.next_module_id;
self.next_module_id += 1;
self.by_name.insert(name.clone(), id);
self.handles_by_id.insert(id, handle.clone());
self.ids_by_handle.insert(handle, id);
self.info.insert(
id,
ModuleInfo {
id,
main,
name,
import_specifiers,
},
);
id
}
The following information is saved:
  • name
  • name to module id mapping
  • module id to handle mapping
  • handle to module id mapping
In our hello world example, there is a single main module that has no imports. The following JS code gets compiled into v8:
"use strict";
function printNumber(input) {
console.log(input);
}
function printString(input) {
console.log(input);
}
printNumber(1);
printString('One');
This is pure JS code.

Step 3 - Add imports to the pending list

This is almost the same as what we saw when building the module graph. Once a module is compiled in v8, it would return a list of imports. In the next step, all the imports are checked:
  • Loop through the imports
  • If the import is already registered,
    • skip it
  • Add module to the pending list by calling add_import
In our example, there are no imports. This step doesn't run.

Step 4 - save root module id

This is the last step of the registration process. While each module has a unique module id, the only id that gets saved is the root module id. This module id is used in the instantiation process.
In our case, the root module id turns out to be 1.
Id
Specifier
1
file:///Users/mayankc/Work/source/deno-vs-nodejs/helloLog.ts