WebAssembly 模塊化與動態鏈接

  1. 前言

模塊化編程(modular programming)是一種軟件設計模式,它將軟件分解爲若干獨立的、可替換的、具有預定功能的模塊,每個模塊實現一個功能,各模塊通過接口(輸入輸出部分)組合在一起形成最終程序。當下流行的 JavaScript、Python、Rust、Java 等語言都有具有模塊 (包) 管理,甚至 C++20 開始都引入了模塊化系統。

模塊化編程從 1980 年代開始廣泛傳播,是 SoC (Separation of concerns)[1] 原則的理想目標,主要有如下特點:

模塊可以理解爲是一個實現特定功能的獨立且通用的代碼單元,意在解決代碼拆分、作用域隔離、功能依賴耦合等問題,提升代碼的可維護性。然而,由於模塊化往往基於代碼的分離而構建獨立出代碼單元,單一的模塊往往無法作爲一個單獨運行單元,因此在運行過程中不可避免的需要引入動態鏈接的機制。

WebAssembly 作爲可移植且兼容 Web 的全新格式,基於模塊化的 WebAssembly 動態鏈接機制,可以將 WebAssembly 應用程序的核心邏輯分離出來,可以更容易地共享,從而消除重複邏輯;功能獨立的小模塊具有更高的分發、下載、加載效率,並且可以做到按需加載,從未使用的模塊永遠不會被下載;此外,WebAssembly 可鏈接模塊還可以進行並行流式編譯、進行模塊緩存、運行期動態鏈接,從而進一步提升加載和啓動效率。

接下來,本文會從 WebAssembly 的模塊化演進入手,介紹其模塊化和動態鏈接的關鍵設計和實現,以及當前面臨的挑戰和未來的發展趨勢。

  1. JS 和 asm.js 模塊化和動態鏈接

課程的第 1 章已經對 WebAssembly 的演進歷史做了介紹,從其歷史發展路徑我們可以看出,WebAssembly 很多關鍵技術與 JavaScript 發展趨勢和方向有很強的關聯性,因此,本文先從 JavaScript 模塊及其動態鏈接入手,進而分析 WebAssembly 多模塊及動態鏈接相關設計和關鍵實現。

2.1 JavaScript 模塊與動態鏈接

衆所周知,JavaScript 有 CMJ(CommonJS)[2],AMD(Asynchronous Module Definition)[3],UMD(Universal Module Definition)[4],ESM(ECMAScript modules)[5] 等多種主要的模塊化規範。由於 CommonJS 接受度較高並且與 WebAssembly 設計比較接近,本文中的示例將基於 CommonJS 模塊化規範進行定義。

在 CommonJS 模塊化規範中,單個獨立的 JavaScript 文件可以被作爲一個模塊,模塊中默認的定義僅在模塊內部可見,文件對外接口和對象需通過 module.exports 對外暴露;此外,JavaScript 文件可以通過 require() 接口來獲取對應模塊導出的對象,從而完成所需接口和對象的鏈接過程。在下圖 1 的示例中,square.js 中定義了 Square 類用於計算矩形區域面積方法 area();爲了計算 square 的面積大小,calculator.js 可以通過調用require(square.js) 來加載 squre.js 模塊並導入 Square 類,而不需要重新實現計算 square 面積的方法,從而實現代碼複用和共享。

圖 1. JavaScript 模塊鏈接示意圖

從上圖 1 的示例中,我們可以發現,JavaScript 通過 require 方法來進行 JavaScript 模塊的加載和符號的動態鏈接,那最終符號的動態鏈接過程又是如何實現的呢?接下來,我們將對 NodeJS CommonJS 模塊的加載和動態鏈接過程進行分析,從而展示 JavaScript 模塊的動態鏈接原理及運行機制 [6]。

在上面的源代碼中,wrap 是 NodeJS 中模塊的包裝器函數,wrapper 是模塊的包裝模板。在 JavaScript (*.js, *.mjs) 的加載過程中,如果該文件作爲一個獨立的模塊時,NodeJS Loader 會首先通過 "模塊封裝器" 函數 wrap 將 JavaScript 源代碼包裝成匿名的函數表達式對象,如下面的源代碼所示。匿名函數表達式將 var、const 或 let 定義的頂級變量作用域範圍限制在模塊中而不是全局對象中,函數表達式參數 "module" 和 "exports" 可以用於從模塊中導出值,"require" 參數用於導入外部模塊,外部模塊除了 JavaScript 文件,還可以是 json 文件以及本地 node 文件,__filename 和 __dirname 參數包含模塊的絕對文件名和目錄路徑。

(function(exports, require, module, __filename, __dirname) {
   // 模塊代碼實際存在於此處
});

當 NodeJS 將模塊文件包裝爲運行時環境中的匿名函數對象後,當 JavaScript 通過 require(path) 函數嘗試加載模塊文件時,實際執行 NodeJS 加載器 loader.js[6] 中的 Module.prototype.require 函數,進而調用 Module._load 執行模塊加載和鏈接,如下列的源代碼所示。

/*
 * source link:
 * https://github.com/nodejs/node/blob/8822f40b2d48841c6d4fb4c04266a5703bdf33e9/lib/internal/modules/cjs/loader.js#L1095
 */
// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(id) {
  /* ... skip irrelevant code */
  requireDepth++;
  try {
    return Module._load(id, this, /* isMain */ false);
  } finally {
    requireDepth--;
  }
};

Module._load 爲了提升模塊加載的效率定義了 Module._cache 用於緩存已經加載的模塊並返回 module.exports 對象。如果模塊已經完成加載則直接返回,否則,爲文件創建一個 Module 實例後放入模塊緩存中,進而調用 Module 實例的 load 函數執行模塊的加載和鏈接過程,如下列源代碼所示。

/*
 * source link:
 * https://github.com/nodejs/node/blob/8822f40b2d48841c6d4fb4c04266a5703bdf33e9/lib/internal/modules/cjs/loader.js#L844
 */
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call
//    `BuiltinModule.prototype.compileForPublicLoader()` and return the exports.
// 3. Otherwise, create a new module for the file and save it to the cache.
//    Then have it load  the file contents before returning its exports
//    object.
Module._load = function(request, parent, isMain) {
  /* ... skip irrelevant code */
  // Don't call updateChildren(), Module constructor already does.
  const module = cachedModule || new Module(filename, parent);
  /* ... skip irrelevant code */
  Module._cache[filename] = module;
  /* ... skip irrelevant code */
  let threw = true;
  try {
    module.load(filename);
    threw = false;
  } finally {
    /* ... skip irrelevant code */
  }
  return module.exports;
};

由於 require 方法可以加載多種類型的文件 (*.json, *.node, *.js), Module.prototype.load 函數首先根據不同的文件擴展類型獲取對應文件的處理函數。當加載 JavaScript 模塊時, Module.prototype.load 函數實際調用 Module._extensions['.js'] 處理函數完成 JavaScript 模塊的編譯和執行邏輯,如下列源代碼所示。

/*
 * source link:
 * https://github.com/nodejs/node/blob/8822f40b2d48841c6d4fb4c04266a5703bdf33e9/lib/internal/modules/cjs/loader.js#L1068
 */
// Given a file name, pass it to the proper extension handler.
Module.prototype.load = function(filename) {
  /* ... skip irrelevant code */
  // return the filename extension
  const extension = findLongestRegisteredExtension(filename);
  /* ... skip irrelevant code */
  // invoke the extension handler, where *.js will
  // invoke Module._extensions['.js'] handler
  Module._extensions[extension](this, filename);
  this.loaded = true;
  /* ... skip irrelevant code */
};

NodeJS 中 Module._extensions['.js'] 處理函數首先通過 fs 加載文件內容,然後調用模塊實例的 Module.prototype._compile 函數完成模塊文件內容的編譯,如下列源代碼所示。

/*
 * source link:
 * https://github.com/nodejs/node/blob/8822f40b2d48841c6d4fb4c04266a5703bdf33e9/lib/internal/modules/cjs/loader.js#L1226
 */
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  // If already analyzed the source, then it will be cached.
  const cached = cjsParseCache.get(module);
  let content;
  if (cached?.source) {
    content = cached.source;
    cached.source = undefined;
  } else {
    content = fs.readFileSync(filename, 'utf8');
  }
  /* ... skip non-critical code */
  module._compile(content, filename);
};

模塊實例的 Module.prototype._compile 函數會在模塊上下文中執行 "模塊包裝器" 生成的匿名函數對象,並傳遞正確的 requiremoduleexports 參數對象,如下列源代碼所示。

/**
 * source code links
 * https://github.com/nodejs/node/blob/8822f40b2d48841c6d4fb4c04266a5703bdf33e9/lib/internal/modules/cjs/loader.js#L1169
 */
// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
// the file.
// Returns exception, if any.
Module.prototype._compile = function(content, filename) {
  /* ... skip non-critical code */
  // content will be wrapped as follows by the wrapper
  // (function(exports, require, module, __filename, __dirname) {
  //    content
  // });
  const compiledWrapper = wrapSafe(filename, content, this);
  /* ... skip non-critical code */
  // prepare the arguments for the wrapped function
  const dirname = path.dirname(filename);
  const require = makeRequireFunction(this, redirects);
  let result;
  const exports = this.exports;
  const thisValue = exports;
  const module = this;
  /* ... skip non-critical code */
  // invoke the wrapped function with arguments prepared
  // (function(exports, require, module, __filename, __dirname) {
  //    content
  //    module.exports = {}
  // });
  result = ReflectApply(compiledWrapper, thisValue,
                        [exports, require, module, filename, dirname]);
  /* ... skip non-critical code */
  return result;
};

從以上的分析過程,可以發現 require 方法執行過程完成了 CommonJS 模塊的動態加載和鏈接過程。exportsrequiremodule 等函數參數是導入模塊和導出模塊的共享對象,基於這些共享對象,JavaScript 模塊才得以在動態加載的過程中完成符號的動態鏈接過程。

2.2 "asm.js" 模塊與動態鏈接

asm.js 是 WebAssembly 的前身,是一種可用於編譯期的低層級的、高效的 JavaScript 的一個嚴格子集,它可以通過 AOT (Ahead-Of-Time,靜態編譯)策略來編譯優化代碼,因此,其模塊化和對外交互特性 (鏈接) 顯得尤爲重要。

下面給出的是一個標準 asm.js 模塊的基本結構,通過這種模塊化的結構,asm.js 可以保證模塊內部的所有代碼都遵循自己獨有的標準和語法規則,即所有模塊內部使用到的變量都保證已經通過 Annotation 的方式進行了強制類型聲明。除此之外,asm.js 模塊作爲一個整體也在代碼層面與原始的 JavaScript 代碼進行了隔離,同時其內部還可以通過暴露出的接口與標準 JavaScript 代碼進行交互,如下列代碼所示。

/* MyAsmModule module definition */
function MyAsmModule(stdlib, foreign, heap) {
    /* asm.js module declare */
    "use asm";
    /* varaible defintion */
    var variable = 0;
    /* funtion defintion */
    function add($0$1) {
      $0 = $0 | 0; // annotate $0 is integer
      $1 = $1 | 0; // annotate $1 is integer
      $2 = (($1) + ($0) | 0); 
      return ($2 | 0); // annotate $2 is integer
    }
    /* module body... */
    /* functions exporting */
    return {
      add: add,
      export_func2: f2,
      /* other functions */
    };
}
/* MyAsmModule module user */
const buffer_size = 0x10000;
/* import function defitinion */
const inport_funcs = {
    import_func1: fa
    /* other functions */
};
/* shared memory definition */
var heap = new ArrayBuffer(buffer_size);
/* create and initialize MyAsmModule */
var asModule = MyAsmModule(window, inport_funcs, heap);
/* invoke asm.js exported function */
var sum = asModule.add(2,3);

從整體上看,asm.js 模塊是一個標準的 JavaScript 函數,函數內部的第一行使用 “use asm” 標記來對模塊進行聲明。一個完整的 asm.js 模塊其內部被分爲三個部分: 變量定義、函數定義和函數導出。模塊導出的函數可以被其他 asm.js 模塊引用,或者直接在 JavaScript 環境下通過 JavaScript 代碼來調用運行。

一個 asm.js 模塊最多可以接受三個可選參數,提供對外部 JavaScript 代碼和數據的訪問:

asm.js 模塊的參數使得模塊可以調用外部 JavaScript,並與外部 JavaScript 共享其 ArrayBuffer 堆緩衝區;相反,從模塊返回的導出對象允許外部 JavaScript 調用 asm.js;asm.js 對象的交互和綁定過程我們稱之爲 asm.js 模塊鏈接。asm.js 模塊化和鏈接的底層機制基本沿襲了 JavaScript 模塊和鏈接的設計,並在一定程度上被 WebAssembly 繼承並發展,雖然 asm.js 標準自 2014 年發佈至今已經鮮有人關注了,但可以爲我們深入理解 WebAssembly 模塊及鏈接機制提供幫助。

接下來,就讓我們一起對 WebAssembly 的模塊化和鏈接機制進行分析和理解。

  1. WebAssembly 模塊及動態鏈接

WebAssembly 在 asm.js 的基礎上對模塊和鏈接機制進行了擴展,它定義了 import 和 export 段來聲明與外界環境交互的關鍵對象和組件;WebAssembly 模塊介紹請參見課程的第 4 章內容。

asm.js 模塊通過輸入參數來引入外界環境的變量,爲了提供統一入口來導入多種不同的類型,WebAssembly 定義了 Import 段來聲明需要使用到的外界環境變量。Import 段會聲明模塊所使用的所有類型的導入對象,包括 Fucntion 對象、Table 對象、 Memroy 對象 或 Global 對象。Import 的設計初衷是使模塊可以共享代碼和數據,同時支持模塊獨立編譯和模塊緩存;在模塊進行實例化階段,這些導入對象將由宿主環境或者三方模塊提供。

與 asm.js 通過返回值來實現模塊的內部函數導出給外部環境使用不同,WebAssembly 採用了類似 CommonJS 的更友好和靈活的方式,它通過與 module.exports 相似的 Export 段方式來導出不同類型的內部變量。Export 段聲明瞭一個對象的列表,其中包含了模塊實例化後外部環境可用的各種類型模塊內部定義的對象,這些對象可以是 Function、 Table、Memory 或 Global 中的任意類型。

因此,WebAssembly 作爲可移植性,語言和平臺無關的發佈產物,可以嵌入在衆多平臺上運行;它的運行時加載和宿主的鏈接過程主要通過 importexport 兩個段中的內容來完成;其中,包含了 WebAssembly 的 4 種類型的關鍵對象,他們分別是 Function,Global,Memory,Table。

圖 2. WebAssembly 模塊鏈接示意圖

WebAssembly 可以嵌入衆多的宿主及不同的語言,並仍在不斷的在擴展現有邊界;上圖 2 展示了 WebAssembly 與 外部環境對象鏈接,內存共享和交互的主要場景;其中 JavaScript 是 WebAssembly 相對成熟的宿主環境和語言,因此,接下來,本文將以 JavaScript 作爲宿主環境,通過如下三個關鍵的場景來深入分析 WebAssembly 與 JavaScript 環境的運行期動態鏈接原理及其可能的實現。

3.1 WebAssembly exports -> JavaScript imports

上圖 2 所示的 WebAssembly 鏈接場景中,shared-module.wasm 提供了一個共享的 WebAssembly 模塊,定義並導出了 WebAssembly 核心類型的對象,包括 Global 棧指針變量 stack_pointer,Table 對象 indrect_funtion_table,memory 對象以及全局函數 fibdistance。JavaScript 宿主在運行期可以動態加載 shared-module.wasm 文件,並通過 WebAssembly 執行環境創建 shared-module 模塊實例,最後爲 WebAssembly 執行環境中的 exports 對象在 JavaScript 環境中創建對應的 JavaScript 對象實例,從而完成 shared-module.wasm 模塊的動態加載和鏈接過程;此後,在運行環境中就可以按照 JavaScript 訪問方式來範圍和調用 WebAssembly exports 的多種類型的對象實例,如下列代碼所示。

/* source link:
 * https://github.com/yaozhongxiao/webassembly_tech/tree/master/samples/module-linking/js-shared-module-linking.js
 */ 
JSModule = {};
/* ... skip non-critical code */
let instance = wasmLoad(__dirname + "/lib/shared-module.wasm", JSModule);
/* exports memory */
let memory = instance.exports.memory;
/* exports global */
let sp = instance.exports.stack_pointer;
/* exports func */
let fn_fib = instance.exports.fib;
let fn_distance = instance.exports.distance;
/* exports table */
let tbl = instance.exports.indirect_function_table;

3.2 JavaScript exports -> WebAssembly imports

上圖 2 所示的 WebAssembly 鏈接場景中,user-module.wasm 定義了一個 WebAssembly 應用程序,其依賴於宿主環境提供的應用所需要的 WebAssembly 核心類型的對象,包括 Global 棧指針變量 stack_pointer,Table 對象 indrect_funtion_table,memory 對象以及全局函數 fibdistance。爲了 WebAssembly 執行環境在加載和實例化 user-module.wasm 模塊時能夠解析模塊中的未定義符號,JavaScript 宿主需提供滿足鏈接需求的 JSModule 對象,WebAssembly 虛擬機會在執行環境中創建與 JSModule 對應的 WebAssembly 實例對象,並將 JSModule 與 WebAssembly 實例對象綁定,從而完成未定義符號解析和鏈接。此後,WebAssembly 執行環境中就可以按照其原生對象訪問方式來訪問 JSModule 對象,如下列代碼所示。

/* source link:
 * https://github.com/yaozhongxiao/webassembly_tech/tree/master/samples/module-linking/js-user-module-linking.js
 */ 
/* ... skip non-critical code */
/* #################### 1. define the JSModule ##################### */
/* define S.funcs */
function fib(num) {
  if (num == 1 || num == 0) {
    return num;
  }
  return fib(num - 1) + fib(num - 2);
}
function distance(n1, n2) {
  let ret = Math.abs(n1 - n2);
  console.log("invoke distance(" + n1 + ", " + n2 + ") = " + ret);
  return ret;
}
/* define S.table */
const fn_table = new WebAssembly.Table({
  initial: 2,
  maximum: 2,
  element: "anyfunc",
});
/* define S.memory */
const importMemory = new WebAssembly.Memory({
  initial: 256,
  maximum: 32768,
});
const sp = new WebAssembly.Global({ value: "i32", mutable: true }, 5243920);
JSModule = {
  share_ctx: {
    stack_pointer: sp,
    fib: fib,
    distance: distance,
    indirect_function_table: fn_table,
    memory: importMemory
  },
  env: {
    print: console.log.bind(console),
  },
};
/* ############ 2. user-module load and link with JSDepModule ############# */
let instance = wasmLoad(__dirname + "/lib/user-module.wasm", JSModule);
/* ... skip non-critical code */

3.3 WebAssembly exports -> WebAssembly imports

現有的 WebAssembly 引擎中並沒有標準化的 WebAssembly 模塊間鏈接實現,雖然 wasm-micro-runtime[12] 中有多模塊和加載時鏈接機制,但使用場景相對有限而且並非是遵循標準化規範的實現。在 JavaScript 環境中,我們可以通過前 2 種方式的組合來實現 WebAssembly exports -> JavaScript re-exports -> WebAssembly imports 的模式,從而間接實現 WebAssembly 模塊間的動態鏈接機制。JavaScript 運行環境首先加載並實例化 shared-module,並將 WebAssembly 實例對象的 exports 導出變量綁定到 JSModule 對象上,這些綁定到 JSModule 上的 WebAssembly 實例對象的 exports 導出變量,都將作爲不可變的綁定提供給其他 WebAssembly 模塊。因此,shared-module 導出對象被包裝到一個 JSModule 對象,包括 Global 棧指針變量 stack_pointer,Table 對象 indrect_funtion_table,memory 對象以及全局函數 fibdistance。WebAssembly 虛擬機在加載和實例化 user-module.wasm 模塊時,將 JSModule 中 shared-module 對應的實例綁定導入到 user-module 模塊中的 WebAssembly imports 中,從而完成 user-module 與 shared-moudule 導入導出符號的動態鏈接,如下列代碼所示。

  /* source link:
   * https://github.com/yaozhongxiao/webassembly_tech/tree/master/samples/module-linking/js-user-module-linking.js
   */ 
   /* ... skip non-critical code */
   /* #################### 1. wasmLoad shared-module ##################### */
  let sharedModule = wasmLoad(__dirname + "/lib/shared-module.wasm"{});
  /* #################### 2. Initialize JSModule with sharedModule ##################### */
  JSModule = {
    share_ctx: {
      stack_pointer: sharedModule.exports.stack_pointer,
      fib: sharedModule.exports.fib,
      distance: sharedModule.exports.distance,
      indirect_function_table: sharedModule.exports.indirect_function_table,
      memory: sharedModule.exports.memory,
    },
    env: {
      print: console.log.bind(console),
    },
  };
  /* ############ 3. load user-module and link with JSDepModule ############# */
  let instance = wasmLoad(__dirname + "/lib/user-module.wasm", JSModule);
  /* ... skip non-critical code */

在本小節中,我們構建了 WebAssembly 與 JavaScript 環境的運行期動態鏈接的三種核心使用場景示例,並深入分析了模塊鏈接的原理及其可能的實現;由於篇幅有限,我們僅在文中展示了示例程序的核心實現代碼,本節所涉及到的示例代碼及完整實現請參見 webassembly_tech[11] 倉庫,讀者可以方便的按照倉庫中的指引快速重建和運行本節中所有的示例程序,

  1. WebAssembly 動態鏈接發展趨勢

4.1 WebAssembly/ES Module Integration

在闡述 WebAssembly 在 JavaScript 環境中的動態鏈接時,我們通過 W3C 定義的標準 JavaScript API 實例化 WebAssembly 模塊,該過程中需要用戶手動、顯示地獲取模塊文件,鏈接導入導出函數,並調用 WebAssembly.instantiateWebAssembly.instantiateStreaming 進行模塊實例化,如下列代碼所示。

let req = fetch("./shared-module.wasm");
let imports = {
    env: {
        print
    }
 };
 WebAssembly.instantiateStreaming(req, imports)
 .then(obj => obj.instance.exports.fib());

從工程學上來說,WebAssembly 模塊的顯示實例化方式是非常不友好和優雅的解決方案。雖然,我們也嘗試在原型實現中提供 wasm-loader.js [14] 庫來複用 WebAssembly 實例化過程,但 WebAssembly 宿主和語言環境的巨大差異不可避免地導致它們需要實現各自的庫,這亟需 WebAssembly 社區統一規範,從而實現進行標準化。由於 WebAssembly 和 JavaScript 的淵源以及 ES Module 組件的標準化進程,ECMAScript Module Integration [15] 提案嘗試添加聲明式 API 來隱藏 WebAssembly 文件請求、加載、實例化和鏈接過程,如下列代碼所示。

import {fib } from "./shared-module.wasm"
import
/* counter.js  */
let count = 42;
function getCount() {
    return count;
}
export {getCount};

;; main.wat --> main.wasm
(module
  (import "./counter.js" "getCount" (func $getCount (func (result i32))))
)

此外,爲了使 JavaScript 開發人員可以輕鬆地組合來自 WebAssembly 模塊和 JavaScript 模塊的功能。ECMAScript Module Integration [5][15][16] 提案意圖實現 WebAssembly 模塊與 JavaScript 模塊的融合,使得 WebAssembly 可以參與 JavaScript 模塊圖,從而使得 JavaScript 和 WebAssembly Module 在 ESM 模塊上做到統一,如下圖 3 所示。

圖 3. WebAssembly 與 JavaScript 模塊集成示意圖

4.2 Module Linking Proposal

WebAssembly 起源於 Web,但又不侷限於 Web 的應用範疇。在前面小節中,結合 WebAssembly 模塊的歷史演進,深入解析了其模塊和動態鏈接的設計。雖然在特定的宿主和語言環境下,我們可以通過特殊的約定來實現部分模塊動態鏈接能力;但隨着 WebAssembly 應用場景和語言環境的不斷豐富,這種方式不可避免的會阻礙了模塊化和代碼重用,導致生產中的代碼重複和語言間的隔離。爲了解決 WebAssembly 獨立定義實例化和動態鏈行爲的困境,Module Linking[17] 曾作爲 WebAssembly Proposal 在 CG 被提出,它希望建立一個可移植的、獨立於宿主和語言的可組合 WebAssembly 模塊生態系統。

在當前的標準中,WebAssembly 只允許導入已實例化模塊的導出對象,而 Module Linking 提案的中心思想是允許主模塊將其依賴項作爲模塊導入;即,通過擴展 WebAssembly 模塊規範,將模塊及其依賴關係在 WebAssembly 模塊和二進制格式中進行標準化,從而實現依賴項作爲模塊導入,並由主模塊控制依賴項的實例化 (提供導入) 和 鏈接 (公開導出)。

Module Linking 提案避免了對任何類型的運行時加載程序 (如 ld.so) 的依賴,相反,它讓 WebAssembly 運行時最終完成所有工作。Module Linking 提案在 WebAssembly 二進制格式中添加了三個新的 Section 以及兩個新的索引空間,他們分別是 模塊索引空間和實例索引空間,其中:

Module Linking 意在保證模塊在加載之前的獨立性,以便多個程序可以共享公共模塊。在下圖 4 中包含 zipper 和 imgmgk 兩個程序,libc、libzip 和 libimg 三個共享模塊;在模塊加載之前,各模塊間形成了圖 4 上半部分所示的模塊靜態依賴關係圖;基於 Module Linking 的動態鏈接機制,使得 WebAssembly 運行時在實例化過程中,可以根據模塊靜態依賴關係圖創建動態鏈接的實例圖,而不再依賴宿主的動態加載能力,如下圖 4 所示。

圖 4. WebAssembly 動態鏈接概念示意圖

然而,模塊動態鏈接機制最初在 Module Linking 提案中首先提出,但是該提案暫時處於 "inactive" 狀態,而相關的工作轉到了 "Component Model" 提案中繼續推進 [17];基於此,在接下來的小節中,我們將對組建模型提案做下簡要的介紹。

4.3 Component Model Proposal

ESM-Integrate [15] 和 Module-Link[17] 提案嘗試 "自下而上" 的從已知問題入手,通過修改 WebAssembly 規範,針對性的進行改良;而 "Component Model"[18] 提案則希望 "自上而下" 的基於模塊化 (組件化) 模型,制定 WebAssembly 的下一代標準。Component Model 提案的核心內容已基本標覆蓋了 Module-Link 提案的關鍵內容,其中,涉及到 WebAssembly 模塊化和動態鏈接的目標,概括起來主要有如下幾個方面:

Component Model 提案既制定了宏偉的目標,又提出了實施方針;即,從初始用例集出發,增量式進行的完善。爲了做到既兼容 WebAssembly 的核心規範,又完成組件模型這個複雜系統的設計和實現,Component Model 提案也採用了計算機科學領域的通用解決方案 (All problems in computer science can be solved by another level of indirection)[17],即,以 Module Linking 爲中心,結合 interface types 等相關特性,爲 WebAssembly 核心規範增加一個間接中間層 "Module Linking Layer"[18],如下圖 5 所示。由於 Component Model 還處於非常初期的 Feature Proposal 階段,本文僅僅作爲一個引子,相關的詳細設計和演變還需要讀者時刻關注和研究。

圖 5. WebAssembly 分層組件化概念模型示意圖

  1. 總結

本文基於 JavaScript 的模塊及其鏈接方式,探討了利用 WebAssembly 模塊的導入導出機制實現動態鏈接的方式及存在的問題,並進一步結合最新提案,介紹其模塊化和動態鏈接的關鍵設計和實現,以及當前面臨的挑戰和未來的發展趨勢。

本文涉及到的示例代碼,原型實現等相關資料,請訪問參考文獻 module-linking[11] 所在的 webassembly_tech 資源庫進行查閱和獲取。

  1. 參考文獻

[1]. Separation of concerns : https://en.wikipedia.org/wiki/Separation_of_concerns
[2]. CommonJS: https://en.wikipedia.org/wiki/CommonJS
[3]. AMD: https://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition
[4]. Universal Module Definition:https://github.com/umdjs/umd
[5]. ECMAScript modules: https://nodejs.org/api/esm.html
[6]. node/loader.js: https://github.com/nodejs/node/blob/8822f40b2d48841c6d4fb4c04266a5703bdf33e9/lib/internal/modules/cjs/loader.js
[7]. ES modules: A cartoon deep-dive: https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
[8]. asm.js Working Draft : http://asmjs.org/spec/latest/
[9]. asm.js: closing the gap between JavaScript and native: https://2ality.com/2013/02/asm-js.html
[10]. WebAssembly Spec: https://webassembly.github.io/spec/core/syntax/index.html
[11]. webassembly_tech: https://github.com/yaozhongxiao/webassembly_tech/tree/master/samples/module-linking
[12]. multi-module : https://github.com/bytecodealliance/wasm-micro-runtime
[13]. wasm-bindgen: https://rustwasm.github.io/wasm-bindgen/
[14]. wasm-loader.js: https://github.com/yaozhongxiao/webassembly_tech/tree/master/samples/module-linking/lib/wasm-loader.js
[15]. ECMAScript module integration (Phase 2 - Proposed Spec Text Available (CG + WG)): https://github.com/WebAssembly/esm-integration/tree/main/proposals/esm-integration
[16]. WebAssembly ES module integration: https://www.youtube.com/watch?v=qR_b5gajwug
[17]. Module Linking(Inactive Proposals): https://github.com/WebAssembly/module-linking
[18]. Component Model (Phase 1 - Feature Proposal (CG)): https://github.com/WebAssembly/component-model/blob/main/design/high-level/Goals.md
[19]. All problems in computer science can be solved by another level of indirection: https://en.wikipedia.org/wiki/David_Wheeler_(computer_scientist)
[20]. Scoping and Layering the Module Linking and Interface Types proposals:
https://github.com/yaozhongxiao/webassembly_tech/tree/master/proposal
https://docs.google.com/presentation/d/1PSC3Q5oFsJEaYyV5lNJvVgh-SNxhySWUqZ6puyojMi8/edit#slide=id.p

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/MPBwFuL2CYFVXIowoB542A