5.15 Register / compile module

Until now, we've gathered and prepared all the pieces of code, known as modules, including the main one. But at this juncture, they're in a state of readiness, waiting for their turn to step onto the stage of action within the v8 engine. This phase we're entering is pivotal – it's when we introduce these modules to the v8 engine, a process referred to as registration and instantiation.

What's worth emphasizing is that, at this precise moment, our focus is solely on the loading part. We're not yet running the modules; that comes later. This separation of tasks ensures a clear boundary between two distinct phases: the loading phase, which is like gathering all the actors backstage, and the execution phase, which is when the play actually begins on the stage.

Overview

By this juncture, all the modules, regardless of their origin—be it local or remote—have been successfully retrieved. This process culminates in the creation of a module graph, while simultaneously transpiling all modules into JavaScript code. It is vital to emphasize that, at this stage, the entirety of the code is in JavaScript; TypeScript (TS) code no longer remains.

With the completion of these preliminary steps, all the modules are poised for integration into the V8 engine. This integration process comprises three distinctive phases:

  1. Root Module Registration: This initial step is intrinsic to the Deno ecosystem. Within V8 parlance, this procedure corresponds to the compilation of a module. Its unique significance to Deno's operation is underscored here.

  2. Imports Registration: A comprehensive scan of all imports is undertaken, resulting in their systematic registration and subsequent compilation. This step ensures the seamless interconnection of various modules, allowing for a cohesive functioning of the codebase.

  3. Root Module Instantiation within V8: In this concluding phase, the root module is actualized within the V8 engine. This marks the culmination of the integration process, where the orchestrated modules become functional entities within the runtime environment.

The process of registration within Deno involves a repeating action, as it necessitates examining all the imports involved. We've previously observed the mechanics of this process through the primary API known as "register_and_recurse." In our illustrative example of a basic script (helloLog.ts), no external modules are imported. As a result, we won't delve deeply into import procedures in this chapter; this aspect will be explored in the subsequent chapter. As a brief refresher, let's review the summarized code for "register_and_recurse." However, this time, our focus will be solely on the code that pertains to loading modules into the V8 engine.

The underlying concept is remarkably straightforward:

  • If the module's ID already exists, it signifies that the module has already been loaded into the V8 engine.

  • Conversely, if the module ID is not found,

    • The module is loaded into the V8 engine (whether it's an ES or JSON module)

    • The module ID provided by V8 is then recorded for future reference.

This fundamental logic governs the registration process within Deno, ensuring efficient handling of module loading and availability in the V8 runtime environment.

Keep in mind that the registration and loading of modules occur as the module graphs are being constructed. This means that when Deno is building the graphical representation of how different modules connect and depend on each other, it's also simultaneously going through the steps of registering those modules and loading their contents.

pub(crate) fn register_and_recurse(
    &mut self,
    scope: &mut v8::HandleScope,
    module_request: &ModuleRequest,
    module_source: ModuleSource,
  ) -> Result<(), ModuleError> {
  ....
  let maybe_module_id = self
      .module_map_rc
      .borrow()
      .get_id(&module_url_found, expected_asserted_module_type);
    let module_id = match maybe_module_id {
      Some(id) => {
        debug!(
          "Already-registered module fetched again: {:?}",
          module_url_found
        );
        id
      }
      None => match module_source.module_type {
        ModuleType::JavaScript => {
          self.module_map_rc.borrow_mut().new_es_module(
            scope,
            self.is_currently_loading_main_module(),
            module_url_found,
            module_source.code,
            self.is_dynamic_import(),
          )?
        }
        ModuleType::Json => self.module_map_rc.borrow_mut().new_json_module(
          scope,
          module_url_found,
          module_source.code,
        )?,
      },
    };
  ....  
}

Step 1 - check if already registered

A module is only registered once, no matter how many times it's imported. Each module is assigned a distinct identification number. The registration process involves a couple of checks.

The initial check determines if the module being registered is a root module. In our provided example, it indeed functions as the root module and stands as the solitary module subject to registration. As it contains no imports from other modules, this primary registration stands alone.

The subsequent check involves verifying whether the module has already been registered. This precaution ensures that each module is registered precisely once. Each module possesses a unique identification number to facilitate this process.

In the context of our example, the module retains its role as the root module and remains unregistered up to this point. This double-check mechanism guarantees that the module, although a root one, hasn't been previously registered. This prevents redundant registrations and maintains the one-time registration principle.

Step 2 - Register module

The concept of "registration" is fundamental within the context of Deno. However, when we consider the v8 environment, a comparable action involves the compilation of a module. When a module hasn't been registered, the system invokes either of the subsequent APIs:

  • new_es_module, which serves to load ES modules

  • new_json_module, designed specifically for loading JSON modules.

The following is the code of new_es_module:

pub(crate) fn new_es_module(
    &mut self,
    scope: &mut v8::HandleScope,
    main: bool,
    name: ModuleName,
    source: ModuleCode,
    is_dynamic_import: bool,
  ) -> Result<ModuleId, ModuleError> {
    let name_str = name.v8(scope);
    let source_str = source.v8(scope);

    let origin = module_origin(scope, name_str);
    let source = v8::script_compiler::Source::new(source_str, Some(&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 exception = tc_scope.exception().unwrap();
      let exception = v8::Global::new(tc_scope, exception);
      return Err(ModuleError::Exception(exception));
    }

    let module = maybe_module.unwrap();

    let mut requests: Vec<ModuleRequest> = vec![];
    let module_requests = module.get_module_requests();
    for i in 0..module_requests.length() {
      let module_request = v8::Local::<v8::ModuleRequest>::try_from(
        module_requests.get(tc_scope, i).unwrap(),
      )
      .unwrap();
      let import_specifier = module_request
        .get_specifier()
        .to_rust_string_lossy(tc_scope);

      let import_assertions = module_request.get_import_assertions();

      let assertions = parse_import_assertions(
        tc_scope,
        import_assertions,
        ImportAssertionsKind::StaticImport,
      );

      // FIXME(bartomieju): there are no stack frames if exception
      // is thrown here
      validate_import_assertions(tc_scope, &assertions);
      if tc_scope.has_caught() {
        let exception = tc_scope.exception().unwrap();
        let exception = v8::Global::new(tc_scope, exception);
        return Err(ModuleError::Exception(exception));
      }

      let module_specifier = match self.loader.resolve(
        &import_specifier,
        name.as_ref(),
        if is_dynamic_import {
          ResolutionKind::DynamicImport
        } else {
          ResolutionKind::Import
        },
      ) {
        Ok(s) => s,
        Err(e) => return Err(ModuleError::Other(e)),
      };
      let asserted_module_type =
        get_asserted_module_type_from_assertions(&assertions);
      let request = ModuleRequest {
        specifier: module_specifier.to_string(),
        asserted_module_type,
      };
      requests.push(request);
    }

    if main {
      let maybe_main_module = self.info.iter().find(|module| module.main);
      if let Some(main_module) = maybe_main_module {
        return Err(ModuleError::Other(generic_error(
          format!("Trying to create \"main\" module ({:?}), when one already exists ({:?})",
          name.as_ref(),
          main_module.name,
        ))));
      }
    }

    let handle = v8::Global::<v8::Module>::new(tc_scope, module);
    let id = self.create_module_info(
      name,
      ModuleType::JavaScript,
      handle,
      main,
      requests,
    );

    Ok(id)
  }

The process involves several simple and clear steps as outlined below:

  1. Perform v8 Specific Initialization: This step entails setting up various essential components such as context, scope, and an error handler that are specific to the v8 engine.

  2. Compile Module Source Code: The module's source code is compiled using the v8 engine, transforming it into a format that can be readily executed.

  3. Manage Imports: The program then proceeds to iterate through the imported modules and adds them to a list, ensuring that all required components are accounted for.

  4. Record Module and Import Specifiers: The last registration call made in the previous step serves to save the module's details and its associated import specifiers. This information is crucial for maintaining the structure and dependencies of the program.

  5. Provide Module Identifier: The final step involves returning the unique module identifier, allowing the program to refer back to this module when needed.

Similarly, the following is the code for new_json_module:

pub(crate) fn new_json_module(
    &mut self,
    scope: &mut v8::HandleScope,
    name: ModuleName,
    source: ModuleCode,
  ) -> Result<ModuleId, ModuleError> {
    let name_str = name.v8(scope);
    let source_str = v8::String::new_from_utf8(
      scope,
      strip_bom(source.as_bytes()),
      v8::NewStringType::Normal,
    )
    .unwrap();

    let tc_scope = &mut v8::TryCatch::new(scope);

    let parsed_json = match v8::json::parse(tc_scope, source_str) {
      Some(parsed_json) => parsed_json,
      None => {
        assert!(tc_scope.has_caught());
        let exception = tc_scope.exception().unwrap();
        let exception = v8::Global::new(tc_scope, exception);
        return Err(ModuleError::Exception(exception));
      }
    };

    let export_names = [v8::String::new(tc_scope, "default").unwrap()];
    let module = v8::Module::create_synthetic_module(
      tc_scope,
      name_str,
      &export_names,
      json_module_evaluation_steps,
    );

    let handle = v8::Global::<v8::Module>::new(tc_scope, module);
    let value_handle = v8::Global::<v8::Value>::new(tc_scope, parsed_json);
    self.json_value_store.insert(handle.clone(), value_handle);

    let id =
      self.create_module_info(name, ModuleType::Json, handle, false, vec![]);

    Ok(id)
  }

et's delve into our introductory example in Deno. In this case, we have a sole primary module that doesn't rely on importing any external modules. The JavaScript code provided below is subjected to compilation and subsequently loaded into the V8 engine:

"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

The process here is quite similar to what we observed while constructing the module graph. After a module gets compiled within the v8 engine, it generates a list of items it imports. The subsequent phase involves the examination of these imports:

  • The imports are sequentially traversed.

  • If a particular import has already been registered,

    • it is disregarded and the process moves forward.

  • The module is then included in the pending list through a function call known as add_import.

Applying this to our current instance, where no imports are present, this particular phase does not actively take place.

Step 4 - Save root module id

This marks the final stage of the registration process. Every module has its own special module id, but only the root module id is preserved and stored. This particular module id plays a crucial role in the creation process.

In the context of our situation, the root module id happens to be 1. This number serves as a significant identifier that guides the instantiation process.

Id

Specifier

1

file:///Users/mayankc/Work/source/denoExamples/helloLog.ts

Step 5 - Save module metadata

The final action involves preserving the module metadata that V8 provides. Among the crucial pieces of information, the module handle stands out. Our task is to ensure that the module handle is associated with the corresponding module ID and kept for future reference.

fn create_module_info(
    &mut self,
    name: FastString,
    module_type: ModuleType,
    handle: v8::Global<v8::Module>,
    main: bool,
    requests: Vec<ModuleRequest>,
  ) -> ModuleId {
    let id = self.handles.len();
    let (name1, name2) = name.into_cheap_copy();
    self
      .by_name_mut(module_type.into())
      .insert(name1, SymbolicModule::Mod(id));
    self.handles.push(handle);
    self.info.push(ModuleInfo {
      id,
      main,
      name: name2,
      requests,
      module_type,
    });

    id
  }

Last updated