WebAssembly 模块化与动态链接

字节前端 ByteFE 发表于 6月以前  | 总阅读数:455 次

1. 前言

模块化编程(modular programming)是一种软件设计模式,它将软件分解为若干独立的、可替换的、具有预定功能的模块,每个模块实现一个功能,各模块通过接口(输入输出部分)组合在一起形成最终程序。当下流行的JavaScript、Python、Rust、Java 等语言都有具有模块(包)管理,甚至 C++20 开始都引入了模块化系统。

模块化编程从 1980 年代开始广泛传播,是 SoC (Separation of concerns)[1] 原则的理想目标,主要有如下特点:

  • 易设计:较大的复杂问题分解为若干较小的简单问题,使我们可以从抽象的模块功能角度而非具体的实现角度去理解软件系统,从而整个系统的结构非常清晰、容易理解,设计人员在设计之初可以更加关注系统的顶层逻辑而非底层细节。
  • 易实现:模块化设计适合团队开发,因为每个团队成员不需要了解系统全貌,只需关注所分配的小任务。另外团队可以灵活地增加人手,新人只需直接接手某个模块,不会影响系统其他模块的开发。
  • 易测试:每个模块不但可以独立开发,也可以独立测试,最后组装时再进行联合测试。
  • 易维护:如果需要修改系统或者扩展系统功能,只需针对特定模块进行修改或者添加新模块。
  • 可重用:模块的代码通过模块的导出、引入的方式可以不加修改地用于其他程序的开发,避免代码的拷贝及重复开发和维护多套代码
  • 低耦合、高隔离性: 每个模块都有各自的作用域空间,从而避免变量污染、命名冲突;将各模块中变量和函数的依赖问题提升到了模块间的依赖,大大简化了依赖管理。

模块可以理解为是一个实现特定功能的独立且通用的代码单元,意在解决代码拆分、作用域隔离、功能依赖耦合等问题,提升代码的可维护性。然而,由于模块化往往基于代码的分离而构建独立出代码单元,单一的模块往往无法作为一个单独运行单元,因此在运行过程中不可避免的需要引入动态链接的机制。

WebAssembly 作为可移植且兼容 Web 的全新格式,基于模块化的 WebAssembly 动态链接机制,可以将WebAssembly 应用程序的核心逻辑分离出来,可以更容易地共享,从而消除重复逻辑;功能独立的小模块具有更高的分发、下载、加载效率,并且可以做到按需加载,从未使用的模块永远不会被下载;此外,WebAssembly 可链接模块还可以进行并行流式编译、进行模块缓存、运行期动态链接,从而进一步提升加载和启动效率。

接下来,本文会从 WebAssembly 的模块化演进入手,介绍其模块化和动态链接的关键设计和实现,以及当前面临的挑战和未来的发展趋势。

2. 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]。

// eslint-disable-next-line func-style
let wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
};

const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});',
];

在上面的源代码中,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 代码和数据的访问:

  • "stdlib": 一个标准库对象,提供对 JavaScript 标准库的有限子集的访问。 asm.js 在其标准中严格规定了一些可以在模块定义中使用的 JavaScript 标准库函数,如果在定义 asm.js 模块方法时使用到这些标准库函数,JavaScript 引擎在编译代码时会自动使用这些函数所对应的静态编译的 AOT 版本代码,从而在最大程度上提高代码的执行效率。
  • "foreign": 外部函数接口 (FFI),提供对自定义外部 JavaScript 函数的访问。 asm.js 本身无法处理的各类场景都可以放到模块外部的函数中进行统一处理,然后再通过该参数在模块内部进行引用。
  • "heap": heap 堆缓冲区,提供一个 ArrayBuffer 作为 asm.js 堆。 由于标准的 asm.js 模块在被 JavaScript 引擎解析和执行之前便会经过 AOT 处理被编译成静态代码,这部分静态代码有着固定的可用内存段。模块运行时对变量的内存分配也统一在这个固定大小的堆内存上进行 ,因此,所有 asm.js 模块外部的数据都需要存放到该堆内存中才能够在 asm.js 模块内部使用。

asm.js 模块的参数使得模块可以调用外部 JavaScript,并与外部 JavaScript 共享其 ArrayBuffer 堆缓冲区;相反,从模块返回的导出对象允许外部 JavaScript 调用 asm.js;asm.js 对象的交互和绑定过程我们称之为 asm.js 模块链接。asm.js 模块化和链接的底层机制基本沿袭了 JavaScript 模块和链接的设计,并在一定程度上被 WebAssembly 继承并发展,虽然 asm.js 标准自 2014 年发布至今已经鲜有人关注了,但可以为我们深入理解 WebAssembly 模块及链接机制提供帮助。

接下来,就让我们一起对 WebAssembly 的模块化和链接机制进行分析和理解。

3. 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] 仓库,读者可以方便的按照仓库中的指引快速重建和运行本节中所有的示例程序,

4. WebAssembly 动态链接发展趋势

使用动态链接技术,不仅能够减小 WebAssembly 应用的二进制文件大小;以此同时,将公共代码提取出来也会在一定程度上减少公共资源在内存中副本数量,从而节省宝贵的内存资源。WebAssembly 模块化和动态链接机制在 JavaScript 环境下可以通过标准的 JavaScript API 来实现大部分需求,但这种机制是建立在宿主和语言的特定约定和依赖的的基础上,对不同的宿主和语言环境没有普适性和标准化;例如,对于 Rust 环境需要采用自身语言相关的 wasm-bindgen[13] 来实现,而无法与 JavaScript 的模块化机制兼容和共享。此外,即使 JavaScript 可以在一定程度上实现 WebAssembly 的动态绑定,但 WebAssembly 仍然无法融入 JavaScript 模块系统中,导致中间有很多的交互操作和成本。CG 和 WG 有多个提案来尝试解决 WebAssembly 模块的实例化和动态链行为;接下来,我们尝试对提案的原理做简要的介绍,以便更好地了解和把握 WebAssembly 模块化和动态链接的趋势,从而最大程度的利用具有高移植性、独立于宿主和语言的可组合 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"

不仅如此,WebAssembly Module 也可以通过 import 来直接使用 JavaScript Module 的导出对象,从而不再依赖 "协调者" 来完成 WebAssembly 与 JS Module 的链接过程,如下列代码所示。

/* 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 Section: 在 WebAssembly 中定义嵌套 Module。
  • Instance Section:在 WebAssembly 中声明和定义本地 Instance 实例,从而在动态链接中就可以不依赖于宿主中 WebAssembly Instance 的导入对象。
  • Alias Section: 为 Import对象 和 Export 对象创建别名,将嵌套实例的导出或父模块中的项目引入本地索引空间。

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 模块化和动态链接的目标,概括起来主要有如下几个方面:

  • 基于 WebAssembly 核心模块,定义一种可独立编译构建的可移植的、加载和运行时高效的二进制格式组件,以实现可移植的跨语言组合。
  • 支持可移植的、可虚拟化的(动态链接和多态)、可静态分析的、与语言无关的接口的定义。
  • 保持并增强 WebAssembly 的独特价值。
  • 语言中立:避免组件模型只偏向一种语言或语言家族,从而避免依赖特定宿主和语言的约定。
  • 可嵌入性:设计组件以嵌入到各种宿主执行环境中,包括浏览器、服务器、中介、小型设备和数据密集型系统。
  • 可优化性:最大化 Ahead-of-Time 编译器可用的静态信息,以最小化实例化和启动的成本。
  • 形式语义:在与核心 WebAssembly 相同的语义框架内定义组件模型。
  • Web 平台集成:通过扩展现有的 JS API、Web API 和 ESM 与 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 分层组件化概念模型示意图

5. 总结

本文基于 JavaScript 的模块及其链接方式,探讨了利用 WebAssembly 模块的导入导出机制实现动态链接的方式及存在的问题,并进一步结合最新提案,介绍其模块化和动态链接的关键设计和实现,以及当前面临的挑战和未来的发展趋势。

本文涉及到的示例代码,原型实现等相关资料,请访问参考文献 module-linking[11] 所在的 webassembly_tech 资源库进行查阅和获取。

6. 参考文献

  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/DavidWheeler(computer_scientist)
  20. Scoping and Layering the Module Linking and Interface Types proposals: https://github.com/yaozhongxiao/webassembly_tech/tree/master/proposalhttps://docs.google.com/presentation/d/1PSC3Q5oFsJEaYyV5lNJvVgh-SNxhySWUqZ6puyojMi8/edit#slide=id.p

本文由微信公众号字节前端 ByteFE原创,哈喽比特收录。
文章来源:https://mp.weixin.qq.com/s/-FbZzEmJbVzTNkR1PgZxMA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:8月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:8月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:8月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 相关文章
为Electron程序添加运行时日志 4年以前  |  19565次阅读
Node.js下通过配置host访问URL 5年以前  |  5622次阅读
用 esbuild 让你的构建压缩性能翻倍 3年以前  |  5351次阅读
 目录