From 33058f2b292b3a581333bdfb21b8f671898c5060 Mon Sep 17 00:00:00 2001 From: Peter Bengtsson Date: Tue, 8 Dec 2020 14:40:17 -0500 Subject: initial commit --- files/zh-cn/webassembly/c_to_wasm/index.html | 252 ++++++++++ files/zh-cn/webassembly/caching_modules/index.html | 156 ++++++ files/zh-cn/webassembly/concepts/index.html | 169 +++++++ .../webassembly/exported_functions/index.html | 76 +++ files/zh-cn/webassembly/index.html | 113 +++++ .../webassembly/loading_and_running/index.html | 131 +++++ files/zh-cn/webassembly/rust_to_wasm/index.html | 321 ++++++++++++ .../webassembly/text_format_to_wasm/index.html | 72 +++ .../understanding_the_text_format/index.html | 540 +++++++++++++++++++++ .../using_the_javascript_api/index.html | 281 +++++++++++ 10 files changed, 2111 insertions(+) create mode 100644 files/zh-cn/webassembly/c_to_wasm/index.html create mode 100644 files/zh-cn/webassembly/caching_modules/index.html create mode 100644 files/zh-cn/webassembly/concepts/index.html create mode 100644 files/zh-cn/webassembly/exported_functions/index.html create mode 100644 files/zh-cn/webassembly/index.html create mode 100644 files/zh-cn/webassembly/loading_and_running/index.html create mode 100644 files/zh-cn/webassembly/rust_to_wasm/index.html create mode 100644 files/zh-cn/webassembly/text_format_to_wasm/index.html create mode 100644 files/zh-cn/webassembly/understanding_the_text_format/index.html create mode 100644 files/zh-cn/webassembly/using_the_javascript_api/index.html (limited to 'files/zh-cn/webassembly') diff --git a/files/zh-cn/webassembly/c_to_wasm/index.html b/files/zh-cn/webassembly/c_to_wasm/index.html new file mode 100644 index 0000000000..afcdbbc3e1 --- /dev/null +++ b/files/zh-cn/webassembly/c_to_wasm/index.html @@ -0,0 +1,252 @@ +--- +title: 编译 C/C++ 为 WebAssembly +slug: WebAssembly/C_to_wasm +tags: + - C + - C++ + - Emscripten + - WebAssembly + - wasm + - 编译 +translation_of: WebAssembly/C_to_wasm +--- +
{{WebAssemblySidebar}}
+ +

当你在用C/C++之类的语言编写模块时,你可以使用Emscripten来将它编译到WebAssembly。让我们来看看它是如何工作的。

+ +

Emscripten 环境安装

+ +

首先,让我们来配置所需要的开发环境。

+ +

所需条件

+ +

你需要将下列工具安装在您的电脑上,首先让我们确认下都有哪些。

+ + + +
+

注意: 在Windows下您可能需要pywin32,为了降低安装pywin32可能遇到的错误,请使用管理员权限在cmd内运行安装程序。

+
+ +

编译Emscripten

+ +

接下来,您需要通过源码自己编译一个Emscripten。运行下列命令来自动化地使用Emscripten SDK。(在你想保存Emscripten的文件夹下运行)。

+ +
git clone https://github.com/juj/emsdk.git
+cd emsdk
+
+# 在 Linux 或者 Mac OS X 上
+./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
+./emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit
+# 如果在你的macos上获得以下错误
+Error: No tool or SDK found by name 'sdk-incoming-64bit'
+# 请执行
+./emsdk install latest
+# 按照提示配置环境变量即可
+./emsdk activate latest
+
+
+# 在 Windows 上
+emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
+emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit
+
+# 注意:Windows 版本的 Visual Studio 2017 已经被支持,但需要在 emsdk install 需要追加 --vs2017 参数。
+
+ +

 安装过程可以会花上一点时间,是时候去休息一下。安装程序会设置所有Emscripten运行所需要的环境变量。

+ +
+

注意: --global标识会让PATH变量在全局被设置,所以接下来所打开的终端或者命令行窗口都会被设置。如果您仅仅想让Emscripten在当前窗口生效,就删掉这个标识。

+
+ +
+

注意: 每当您想要使用Emscripten时,尝试从远程更新最新的emscripten代码是个很好的习惯(运行 git pull)。如果有更新,重新执行 install 和 activate 命令。这样就可以确保您使用的Emscripten一直保持最新。

+
+ +

现在让我们进入emsdk文件夹,输入以下命令来让你进入接下来的流程,编译一个样例C程序到asm.js或者wasm。

+ +
# on Linux or Mac OS X
+source ./emsdk_env.sh
+
+# on Windows
+emsdk_env.bat
+
+ +

编译样例代码

+ +

现在环境配置完毕,让我们看看如何使用它把C代码编译到Emscripten。当使用Emscripten来编译的时候有很多种不同的选择,我们介绍其中主要的2种:

+ + + +

让我们一个一个看看。

+ +

生成 HTML 和 JavaScript

+ +

我们先来看一个最简单的例子,通过这个,你可以使用Emscripten来将任何代码生成到WebAssembly,然后在浏览器上运行。

+ +
    +
  1. 首先我们需要编译一段样例代码。将下方的C代码复制一份然后命名为hello.c保存在一个新的文件夹内。 +
    #include <stdio.h>
    +
    +int main(int argc, char ** argv) {
    +  printf("Hello World\n");
    +}
    +
  2. +
  3. 现在,转到一个已经配置过Emscripten编译环境的终端窗口中,进入刚刚保存hello.c文件的文件夹中,然后运行下列命令: +
    emcc hello.c -s WASM=1 -o hello.html
    +
  4. +
+ +

下面列出了我们命令中选项的细节:

+ + + +

这个时候在您的源码文件夹应该有下列文件:

+ + + +

运行你的例子

+ +

现在使用一个支持 WebAssembly 的浏览器,加载生成的 hello.html 。

+ +
+

提示 :Firefox 52+ 和 Chrome 57+ 和最新版本的 Opera 已经默认启用,你也可以在 Firefox 47+ 中通过在 about:config 页面启用 javascript.options.wasm 字段获得支持,Chrome 51+ 和 Opera 38+ 可以在 chrome://flags 页面启用 Experimental WebAssembly 选项以支持 WebAssembly

+
+ +

如果一切顺利,你应该可以在页面上的 Emscripten 控制台浏览器控制台 中看到 "Hello World" 的输出。

+ +

恭喜!你已经成功将 C 代码编译成 JavaScript 并且在浏览器中执行了!

+ +

使用自定义HTML模板

+ +

有些时候你可能想要使用一个自定义的 HTML 模板。让我们看看怎么实现。

+ +
    +
  1. +

    首先,在一个新文件夹中保存以下 C 代码到 hello2.c 中:

    + +
    #include <stdio.h>
    +
    +int main(int argc, char ** argv) {
    +    printf("Hello World\n");
    +}
    +
  2. +
  3. +

    在  emsdk 中搜索一个叫做 shell_minimal.html 的文件,然后复制它到刚刚创建的目录下的 html_template 文件夹。

    + +
    mkdir html_template
    +cp ~/emsdk/emscripten/1.38.15/src/shell_minimal.html html_template
    +
  4. +
  5. +

    现在使用你的Emscripten编译器环境的终端窗口进入你的新目录, 然后运行下面的命令:

    + +
    emcc -o hello2.html hello2.c -O3 -s WASM=1 --shell-file html_template/shell_minimal.html
    + +

    这次使用的选项略有不同:

    + +
      +
    • 我们使用了 -o hello2.html ,这意味编译器将仍然输出 js 胶水代码 和 html 文件。
    • +
    • 我们还使用了 --shell-file html_template/shell_minimal.html ,这指定了您要运行的例子使用 HTML 页面模板。
    • +
    +
  6. +
  7. +

    下面让我们来运行这个例子。上面的命令已经生成了 hello2.html,内容和我们使用的模板非常相像,只不过多加了一些 js 胶水和加载wasm文件的代码。 在浏览器中打开它,你会看到与上一个例子相同的输出。

    +
  8. +
+ +
+

注意:通过用.js取代.htm(l)作为文件后缀名,你就可以得到只有JavaScript的输出文件,而不再是完整的HTML文件。例如:emcc -o hello2.js hello2.c -O3 -s WASM=1. 你可以完全从零开始创建你自己的HTML文件。尽管如此,不推荐这样做。因为Emscripten需要大量的JavaScript“胶水”代码从而能够 处理内存分配、内存泄漏以及大量的其他问题。这些问题都已经在提供的模板中得到了处理。使用模板要比自己编写模板要容易得多。不过,当对模板所做的事情越来越熟悉的时候,你就能够按照自己的需要创建定制化的模板了。

+
+ +

调用一个定义在C中的自定义方法

+ +

如果需要调用一个在 C 语言自定义的函数,你可以使用  Emscripten 中的 ccall() 函数,以及 EMSCRIPTEN_KEEPALIVE 声明 (将你的函数添加到导出函数列表中(详见 Why do functions in my C/C++ source code vanish when I compile to JavaScript, and/or I get No functions to process?))。

+ +

接下来让我们看看这是怎么实现的。

+ +
    +
  1. +

    首先,将以下代码在新目录中保存为 hello3.c

    + +
    #include <stdio.h>
    +#include <emscripten/emscripten.h>
    +
    +int main(int argc, char ** argv) {
    +    printf("Hello World\n");
    +}
    +
    +#ifdef __cplusplus
    +extern "C" {
    +#endif
    +
    +int EMSCRIPTEN_KEEPALIVE myFunction(int argc, char ** argv) {
    +  printf("我的函数已被调用\n");
    +}
    +
    +#ifdef __cplusplus
    +}
    +#endif
    + +

    默认情况下,Emscripten 生成的代码只会调用 main() 函数,其它的函数将被视为无用代码。在一个函数名之前添加 EMSCRIPTEN_KEEPALIVE 能够防止这样的事情发生。你需要导入 emscripten.h 库来使用 EMSCRIPTEN_KEEPALIVE

    + +
    +

    注意: 为了保证万一你想在 C++ 代码中引用这些代码时代码可以正常工作,我们添加了 #ifdef 代码块。由于 C 与 C++ 中名字修饰规则的差异,添加的代码块有可能产生问题,但目前我们设置了这一额外的代码块以保证你使用 C++ 时,这些代码会被视为外部 C 语言函数。

    +
    +
  2. +
  3. +

    为了方便起见,现在将 html_template/shell_minimal.html 也添加到这一目录(但在实际开发环境中你肯定需要将其放到某一特定位置)。

    +
  4. +
  5. +

    运行以下命令编译:(注意由于使用ccall函数,需要添加指定参数)

    + +
    emcc -o hello3.html hello3.c -O3 -s WASM=1 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']" --shell-file html_template/shell_minimal.html
    +
  6. +
  7. +

    如果你在浏览器中在此加载实例,你将看到和之前相同的结果。

    +
  8. +
  9. +

    现在我们需要运行新的 myFunction() JavaScript 函数。首先,按照以下实例添加一个 {{htmlelement("button")}} ,就在 <script type='text/javascript'> 开头标签之前。

    + +
    <button class="mybutton">运行我的函数</button>
    +
  10. +
  11. +

    现在在最后一个 {{htmlelement("script")}} 元素 (就在 </script> 关闭标签之前)中添加以下代码:

    + +
    document.querySelector('.mybutton').addEventListener('click', function(){
    +  alert('检查控制台');
    +  var result = Module.ccall('myFunction', // name of C function
    +                             null, // return type
    +                             null, // argument types
    +                             null); // arguments
    +});
    +
  12. +
+ +

以上就是如何使用 ccall() 调用导出的函数的方式。

+ +

另请参见

+ + diff --git a/files/zh-cn/webassembly/caching_modules/index.html b/files/zh-cn/webassembly/caching_modules/index.html new file mode 100644 index 0000000000..0a74fcb5d2 --- /dev/null +++ b/files/zh-cn/webassembly/caching_modules/index.html @@ -0,0 +1,156 @@ +--- +title: 缓存已编译的WebAssembly模块 +slug: WebAssembly/Caching_modules +tags: + - IndexedDB + - JavaScript + - WebAssembly + - wasm + - 模块 + - 缓存 + - 编译 +translation_of: WebAssembly/Caching_modules +--- +
{{WebAssemblySidebar}}
+ +

对于提高应用的性能来说,缓存是很有用的——我们可以在客户端存储已编译的WebAssembly模块,从而可以避免每次都下载和编译它们。本文解释了这方面的最佳实践。

+ +

使用IndexedDB实现缓存

+ +

IndexedDB是一个事务型数据库系统,它允许你在客户端存储和获取结构化数据。它适合在本地保存包含应用程序状态的资源,包括文本、二进制大对象以及其他任何可以克隆的对象。

+ +

这包括已编译的wasm模块({{jsxref("WebAssembly.Module")}} JavaScript对象)。

+ +

建立缓存库

+ +

因为IndexedDB在一定程度上是老式风格的API,所以,我们想提供一个库函数以便加快写缓存代码的速度并使它能够更好的与当今更现代的API配合。

+ +

在我们的wasm-utils.js脚本库中,你会发现instantiateCachedURL()——该函数使用给定版本的dbVersion获取给定url的wasm模块,使用给定的importObject实例化,并且返回一个将会解析成最终的wasm实例的promise。而且,它会创建一个用来把已编译的wasm模块缓存起来的数据库,尝试在数据库中存储新的模块,并且从数据库中获取之前缓存的模块,从而使你免于再次下载它们。

+ +
+

: 整个网站的wasm缓存(不只是给定的URL)是通过传入到函数中的dbVersion进行版本控制的。如果wasm模块代码更新了或者它的URL发生了变化,你需要更新dbVersion。对于instantiateCachedURL()的任何后续调用将会清除掉全部的缓存,从而使你避免使用过时的模块。

+
+ +

该函数从定义一些必要的常量开始:

+ +
function instantiateCachedURL(dbVersion, url, importObject) {
+  const dbName = 'wasm-cache';
+  const storeName = 'wasm-cache';
+ +

建立数据库

+ +

在instantiateCachedURL()中的第一个辅助函数openDatabase(),创建一个存储wasm模块的对象存储空间,以及在dbVersion更新后清除数据库;它返回一个解析成新的数据库的promise。

+ +
  function openDatabase() {
+    return new Promise((resolve, reject) => {
+      var request = indexedDB.open(dbName, dbVersion);
+      request.onerror = reject.bind(null, 'Error opening wasm cache database');
+      request.onsuccess = () => { resolve(request.result) };
+      request.onupgradeneeded = event => {
+        var db = request.result;
+        if (db.objectStoreNames.contains(storeName)) {
+            console.log(`Clearing out version ${event.oldVersion} wasm cache`);
+            db.deleteObjectStore(storeName);
+        }
+        console.log(`Creating version ${event.newVersion} wasm cache`);
+        db.createObjectStore(storeName)
+      };
+    });
+  }
+ +

在数据库中查找模块

+ +

我们的下一个函数lookupInDatabase(),提供了一个简单的基于promise的操作,用来在我们之前创建的对象存储空间中查找给定的url。如果成功,它可以解析出存储的已编译模块;如果失败,它会给出一个错误。

+ +
  function lookupInDatabase(db) {
+    return new Promise((resolve, reject) => {
+      var store = db.transaction([storeName]).objectStore(storeName);
+      var request = store.get(url);
+      request.onerror = reject.bind(null, `Error getting wasm module ${url}`);
+      request.onsuccess = event => {
+        if (request.result)
+          resolve(request.result);
+        else
+          reject(`Module ${url} was not found in wasm cache`);
+      }
+    });
+  }
+ +

存储和实例化模块

+ +

接下来,我们定义了一个函数storeInDatabase(),它可以触发一个异步操作,从而在给定的数据库中存储给定的wasm模块。

+ +
  function storeInDatabase(db, module) {
+    var store = db.transaction([storeName], 'readwrite').objectStore(storeName);
+    var request = store.put(module, url);
+    request.onerror = err => { console.log(`Failed to store in wasm cache: ${err}`) };
+    request.onsuccess = err => { console.log(`Successfully stored ${url} in wasm cache`) };
+  }
+ +

最后,我们定义一个辅助函数——fetchAndInstantiate(),它从给定的url获取数据,将其编译成一个模块,并且使用给定的导入对象实例化该模块。

+ +
  function fetchAndInstantiate() {
+    return fetch(url).then(response =>
+      response.arrayBuffer()
+    ).then(buffer =>
+      WebAssembly.instantiate(buffer, importObject)
+    )
+  }
+ +

使用辅助函数

+ +

使用这些定义好的基于Promise的辅助函数,我们现在可以表达一个IndexedDB缓存查找的核心逻辑了。首先,我们通过尝试打开一个数据库,然后,查看在给定的数据库db中是否存在与url相对应的模块:

+ +
  return openDatabase().then(db => {
+    return lookupInDatabase(db).then(module => {
+ +

如果找到了模块,那么,使用给定的导入对象对其进行实例化:

+ +
      console.log(`Found ${url} in wasm cache`);
+      return WebAssembly.instantiate(module, importObject);
+    },
+ +

如果没有找到,那么,我们从零开始编译它,然后,使用给定的url作为键,将已编译的模块存储到数据库中,方便下次使用。

+ +
    errMsg => {
+      console.log(errMsg);
+      return fetchAndInstantiate().then(results => {
+        storeInDatabase(db, results.module);
+        return results.instance;
+      });
+    })
+  },
+ +
+

WebAssembly.instantiate()返回一个模块实例为的就是这种用处:模块表示已经编译的代码,并且可以在IndexedDB中存取或者通过postMessage()在Workers之间共享;实例是具有状态的,并且包含了可以调用的JavaScript函数,因此不能够被存储或者共享。

+
+ +

如果打开数据库失败(比如由于权限或者空间限制),我们改用获取和编译模块的方式,并且不再尝试保存结果(因为没有数据库可以保存它们)。

+ +
  errMsg => {
+    console.log(errMsg);
+    return fetchAndInstantiate().then(results =>
+      results.instance
+    );
+  });
+}
+ +

缓存wasm模块

+ +

有了上面定义的库函数,取得一个wasm模块实例,并且使用它的导出特性(同时在后台处理缓存)只需要使用下面的参数进行调用即可:

+ + + +
const wasmCacheVersion = 1;
+
+instantiateCachedURL(wasmCacheVersion, 'test.wasm').then(instance =>
+  console.log("Instance says the answer is: " + instance.exports.answer())
+).catch(err =>
+  console.error("Failure to instantiate: " + err)
+);
+ +

你可以在GitHub上找到这个例子的源代码 indexeddb-cache.html (或者实时运行)。

diff --git a/files/zh-cn/webassembly/concepts/index.html b/files/zh-cn/webassembly/concepts/index.html new file mode 100644 index 0000000000..baf2881e6d --- /dev/null +++ b/files/zh-cn/webassembly/concepts/index.html @@ -0,0 +1,169 @@ +--- +title: WebAssembly概念 +slug: WebAssembly/Concepts +tags: + - C + - C++ + - Emscripten + - JavaScript + - WebAssembly + - 文本格式 + - 概念 + - 网络平台 +translation_of: WebAssembly/Concepts +--- +

{{WebAssemblySidebar}}

+ +

本文解释了WebAssembly如何工作的概念,包括它的目标、它解决的问题以及它是如何在网络浏览器的渲染引擎中运行的。

+ +

WebAssembly是什么?

+ +

WebAssembly是一种运行在现代网络浏览器中的新型代码,并且提供新的性能特性和效果。它设计的目的不是为了手写代码而是为诸如C、C++和Rust等低级源语言提供一个高效的编译目标。

+ +

对于网络平台而言,这具有巨大的意义——这为客户端app提供了一种在网络平台以接近本地速度的方式运行多种语言编写的代码的方式;在这之前,客户端app是不可能做到的。

+ +

而且,你在不知道如何编写WebAssembly代码的情况下就可以使用它。WebAssembly的模块可以被导入的到一个网络app(或Node.js)中,并且暴露出供JavaScript使用的WebAssembly函数。JavaScript框架不但可以使用WebAssembly获得巨大性能优势和新特性,而且还能使得各种功能保持对网络开发者的易用性。

+ +

WebAssembly的目标

+ +

作为 W3C WebAssembly Community Group中的一项开放标准,WebAssembly是为下列目标而生的:

+ + + +
+

注:WebAssembly也用在网络和JavaScript环境之外(参考非网络嵌入)。

+
+ +

WebAssembly如何适应网络平台?

+ +

网络平台可以被想象成拥有两个部分:

+ + + +

从历史角度讲,虚拟机过去只能加载JavaScript。这对我们而言足够了,因为JavaScript足够强大从而能够解决人们在当今网络上遇到的绝大部分问题。尽管如此,当试图把JavaScript应用到诸如3D游戏、虚拟现实、增强现实、计算机视觉、图像/视频编辑以及大量的要求原生性能的其他领域的时候,我们遇到了性能问题(参考 WebAssembly 使用案例 获取更多细节)。

+ +

而且,下载、解析以及编译巨大的JavaScript应用程序的成本是过高的。移动平台和其他资源受限平台进一步放大了这些性能瓶颈。

+ +

WebAssembly是一门不同于JavaScript的语言,但是,它不是用来取代JavaScript的。相反,它被设计为和JavaScript一起协同工作,从而使得网络开发者能够利用两种语言的优势:

+ + + +

随着WebAssembly出现在了浏览器中,我们前面提到的虚拟机将会加载和运行两种类型的代码——JavaScript和WebAssembly。

+ +

不同类型的代码能够按照需要进行相互调用——WebAssembly的JavaScript API使用能够被正常调用的JavaScript函数封装了导出的WebAssembly代码,并且WebAssembly代码能够导入和同步调用常规的JavaScript函数。事实上,WebAssembly代码的基本单元被称作一个模块,并且WebAssembly的模块在很多方面都和ES2015的模块是等价的。

+ +

WebAssembly关键概念

+ +

为了理解WebAssembly如何在浏览器中运行,需要了解几个关键概念。所有这些概念都是一一映射到了WebAssembly的JavaScript API中。

+ + + +

JavaScript API为开发者提供了创建模块、内存、表格和实例的能力。给定一个WebAssembly实例,JavaScript代码能够调用普通JavaScript函数暴露出来的导出代码。通过把JavaScript函数导入到WebAssembly实例中,任意的JavaScript函数都能被WebAssembly代码同步调用。

+ +

因为JavaScript能够完全控制WebAssembly代码如何下载、编译运行,所以,JavaScript开发者甚至可以把WebAssembly想象成一个高效地生成高性能函数的JavaScript特性。

+ +

将来,WebAssembly模块将会像ES2015模块那样加载(使用<script type='module'>),这也就意味着JavaScript代码能够像轻松地使用一个ES2015模块那样来获取、编译和导入一个WebAssembly模块。

+ +

如何在我的app中使用WebAssembly?

+ +

上面我们讨论了WebAssembly向网络平台增加的基本要素:

+ + + +

现在让我们讨论如何在实践中使用这些基本要素。

+ +

WebAssembly生态系统处在初始阶段;更多的工具会毫无疑问得不断出现。当然,有两个主要的着手点:

+ + + +

让我们讨论这几项:

+ +

从C/C++移植

+ +

 

+ +

创建WASM代码的众多选项中有两个是在线WASM汇编程序或Emscripten。有许多在线WASM汇编程序可供选择,例如:

+ + + +

对于那些想知道从哪里开始的人来说,这些是很好的资源,但是他们缺少一些Emscripten的工具和优化。

+ +

Emscripten工具能够将一段C/C++代码,编译出:

+ + + +

+ +

简而言之,工作流程如下所示:

+ +
    +
  1. Emscripten首先把C/C++提供给clang+LLVM——一个成熟的开源C/C++编译器工具链,比如,在OSX上是XCode的一部分。
  2. +
  3. Emscripten将clang+LLVM编译的结果转换为一个.wasm二进制文件。
  4. +
  5. 就自身而言,WebAssembly当前不能直接的存取DOM;它只能调用JavaScript,并且只能传入整形和浮点型的原始数据类型作为参数。这就是说,为了使用任何Web API,WebAssembly需要调用到JavaScript,然后由JavaScript调用Web API。因此,Emscripten创建了HTML和JavaScript胶水代码以便完成这些功能。
  6. +
+ +
+

注意:计划将来允许WebAssembly直接调用Web API

+
+ +

JavaScript胶水代码并不是像你想象的那么简单。首先,Emscripten实现了流行的C/C++库,比如, SDLOpenGLOpenAL以及部分POSIX。这些库以Web API的形式实现,并且每个库需要一个JavaScript胶水代码来连接WebAssembly和低层的Web API。

+ +

因此,部分胶水代码实现了C/C++代码使用的库的功能。而且,胶水代码还包括调用前面提到的WebAssembly的JavaScript API来获取、加载和运行.wasm文件的逻辑。

+ +

生成的HTML文档加载JavaScript胶水文件,并且将stdout输出到{{htmlelement("textarea")}}。如果应用程序使用了OpenGL,HTML文档还会包含一个用来渲染目标的{{htmlelement("canvas")}}标签。

+ +

修改Emscripten的输出文件并将其转换为需要的web app是很容易的。

+ +

你可以在emscripten.org找到关于Emscripten的完整文档以及在Compiling from C/C++ to WebAssembly找到一个实现工具链并交叉编译你自己的C/C++应用程序为wasm的指南。

+ +

直接编写WebAssembly代码

+ +

你想构建自己的编译器,或者你自己的工具,或者创建一个能够在运行时生成WebAssembly代码的JavaScript库?

+ +

就像真实的汇编语言一样,WebAssembly的二进制格式也有文本表示——两者之间1:1对应。你可以手工书写或者生成这种格式然后使用这些工具(WebAssemby text-to-binary tools)中的任何一个把它转换为二进制格式。

+ +

这有一份如何做这些的简单指南,参考我们的文章转换WebAssembly文本格式为wasm

+ +

总结

+ +

本文给你解释了WebAssembly是什么,它为什么如此有用,它是如何适应网络的以及你如何才能使用它。

+ +

参考

+ + diff --git a/files/zh-cn/webassembly/exported_functions/index.html b/files/zh-cn/webassembly/exported_functions/index.html new file mode 100644 index 0000000000..c19706f594 --- /dev/null +++ b/files/zh-cn/webassembly/exported_functions/index.html @@ -0,0 +1,76 @@ +--- +title: 导出的WebAssembly函数 +slug: WebAssembly/Exported_functions +tags: + - JavaScript + - WebAssembly + - wasm + - 导出 + - 导出的wasm函数 + - 导出的函数 + - 指南 +translation_of: WebAssembly/Exported_functions +--- +
{{WebAssemblySidebar}}
+ +

导出WebAssembly函数的过程,其实就是指这些函数在JavaScript中如何用表示。本文更详细的介绍它们。

+ +

导出的...什么?

+ +

导出的WebAssembly函数只是用JavaScript来表示WebAssembly函数的封装而已。当你调用它们的时候,就会有一些后台活动把参数转换为wasm能够处理的类型(例如,把JavaScript数字转换为Int32类型),参数被传递到wasm模块中的函数,函数被调用,返回值被转换并传回到JavaScript。

+ +

你可以通过两种方式来获得导出的WebAssembly函数:

+ + + +

无论哪种方式,你得到的都是底层函数的相同封装。从JavaScript的角度来看,每一个wasm函数看起来也是一个JavaScript函数——但是,它们被封装在导出的wasm函数对象实例中,并且只有有限的方式来获取它们。

+ +

一个例子

+ +

让我们看个例子从而让事情更清晰(你可以在GitHub上找到这个例子table-set.html;或者实时运行然后查看wasm文本表示):

+ +
var otherTable = new WebAssembly.Table({ element: "anyfunc", initial: 2 });
+
+fetchAndInstantiate('table.wasm').then(function(instance) {
+  var tbl = instance.exports.tbl;
+  console.log(tbl.get(0)());  // 13
+  console.log(tbl.get(1)());  // 42
+  otherTable.set(0,tbl.get(0));
+  otherTable.set(1,tbl.get(1));
+  console.log(otherTable.get(0)());
+  console.log(otherTable.get(1)());
+});
+ +

在这里,我们使用WebAssembly.Table构造函数在JavaScript中创建了一个表格(otherTable),然后使用fetchAndInstantiate()实用函数把table.wasm加载到我们的页面。

+ +

然后,我们得到了从模块中导出的函数,通过tbl.get()获取引用的函数并且把每一次的调用结果输出到控制台。接下来,我们使用set()使得otherTable表格包含了与tbl表格相同的函数。

+ +

为了证明这一点,我们从otherTable 中获取了这些引用并且也把他们的结果打印到控制台,结果是一样的。

+ +

它们确实是函数

+ +

在前面的例子中,每次Table.prototype.get()调用的返回值都是一个导出的WebAssembly函数——这正是我们一直在讨论的。

+ +

它们确实是JavaScript函数也是对WebAssembly函数的封装。如果你把上面的例子加载到支持WebAssembly的浏览器中,然后在你的控制台运行下面几行代码:

+ +
var testFunc = otherTable.get(0);
+typeof testFunc;
+ +

你得到的返回结果是function 。对于这个函数,你可以像对待其他JavaScript函数那样做你想做的任何事——call()、 bind()等等。testFunc.toString()返回一个有趣的结果:

+ +
function 0() {
+    [native code]
+}
+ +

这带给你关于封装类型特征的更多理解。

+ +

关于导出的WebAssembly函数的一些其他值得关注的特性:

+ + diff --git a/files/zh-cn/webassembly/index.html b/files/zh-cn/webassembly/index.html new file mode 100644 index 0000000000..b1975ab866 --- /dev/null +++ b/files/zh-cn/webassembly/index.html @@ -0,0 +1,113 @@ +--- +title: WebAssembly +slug: WebAssembly +tags: + - WebAssembly + - wasm +translation_of: WebAssembly +--- +
{{WebAssemblySidebar}}
+ +

WebAssembly是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如C / C ++等语言提供一个编译目标,以便它们可以在Web上运行。它也被设计为可以与JavaScript共存,允许两者一起工作。

+ +

简而言之

+ +

对于网络平台而言,WebAssembly具有巨大的意义——它提供了一条途径,以使得以各种语言编写的代码都可以以接近原生的速度在Web中运行。在这种情况下,以前无法以此方式运行的客户端软件都将可以运行在Web中。

+ +

WebAssembly被设计为可以和JavaScript一起协同工作——通过使用WebAssembly的JavaScript API,你可以把WebAssembly模块加载到一个JavaScript应用中并且在两者之间共享功能。这允许你在同一个应用中利用WebAssembly的性能和威力以及JavaScript的表达力和灵活性,即使你可能并不知道如何编写WebAssembly代码。

+ +

而且,更棒的是,这是通过W3C WebAssembly Community Group开发的一项网络标准,并得到了来自各大主要浏览器厂商的积极参与。

+ +
+
+

指南

+ +
+
WebAssembly概念
+
通过阅读WebAssembly后面的高层概念开始——也就是说,它为什么如此有用,它是如何适应网络平台的(以及其他)和如何使用它。
+
从C/C++编译为WebAssembly
+
当你使用C/C++编写了代码后,你可以使用诸如Emscripten的工具把它编译为.wasm文件。让我们看看这是如何工作的。
+
加载运行WebAssembly代码
+
本文介绍如何通过把Fetch或者XHR API与WebAssembly JavaScript API结合在一起来获取、编译和实例化.wasm文件。
+
缓存编译后的WebAssembly模块
+
就提升应用启动速度而言,在客户端缓存大的WebAssembly模块是有用的。本文解释了如何使用IndexedDB来实现这一点。
+
使用WebAssembly的JavaScript API
+
当加载了一个.wasm模块之后,你就想要使用它。在本文中,我们向你展示了如何通过WebAssembly的JavaScript API来使用WebAssembly。
+
导出的WebAssembly函数
+
导出的WebAssembly函数是WebAssembly函数的JavaScript表现形式,并且允许从JavaScript中调用WebAssembly代码。本文描述它们。
+
理解WebAssembly的文本格式
+
本文介绍了wasm的文本格式。这是在进行调试的时候浏览器开发者工具中展现出来的.wasm模块的一种低级文本表示。
+
把WebAssembly文本格式转换为wasm
+
本文提供了一个如何把编写的文本格式的WebAssembly模块转换为 wasm二进制的指南。
+
+
+ +
+

API参考

+ +
+
{{jsxref("Global_objects/WebAssembly", "WebAssembly")}}
+
本对象是所有与WebAssembly相关功能的命名空间。
+
{{jsxref("Global_objects/WebAssembly/Module", "WebAssembly.Module")}}
+
一个WebAssembly.Module对象包括了无状态的WebAssembly代码。该代码已经被浏览器编译并且能够通过Workers高效地共享缓存到IndexedDB中以及多次实例化。
+
{{jsxref("Global_objects/WebAssembly/Instance", "WebAssembly.Instance")}}
+
一个WebAssembly.Instance对象是一个有状态的、可执行的模块的实例。实例对象包含所有的能够从JavaScript调用到WebAssembly代码的导出的WebAssembly函数
+
{{jsxref("Global_objects/WebAssembly/instantiate", "WebAssembly.instantiate()")}}
+
WebAssembly.instantiate() 函数是编译和实例化WebAssembly代码的主要的API,它返回一个Module及其第一个实例。
+
{{jsxref("Global_objects/WebAssembly/Memory", "WebAssembly.Memory()")}}
+
一个WebAssembly.Memory 对象是一个可变长的{{jsxref("Global_objects/ArrayBuffer", "ArrayBuffer")}}。它拥有能够被实例存取的原始字节内存。
+
{{jsxref("Global_objects/WebAssembly/Table", "WebAssembly.Table()")}}
+
WebAssembly.Table对象是一个可变长类型数组。它存储诸如函数引用之类的不透明值并且能够被实例存取。
+
{{jsxref("WebAssembly.CompileError()")}}
+
创建一个新的WebAssembly CompileError对象。
+
{{jsxref("WebAssembly.LinkError()")}}
+
创建一个新的WebAssembly LinkError对象。
+
{{jsxref("WebAssembly.RuntimeError()")}}
+
创建一个新的WebAssembly RuntimeError对象。
+
+
+
+ +

示例

+ + + +

规范

+ + + + + + + + + + + + + + + + +
SpecificationStatusComment
{{SpecName('WebAssembly JS')}}{{Spec2('WebAssembly JS')}}Initial draft definition of the JavaScript API.
+ +

浏览器兼容性

+ +
+ + +

{{Compat("javascript.builtins.WebAssembly")}}

+
+ +

参见

+ + diff --git a/files/zh-cn/webassembly/loading_and_running/index.html b/files/zh-cn/webassembly/loading_and_running/index.html new file mode 100644 index 0000000000..334aa4a8c0 --- /dev/null +++ b/files/zh-cn/webassembly/loading_and_running/index.html @@ -0,0 +1,131 @@ +--- +title: 加载和运行WebAssembly代码 +slug: WebAssembly/Loading_and_running +tags: + - Fetch + - WebAssembly + - XMLHttpRequest + - 字节码 +translation_of: WebAssembly/Loading_and_running +--- +
{{WebAssemblySidebar}}
+ +

为了在JavaScript中使用WebAssembly,在编译/实例化之前,你首先需要把模块放入内存。比如,通过XMLHttpRequestFetch,模块将会被初始化为带类型数组;不过,将来会开发更多的方式。本文提供了一篇关于获取WebAssembly字节码的不同机制以及如何编译/实例化并运行它的参考。

+ +

这里的主题是什么?

+ +

WebAssembly还没有和<script type='module'>或ES6的import语句集成,也就是说,当前还没有内置的方式让浏览器为你获取模块。当前唯一的方式就是创建一个包含你的WebAssembly模块二进制代码的 {{domxref("ArrayBuffer")}} 并且使用{{jsxref("WebAssembly.instantiate()")}}编译它。这与new Function(string)类似,除了使用一个包含了WebAssembly源代码的数组缓存替换掉包含了JavaScript源代码的字符串。

+ +

那么,我们该如何获取这些字节并存入到一个数组缓存并编译它呢?下面进行解释。

+ +

使用Fetch

+ +

Fetch是一个用来获取网络资源的方便现代的API。

+ +

假设网络上有一个叫做simple.wasm的WebAssembly模块:

+ + + +

代码块看起来像这样:

+ +
fetch('module.wasm').then(response =>
+  response.arrayBuffer()
+).then(bytes =>
+  WebAssembly.instantiate(bytes, importObject)
+).then(results => {
+  // Do something with the compiled results!
+});
+ +

顺便说一下instantiate()重载

+ +

{{jsxref("WebAssembly.instantiate()")}}函数有两种重载形式——一种是前面展示的那样,接受待编译的字节码作为参数并且返回一个promise并且该promise可以解析为一个包含已编译的模块对象及其实例的对象。

+ +
{
+  module : Module // 新编译的WebAssembly.Module对象,
+  instance : Instance // 新的模块对象实例
+}
+ +
+

: 通常,我们只关心实例,但是,当我们想缓存模块,使用 postMessage()与另外一个worker或window共享模块或者只是创建更多的实例的时候,拥有模块对象是很有用的。

+
+ +
+

: 这二种重载形式接受一个WebAssembly.Module对象作为参数,并且返回一个包含了一个实例对象的promise。参考第二种重载示例

+
+ +

获取及实例化的实用函数

+ +

上面的代码样式可以工作,但是,每次都重新编写它们就显得啰嗦了,特别是当你想要加载多个模块的时候。为了简单起见,我们创建了一个叫做fetchAndInstantiate()的实用函数,它在后台工作并返回一个promise。你可以在wasm-utils.js中找到这个函数。它看起来像这样:

+ +
function fetchAndInstantiate(url, importObject) {
+  return fetch(url).then(response =>
+    response.arrayBuffer()
+  ).then(bytes =>
+    WebAssembly.instantiate(bytes, importObject)
+  ).then(results =>
+    results.instance
+  );
+}
+ +

把这个函数加入到HTML中,你就可以使用一行简单代码做到获取和实例化WebAssembly模块并且得到一个实例。

+ +
fetchAndInstantiate('module.wasm', importObject).then(function(instance) {
+  ...
+})
+ +
+

: 在我们的文档中,你可以看到许多这么用的例子(例如,参考index.html)——这是我们推荐的加载模块的标准方式。

+
+ +

运行你的WebAssembly代码

+ +

一旦在JavaScript中得到了可用的WebAssembly实例,你就可以开始使用那些通过 {{jsxref("WebAssembly.Instance/exports", "WebAssembly.Instance.exports")}} 属性导出的特性了。你的代码可能看起来像这样:

+ +
fetchAndInstantiate('myModule.wasm', importObject).then(function(instance) {
+  // 调用导出函数:
+  instance.exports.exported_func();
+
+  // 或者获取导出内存的缓存内容:
+  var i32 = new Uint32Array(instance.exports.memory.buffer);
+
+  // 或者获取导出表格中的元素:
+  var table = instance.exports.table;
+  console.log(table.get(0)());
+})
+ +
+

:关于从WebAssembly模块导出是如何工作的更多信息,请阅读使用WebAssembly的JavaScript API理解WebAssembly文本格式

+
+ +

使用XMLHttpRequest

+ +

XMLHttpRequest在一定程度上而言要比Fetch老旧一些,但是,仍然可以很好地被用来获取带类型数组。仍然假设我们的模块叫做simple.wasm:

+ +
    +
  1. 创建一个 {{domxref("XMLHttpRequest()")}} 实例,然后使用它的{{domxref("XMLHttpRequest.open","open()")}} 方法来开启一个请求——设置请求方法为GET并且声明我们想要获取的文件路径。
  2. +
  3. 关键之处在于使用{{domxref("XMLHttpRequest.responseType","responseType")}}属性设置响应类型为'arraybuffer'。
  4. +
  5. 接下来使用{{domxref("XMLHttpRequest.send()")}}发送请求。
  6. +
  7. 当响应已经完成下载之后,我们使用{{domxref("XMLHttpRequest.onload", "onload")}}事件处理器来调用一个函数——在这个函数中,我们从{{domxref("XMLHttpRequest.response", "response")}}属性中得到数组缓存然后就像使用Fetch那样把它传递给{{jsxref("WebAssembly.instantiate()")}} 。
  8. +
+ +

最终代码看起来像这样:

+ +
request = new XMLHttpRequest();
+request.open('GET', 'simple.wasm');
+request.responseType = 'arraybuffer';
+request.send();
+
+request.onload = function() {
+  var bytes = request.response;
+  WebAssembly.instantiate(bytes, importObject).then(results => {
+    results.instance.exports.exported_func();
+  });
+};
+ +
+

:你可以在xhr-wasm.html看到实际使用的例子。

+
diff --git a/files/zh-cn/webassembly/rust_to_wasm/index.html b/files/zh-cn/webassembly/rust_to_wasm/index.html new file mode 100644 index 0000000000..0b2a70598e --- /dev/null +++ b/files/zh-cn/webassembly/rust_to_wasm/index.html @@ -0,0 +1,321 @@ +--- +title: 编译 Rust 为 WebAssembly +slug: WebAssembly/Rust_to_wasm +tags: + - WebAssembly + - rust + - wasm + - 编译 +translation_of: WebAssembly/Rust_to_wasm +--- +
{{WebAssemblySidebar}}
+ +

如果你写了一些 Rust 代码,你可以把它编译成 WebAssembly!这份教程将带你编译 Rust 项目为 wasm 并在一个现存的 web 应用中使用它。

+ +

Rust 和 WebAssembly 用例

+ +

Rust 和 WebAssembly 有两大主要用例:

+ + + +

目前,Rust 团队正专注于第二种用例,因此我们也将着重介绍它。对于第一种用例,可以参阅 yew 这类项目。

+ +

在本教程中,我们将使用 Rust 的 npm 包构建工具 wasm-pack 来构建一个 npm 包。这个包只包含 WebAssembly 和 JavaScript 代码,以便包的用户无需安装 Rust 就能使用。他们甚至不需要知道这里包含 WebAssembly!

+ +

安装 Rust 环境

+ +

让我们看看安装 Rust 环境的所有必要步骤。

+ +

安装Rust

+ +

前往 Install Rust 页面并跟随指示安装 Rust。这里会安装一个名为 “rustup” 的工具,这个工具能让你管理多个不同版本的 Rust。默认情况下,它会安装用于惯常Rust开发的 stable 版本 Rust Release。Rustup 会安装 Rust 的编译器 rustc、Rust 的包管理工具 cargo、Rust 的标准库 rust-std 以及一些有用的文档 rust-docs

+ +
+

Note: 需要注意,在安装完成后,你需要把 cargo 的 bin 目录添加到你系统的 PATH 。一般来说它会自动添加,但需要你重启终端后才会生效。 

+
+ +

wasm-pack

+ +

要构建我们的包,我们需要一个额外工具 wasm-pack。它会帮助我们把我们的代码编译成 WebAssembly 并制造出正确的 npm 包。使用下面的命令可以下载并安装它:

+ +
$ cargo install wasm-pack
+
+ +

安装 Node.js 并获取 npm 账户

+ +

在这个例子中我们将会构建一个 npm 包,因此你需要确保安装 Node.js 和 npm 已经安装。另外,我们将会把包发布到 npm 上,因此你还需要一个 npm 账号。它们是免费的。发布这个包并不是必须的,但是发布它非常简单,因此在本例中我们默认你会发布这个包。

+ +

Get npm! 页面按照说明下载并安装 Node.js 和 npm。在选择版本时,选择一个你喜欢的版本;本例不限定特定版本。

+ +

npm signup page 注册 npm 账户,并填写表格。

+ +

接下来,在命令行中运行 npm adduser:

+ +
> npm adduser
+Username: yournpmusername
+Password:
+Email: (this IS public) you@example.com
+
+ +

你需要完善你的用户名,密码和邮箱。如果成功了,你将会看到:

+ +
Logged in as yournpmusername on https://registry.npmjs.org/.
+
+ +

如果并未正常运行,请联系 npm 解决。

+ +

构建我们的 WebAssembly npm 包

+ +

万事俱备,来创建一个新的 Rust 包吧。打开你用来存放你私人项目的目录,做这些事:

+ +
$ cargo new --lib hello-wasm
+     Created library `hello-wasm` project
+
+ +

这里会在名为 hello-wasm 的子目录里创建一个新的库,里面有下一步之前你所需要的一切:

+ +
+-- Cargo.toml
++-- src
+    +-- lib.rs
+
+ +

首先,我们有一个 Cargo.toml 文件,这是我们配置构建的方式。如果你用过 Bundler 的 Gemfile 或者 npm 的 package.json,你应该会感到很熟悉。Cargo 的用法和它们类似。

+ +

接下来,Cargo 在 src/lib.rs 生成了一些 Rust 代码:

+ +
#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+        assert_eq!(2 + 2, 4);
+    }
+}
+
+ +

我们完全不需要使用这些测试代码,所以继续吧,我们删掉它。

+ +

来写点 Rust 代码吧!

+ +

让我们在 src/lib.rs 写一些代码替换掉原来的:

+ +
extern crate wasm_bindgen;
+
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+extern {
+    pub fn alert(s: &str);
+}
+
+#[wasm_bindgen]
+pub fn greet(name: &str) {
+    alert(&format!("Hello, {}!", name));
+}
+
+ +

这就是我们这个 Rust 项目的内容。它有三个主要部分,让我们按顺序来讲。这里将会给出一个缺少部分细节的高级说明;如果想要了解更多 Rust 知识,请查看在线书籍 The Rust Programming Language

+ +

使用 wasm-bindgen 在 Rust 与 JavaScript 之间通信

+ +

第一部分看起来像这样:

+ +
extern crate wasm_bindgen;
+
+use wasm_bindgen::prelude::*;
+
+ +

第一行就像在说 “哇 Rust,我们在用一个叫做 wasm_bindgen 的库”。在 Rust 当中,库被称为 “crates”,因为我们使用的是一个外部库,所以有  "extern"。

+ +

明白了吗? Cargo ships crates.

+ +

第三行包括了一个将库中的代码引入到你的代码中的使用命令。在这个情况下,将会引入 wasm_bindgen::prelude 的全部模块。我们将在下一节中使用这些内容。

+ +

在我们开始下一节之前,我们将讲一讲 wasm-bindgen.

+ +

wasm-pack 使用 wasm-bindgen,其它工具,去提供一个连接 JavaScript 和 Rust 的桥。它允许 JavaScript 使用 string 调用 Rust API,或者调用一个 Rust function 去捕获 JavaScript 异常。

+ +

我们将在我们的包中使用 wasm-bindgen 的功能。事实上,这是下一节的内容!

+ +

在 Rust 中调用来自 JavaScript  的外部函数

+ +

接下来的部分看起来像这样:

+ +
#[wasm_bindgen]
+extern {
+    pub fn alert(s: &str);
+}
+
+ +

#[] 中的内容叫做 "属性",并以某种方式改变下面的语句。在这种情况下,下面的语句是一个 extern,它将告诉 Rust that 我们想调用一些外部定义的函数。这个属性告诉我们 "wasm-bindgen 知道如何找到这些函数"。

+ +

第三行是用 Rust 写的函数签名。它告诉我们 "alert 函数接受一个叫做 s 的字符串作为参数。"

+ +

你可能会疑惑这个函数是什么,你的疑惑可能是正确的:这是 the alert function provided by JavaScript!我们将在下一节中调用这个函数。

+ +

当你想调用新的 JavaScript 函数时,你可以在这里写他们,wasm-bindgen 将负责为您设置一切。并非一切都得到支持,但我们正在努力!如果缺少某些内容,file bugs

+ +

编写能够在JavaScript中调用的 Rust 函数

+ +

最后一部分是这样的:

+ +
#[wasm_bindgen]
+pub fn greet(name: &str) {
+    alert(&format!("Hello, {}!", name));
+}
+
+ +

我们又看到了 #[wasm_bindgen] 属性。在这里,它并非定义一个 extern 块,而是 fn,这代表我们希望能够在 JavaScript 中使用这个 Rust 函数。 这和 extern 正相反:我们并非引入函数,而是要把函数给外部世界使用。

+ +

这个函数的名字是 greet,它需要一个参数,一个字符串 (写作 &str)。它调用了我们前面在 extern 块中引入的 alert 函数。它传递了一个让我们串联字符串的 format! 宏的调用。

+ +

format! 在这里有两个参数,一个格式化字符串和一个要填入的变量。格式化字符串是 "Hello, {}!" 部分。它可以包含一个或多个 {},变量将会被填入其中。传递的变量是 name,也就是这个函数的参数。所以当我们调用 greet("Steve")时我们就能看到 "Hello, Steve!"。

+ +

这个传递到了 alert(),所以当我们调用这个函数时,我们应该能看到他谈弹出了一个带有 "Hello, Steve!" 的消息框。

+ +

我们的库写完了,是时候构建它了。

+ +

把我们的代码编译到 WebAssembly

+ +

为了能够正确的编译我们的代码,首先我们需要配置 Cargo.toml。打开这个文件,将内容改为如下所示:

+ +
[package]
+name = "hello-wasm"
+version = "0.1.0"
+authors = ["Your Name <you@example.com>"]
+description = "A sample project with wasm-pack"
+license = "MIT/Apache-2.0"
+repository = "https://github.com/yourgithubusername/hello-wasm"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+wasm-bindgen = "0.2"
+
+ +

你需要改为自己的仓库,同时 Cargo 需要通过 git 来完善 authors 部分。

+ +

最重要的是添加底下的部分。第一个部分 — [lib] — 告诉 Rust 为我们的包建立一个 cdylib 版本;在本教程中我们不会讲解它的含义。有关更多信息,请参阅 CargoRust Linkage 文档。

+ +

第二个部分是 [dependencies] 部分。在这里我们告诉 Cargo 我们需要依赖哪个版本的 wasm-bindgen ;在这个例子中,它是 0.2.z 版本的 (不是 0.3.0 或者其他版本)。

+ +

构建包

+ +

现在我们已经完成了所有配置项,开始构建吧!在命令行输入以下命令:

+ +
$ wasm-pack build --scope mynpmusername
+
+ +

这个命令将做一系列事情 (这会花一些时间,特别是当你第一次运行 wasm-pack)。想了解详细情况,查看这篇在 Mozilla Hacks 上的文章。简单来说,wasm-pack build 将做以下几件事:

+ +
    +
  1. 将你的 Rust 代码编译成 WebAssembly。
  2. +
  3. 在编译好的 WebAssembly 代码基础上运行 wasm-bindgen,生成一个 JavaScript 文件将 WebAssembly 文件包装成一个模块以便 npm 能够识别它。
  4. +
  5. 创建一个 pkg 文件夹并将 JavaScript 文件和生成的 WebAssembly 代码移到其中。
  6. +
  7. 读取你的 Cargo.toml 并生成相应的 package.json。
  8. +
  9. 复制你的 README.md (如果有的话) 到文件夹中。
  10. +
+ +

最后的结果?你在 pkg 文件夹下有了一个 npm 包。

+ +

对代码体积的一些说明

+ +

如果你检查生成的 WebAssembly 文件体积,它可能有几百 kB。我们没有让 Rust 去压缩生成的代码,从而大大减少生成包的体积。这和本次教程主题无关,但如果你想了解更多,查看 Rust WebAssembly 工作组文档上关于 减少 .wasm 体积 的说明。

+ +

把我们的包发布到 npm

+ +

把我们的新包发布到 npm registry:

+ +
$ cd pkg
+$ npm publish --access=public
+
+ +

我们现在有了一个 npm 包,使用 Rust 编写,但已经被编译为 WebAssembly 了。现在这个包已经可以被 JavaScript 使用了,而且使用它完全不需要用户安装 Rust ;包中的代码是 WebAssembly 代码,而不是 Rust 源码!

+ +

在网站上使用我们的包

+ +

让我们建立一个使用我们包的网站! 人们通过各种打包工具使用 npm包,在本教程中,我们将使用 webpack 它比其他某些打包工具稍微复杂一点,但展示了更实际的用法。
+
+ 让我们离开
pkg目录,并创建一个新目录site,尝试以下操作:

+ +
$ cd ../..
+$ mkdir site
+$ cd site
+
+ +

创建一个新文件 package.json,然后输入如下代码:

+ +
{
+  "scripts": {
+    "serve": "webpack-dev-server"
+  },
+  "dependencies": {
+    "@mynpmusername/hello-wasm": "^0.1.0"
+  },
+  "devDependencies": {
+    "webpack": "^4.25.1",
+    "webpack-cli": "^3.1.2",
+    "webpack-dev-server": "^3.1.10"
+  }
+}
+
+ +

请注意,您需要在依赖项部分的 @ 之后填写自己的用户名。
+
+ 接下来,我们需要配置Webpack。 创建
webpack.config.js 并输入:

+ +
const path = require('path');
+module.exports = {
+  entry: "./index.js",
+  output: {
+    path: path.resolve(__dirname, "dist"),
+    filename: "index.js",
+  },
+  mode: "development"
+};
+
+ +

现在我们需要一个HTML文件。 创建一个index.html并写入如下内容:

+ +
<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>hello-wasm example</title>
+  </head>
+  <body>
+    <script src="./index.js"></script>
+  </body>
+</html>
+
+ +

最后,从HTML文件中引用index.js

+ +
const js = import("./node_modules/@yournpmusername/hello-wasm/hello_wasm.js");
+js.then(js => {
+  js.greet("WebAssembly");
+});
+
+ +

请注意,您需要再次填写您的npm用户名。
+
+ 这将从node_modules文件夹导入我们的模块。这不是最佳做法,但这里只做一个演示,因此暂时就这样用。 加载后,它将从该模块调用greet函数,并传入字符串“WebAssembly”参数。注意这里看上去没有什么特别的,但是我们正在调用 Rust 代码! 就JavaScript代码所知,这只是一个普通模块。
+
+ 我们已经完成了所有的文件! 让我们试一下:

+ +
$ npm install
+$ npm run serve
+
+ +

这将启动一个Web服务器。 访问 http://localhost:8080 ,您应该会在屏幕上看到一个警告框,其中包含 Hello, WebAssembly!我们已经成功地从 JavaScript 调用了 Rust,并从 Rust 调用了 JavaScript。

+ +

+

结论

+ + +

本教程到此结束。希望你觉得它有用。

+ +

在这个领域,有很多工作正在推进当中。如果你希望它变得更好,可以参阅  Rust Webassembly 工作组

diff --git a/files/zh-cn/webassembly/text_format_to_wasm/index.html b/files/zh-cn/webassembly/text_format_to_wasm/index.html new file mode 100644 index 0000000000..671c2a0b71 --- /dev/null +++ b/files/zh-cn/webassembly/text_format_to_wasm/index.html @@ -0,0 +1,72 @@ +--- +title: 将WebAssembly文本格式转换为wasm +slug: WebAssembly/Text_format_to_wasm +tags: + - WebAssembly + - wabt + - wasm + - wast2wasm + - 文本格式 + - 汇编 + - 转换 +translation_of: WebAssembly/Text_format_to_wasm +--- +
{{WebAssemblySidebar}}
+ +

WebAssembly 有一个基于S-表达式的文本表示形式,设计为在文本编辑器,浏览器开发人员工具等中暴露的一个中间形式。本文解释了它是如何工作的一些内容以及如何使用可用的工具把文本格式文件转换为.wasm汇编格式文件。

+ +
+

注:文本格式文件通常被保存为.wat扩展名;有时.wast也被使用,它是说文件包含了额外的测试命令(断言等)并且它们不需要转换到.wasm中。

+
+ +

初识文本格式

+ +

让我们看一个简单的例子——下面的程序从一个叫做imports的模块中导入了一个叫做imported_func的函数并且导出了一个叫做exported_func的函数:

+ +
(module
+  (func $i (import "imports" "imported_func") (param i32))
+  (func (export "exported_func")
+    i32.const 42
+    call $i
+  )
+)
+ +

WebAssembly函数exported_func是被导出供我们的环境(比如,使用了WebAssembly模块的网络应用)使用。当被调用的时,它进而调用了一个被导入的叫做imported_func的函数并且向该函数传递了一个值(42)作为参数。

+ +

把.wat文本文件转换为.wasm二进制文件

+ +

让我们尝试一下把前面提到的wat文本表示的例子转换为wasm汇编格式。

+ +
    +
  1. 首先,把前面的清单内容复制到一个文本文件中;命名为simple.wat。
  2. +
  3. 在使用它之前,我们需要把这个文本表示汇编为浏览器能够识别的汇编语言。为了达到这个目的,我们可以使用wabt工具,该工具包括了在WebAssembly文本表示和wasm之间进行相互转化的编译器以及其他一些功能。访问 https://github.com/webassembly/wabt——按照该页面的指令来安装好工具。
  4. +
  5. 当你安装好工具之后,将/wabt/out目录添加到你的系统路径。
  6. +
  7. 下一步,执行wat2wasm程序,把输入文件的路径传递给它,紧跟一个-o参数,然后是输出文件的路径: +
    wat2wasm simple.wat -o simple.wasm
    +
  8. +
+ +

该命令会把wasm输出到一个叫做simple.wasm的文件,该文件包含了.wasm汇编代码。

+ +
+

:你可以使用wasm2wat工具把汇编代码转换为文本表示;例如,wasm2wat simple.wasm -o text.wat。

+
+ +

查看汇编输出

+ +

因为输出文件是基于汇编的,所以,它不能在常规的文本编辑器中查看。尽管如此,你可以使用wat2wasm工具的-v选项来查看。试试这个:

+ +
wat2wasm simple.wat -v
+ +

这会在终端产生一个如下所示的输出:

+ +

several strings of binary with textual descriptions beside them. For example: 0000008: 01 ; section code

+ +

另见

+ + diff --git a/files/zh-cn/webassembly/understanding_the_text_format/index.html b/files/zh-cn/webassembly/understanding_the_text_format/index.html new file mode 100644 index 0000000000..6e4c72ffa4 --- /dev/null +++ b/files/zh-cn/webassembly/understanding_the_text_format/index.html @@ -0,0 +1,540 @@ +--- +title: 理解WebAssembly文本格式 +slug: WebAssembly/Understanding_the_text_format +tags: + - JavaScript + - S-表达式 + - WebAssembly + - wasm + - 共享地址 + - 内存 + - 函数 + - 文本格式 + - 表格 + - 调用 +translation_of: WebAssembly/Understanding_the_text_format +--- +
{{WebAssemblySidebar}}
+ +

为了能够让人类阅读和编辑WebAssembly,wasm二进制格式提供了相应的文本表示。这是一种用来在文本编辑器、浏览器开发者工具等工具中显示的中间形式。本文用基本语法的方式解释了这种文本表示是如何工作的,以及它是如何与它表示的底层字节码,及在JavaScript中表示wasm的封装对象关联起来的。
+ 本质上,这种文本形式更类似于处理器的汇编指令。

+ +
+

:如果你是一个Web开发者并且只是想在页面中加载wasm模块然后在你的代码中使用它(参考使用WebAssembly的JavaScript API),那么,本文可能有点儿强人所难了。但是,如果你想编写wasm模块从而优化你的JavaScript的性能或者构建你自己的WebAssembly编译器,那么,本文是很有用的。

+
+ +

S-表达式

+ +

不论是二进制还是文本格式,WebAssembly代码中的基本单元是一个模块。在文本格式中,一个模块被表示为一个大的S-表达式。

+ +

S-表达式是一个非常古老和非常简单的用来表示树的文本格式。因此,我们可以把一个模块想象为一棵由描述了模块结构和代码的节点组成的树。不过,与一门编程语言的抽象语法树不同的是,WebAssembly的树是相当平的,也就是大部分包含了指令列表。

+ +

首先,让我们看下S-表达式长什么样。树上的每个一个节点都有一对括号——( ... )——包围。括号内的第一个标签告诉你该节点的类型,其后跟随的是由空格分隔的属性或孩子节点列表。

+ +

S-表达式如下:

+ +
(module (memory 1) (func))
+ +

这条表达式,表示一棵根节点为“模块(module)”的树,该树有两个孩子节点,分别是 属性为1的“内存(memory)”节点 和 一个“函数(func)”节点。我们一会儿就会看到这些节点的含义。

+ +

最简单的模块

+ +

让我们从最简单最短的可能的wasm模块开始。

+ +
(module)
+ +

这个模块完全是空的,但是仍然是一个合法的模块。

+ +

如果我们现在把该模块转换为二进制(参考把WebAssembly文本格式转换为wasm),我们将会看到在二进制格式中描述的8字节的模块头:

+ +
0000000: 0061 736d              ; WASM_BINARY_MAGIC
+0000004: 0d00 0000              ; WASM_BINARY_VERSION
+ +

向你的模块中增加功能

+ +

好了,那并不是很有趣,让我们向模块中增加一些可执行代码。

+ +

WebAssembly模块中的所有代码都是划分到函数里面。函数具有下列的伪代码结构:

+ +
( func <signature> <locals> <body> )
+ + + +

签名和参数

+ +

签名是由一系列参数类型声明,及其后面的返回值类型声明列表组成。值得注意的是:

+ + + +

每一个参数都有一个显式声明的类型,wasm当前有四个可用类型:

+ + + +

参数格式为 (param <类型>),返回值格式为 (result <类型>)

+ +

因此,接受两个32位整数,返回一个64位浮点数的函数应该这样写:

+ +
(func (param i32) (param i32) (result f64) ... )
+ +

在签名的后面是带有类型的局部变量,格式为 (local <类型>)。函数调用可以通过参数实参值对局部变量进行初始化。

+ +

获取和设置局部变量和参数

+ +

局部变量和参数能够被函数体使用get_local和set_local指令进行读写。

+ +

get_local/set_local指令使用数字索引来指向将被存取的条目:按照它们的声明顺序,参数在前,局部变量在后。因此,给定下面的函数:

+ +
(func (param i32) (param f32) (local f64)
+  get_local 0
+  get_local 1
+  get_local 2)
+ + + +

由于使用数字索引来指向某个条目容易让人混淆,因此,也可以通过别名的方式来访问它们,方法就是在类型声明的前面添加一个使用美元符号($)作为前缀的名字。例如:

+ +
(func (param $p1 i32) (param $p2 f32) (local $loc i32) …)
+ +

这里,使用get_local $p1就代替get_local 0,访问参数i32变量时,就可以通过 $p1 进行访问。

+ +
+

注意,当文本转换为二进制后,二进制中只包含整数。

+
+ +

栈式机器

+ +

虽然浏览器把wasm编译为某种更高效的东西,但是,wasm的执行是以栈式机器定义的。也就是说,其基本理念是每种类型的指令都是在栈上执行数值的入栈出栈操作。

+ +

例如,get_local被定义为把它读到的局部变量值压入到栈上,然后i32.add从栈上取出两个i32类型值(它的含义是把前面压入栈上的两个值取出来)计算它们的和(以2^32求模),最后把结果压入栈上。

+ +

当函数被调用的时候,它是从一个空栈开始的。随着函数体指令的执行,栈会逐步填满和清空。例如,在执行了下面的函数之后:

+ +
(func (param $p i32)
+  get_local $p
+  get_local $p
+  i32.add)
+ +

栈上只包含一个i32类型值——表达式 ($p + $p)的结果,该结果是由i32.add得到的。函数的返回值就是栈上留下的那个最终值。

+ +

WebAssembly验证规则确保栈准确匹配:如果你声明了(result f32),那么,最终栈上必须包含一个f32类型值。如果没有result类型,那么栈必须是空的。

+ +

我们的第一个函数体

+ +

正如前面提到的,函数体就是函数被调用后执行的指令列表。把已经学到的放在一起,我们能够定义一个包含我们的简单函数的模块:

+ +
(module
+  (func (param $lhs i32) (param $rhs i32) (result i32)
+    get_local $lhs
+    get_local $rhs
+    i32.add))
+ +

这个函数获取两个参数,然后相加,最后返回其结果。

+ +

有很多东西都可以放在函数体里面,但是,现在我们从简单的开始,然后随着逐步前进,你会看到更多的例子。访问webassembly.org语义手册获取可用操作码的完整列表。

+ +

调用函数

+ +

我们的函数自己不会做什么——现在,我们需要调用它。我们该如何做呢?正如在一个ES2015模块里面一样,wasm函数必须通过模块里面的export语句显式地导出。

+ +

像局部变量一样,函数默认也是通过索引来区分的,但是为了方便,可以给它们起个名字。让我们由此开始——首先,在关键字func的后面增加一个美元符号开头的名字:

+ +
(func $add … )
+ +

现在,我们需要增加一个导出声明——看起来像下面这样:

+ +
(export "add" (func $add))
+ +

这里的add是JavaScript中用来区别这个函数的名字,而$add则是指出模块中的哪个WebAssembly函数将会被导出:

+ +

所以,我们最终的模块(当前)看起来像下面这样:

+ +
(module
+  (func $add (param $lhs i32) (param $rhs i32) (result i32)
+    get_local $lhs
+    get_local $rhs
+    i32.add)
+  (export "add" (func $add))
+)
+ +

如果你想继续研究这个例子,那么把我们上面的模块保存到一个名叫add.wat的文件中,然后使用wabt(参考把WebAssembly文本格式转换为wasm)将其转换为名叫add.wasm的二进制文件。

+ +

接下来,我们把二进制文件加载到叫做addCode的带类型数组(获取WebAssembly字节码),编译并实例化它,然后在JavaScript中执行我们的add函数(现在,我们可以在实例的exports属性中找到add())。

+ +
fetchAndInstantiate('add.wasm').then(function(instance) {
+   console.log(instance.exports.add(1, 2));  // "3"
+});
+
+// fetchAndInstantiate() found in wasm-utils.js
+function fetchAndInstantiate(url, importObject) {
+  return fetch(url).then(response =>
+    response.arrayBuffer()
+  ).then(bytes =>
+    WebAssembly.instantiate(bytes, importObject)
+  ).then(results =>
+    results.instance
+  );
+}
+ +
+

:你可以在GitHub上找到这个例子 add.html (实时运行)。另外,参考 WebAssembly.instantiate() 来获取关于实例化函数的更多细节以及wasm-utils.js来获取fetchAndInstantiate()的源代码。

+
+ +

探索基本原则

+ +

现在,我们已经讨论了基本概念,让我们继续看看更高级的特性。

+ +

在同一模块里的函数调用其他函数成员

+ +

为函数给定一个索引或名字,call指令可以调用它。例如,下面的模块包含两个函数——一个返回值42,另一个返回,第一个函数结果加1。

+ +
(module
+  (func $getAnswer (result i32)
+    i32.const 42)
+  (func (export "getAnswerPlus1") (result i32)
+    call $getAnswer
+    i32.const 1
+    i32.add))
+ +
+

:i32.const只是定义一个32位整数并把它压入栈。你可以把i32替换为任何其他可用的类型,并把const值修改为你想要的任何值(这里,我们把这个值设置为42)。

+
+ +

在这个例子中,你注意到一个(export "getAnswerPlus1")代码段,并且它声明在第二个函数的func语句之后——这声明我们想导出这个函数,以及定义导出的名字的简便方法。

+ +

从功能上来说,这与我们前面做过的那样,在函数外面,即模块的其他地方,包括一个独立的函数语句是等价的。例如:

+ +
(export "getAnswerPlus1" (func $functionName))
+ +

调用我们前面模块的JavaScript看起来像这样:

+ +
fetchAndInstantiate('call.wasm').then(function(instance) {
+  console.log(instance.exports.getAnswerPlus1());  // "43"
+});
+ +
+

注:你可以在GitHub上找到这个例子call.wasm (或实时运行)。再提一次,查看wasm-utils.js来了解fetchAndInstantiate()的源代码。

+
+ +

从JavaScript导入函数

+ +

我们已经见过JavaScript调用WebAssembly函数,但是WebAssembly如何调用JavaScript函数呢?事实上,WebAssembly对JavaScript没有任何了解,但是,它有一个可以导入JavaScript或wasm函数的通用方法。让我们看一个例子:

+ +
(module
+  (import "console" "log" (func $log (param i32)))
+  (func (export "logIt")
+    i32.const 13
+    call $log))
+ +

WebAssembly使用了两级命名空间,所以,这里的导入语句是说我们要求从console模块导入log函数。另外,你可以看到在logIt函数中,通过call指令调用了JavaScrpit导入的函数log。

+ +

导入的函数就像普通函数一样:它们拥有一个WebAssembly验证机制,会静态检查的签名,可以被设置一个索引,能够被命名和被调用。

+ +

JavaScript函数没有签名的概念,因此,无论导入的声明签名是什么,任何JavaScript函数都可以被传递过来。一旦一个模块声明了一个导入, WebAssembly.instantiate()的调用者必须传递一个拥有相应属性的导入对象。

+ +

就上面而言,我们需要一个(让我们称之为importObject的)对象,并且importObject.console.log是一个JavaScript函数。

+ +

这看起来像下面这样:

+ +
var importObject = {
+  console: {
+    log: function(arg) {
+      console.log(arg);
+    }
+  }
+};
+
+fetchAndInstantiate('logger.wasm', importObject).then(function(instance) {
+  instance.exports.logIt();
+});
+ +
+

:你可以在GitHub上找到这个例子logger.html (实时运行)。

+
+ +

WebAssembly内存

+ +

上面的例子是一个相当简单的日志函数:它只是打印一个整数!要是我们想输出一个文本字符串呢?为了处理字符串及其他复杂数据类型,WebAssembly提供了内存。

+ +

按照WebAssembly的定义,内存就是一个随着时间增长的字节数组。WebAssembly包含诸如i32.load和i32.store指令来实现对线性内存的读写。

+ +

从JavaScript的角度来看,内存就是一个ArrayBuffer,并且它是可变大小的。从字面上来说,这也是asm.js所做的(除了它不能改变大小;参考asm.js编程模型)。

+ +

因此,一个字符串就是位于这个线性内存某处的字节序列。

+ +

让我们假设我们已经把一个合适的字符串字节写入到了内存中;那么,我们该如何把那个字符串传递给JavaScript呢?

+ +

关键在于JavaScript能够通过{{jsxref("WebAssembly.Memory()")}}接口创建WebAssembly线性内存实例,并且能够通过相关的实例方法获取已经存在的内存实例(当前每一个模块实例只能有一个内存实例)。内存实例拥有一个buffer获取器,它返回一个指向整个线性内存的ArrayBuffer。

+ +

内存实例也能够增长。举例来说,在JavaScript中可以调用Memory.grow()方法。由于ArrayBuffer不能改变大小,所以,当增长产生的时候,当前的ArrayBuffer会被移除, 并且一个新的ArrayBuffer会被创建并指向新的、更大的内存。这意味着为了向JavaScript传递一个字符串,我们所需要做的就是把字符串在线性内存中的偏移量,以及表示其长度的方法传递出去。

+ +

虽然有许多不同的方法在字符串自身当中保存字符串的长度(例如,C字符串);但是,这里为了简单起见,我们仅仅把偏移量和长度都作为参数:

+ +
(import "console" "log" (func $log (param i32) (param i32)))
+ +

在JavaScript端,我们可以使用文本解码器API,轻松地把我们的字节解码转化为一个JavaScript字符串。(这里,我们使用utf8,不过,许多其他编码也是支持的。)

+ +
consoleLogString(offset, length) {
+  var bytes = new Uint8Array(memory.buffer, offset, length);
+  var string = new TextDecoder('utf8').decode(bytes);
+  console.log(string);
+}
+ +

这个谜题的最后一部分就是consoleLogString从哪里获得?WebAssembly的内存(memory)实例。这里,WebAssembly给我们很大灵活性:我们既可以使用JavaScript创建一个内存对象,让WebAssembly模块导入这个内存,或者我们让WebAssembly模块创建这个内存并把它导出给JavaScript。

+ +

为了简单起见,让我们用JavaScript创建它,然后把它导入到WebAssembly。我们的导入语句编写如下:

+ +
(import "js" "mem" (memory 1))
+ +

1表示导入的内存必须至少有1页内存。(WebAssembly定义一页为64KB。)

+ +

因此,让我们看一个完整的打印字符串“Hi”的模块。在一个常规的已编译的C程序,你会调用一个函数来为字符串分配一段内存。但是,因为我们正在编写自己的汇编,并且我们拥有整个线性内存,所以,我们可以使用数据(data)段把字符串内容写入到一个全局内存中。数据段允许字符串字节在实例化时被写在一个指定的偏移量。而且,它与原生的可执行格式中的数据(.data)段是类似的。

+ +

我们最终的wasm模块看起来像这样:

+ +
(module
+  (import "console" "log" (func $log (param i32 i32)))
+  (import "js" "mem" (memory 1))
+  (data (i32.const 0) "Hi")
+  (func (export "writeHi")
+    i32.const 0  ;; pass offset 0 to log
+    i32.const 2  ;; pass length 2 to log
+    call $log))
+ +
+

: 注意上面的双分号语法,它允许在WebAssembly文件中添加注释。

+
+ +

现在,我们可以从JavaScript中创建一个1页的内存(Memory )然后把它传递进去。这会在控制台输出"Hi"。

+ +
var memory = new WebAssembly.Memory({initial:1});
+
+var importObj = { console: { log: consoleLogString }, js: { mem: memory } };
+
+fetchAndInstantiate('logger2.wasm', importObject).then(function(instance) {
+  instance.exports.writeHi();
+});
+ +
+

:你可以在GitHub上找到完整源代码logger2.html (或者实时运行)。

+
+ +

WebAssembly表格

+ +

为了结束WebAssembly文本格式之旅,让我们看看最难理解的、常常令人迷惑的WebAssembly部分:表格。

+ +

总的来说,表格是从WebAssembly代码中通过索引获取的可变大小的引用数组。

+ +

为了了解为什么表格是必须的,我们首先需要观察前面看到的call指令,它接受一个静态函数索引,并且只调用了一个函数——但是,如果被调用者是一个运行时值呢?

+ + + +

WebAssembly需要一种做到这一点的调用指令,因此,我们有了接受一个动态函数操作数的call_indirect指令。问题是,在WebAssembly中,当前操作数的仅有的类型是i32/i64/f32/f64。

+ +

WebAssembly可以增加一个anyfunc类型("any"的含义是该类型能够持有任何签名的函数),但是,不幸的是,由于安全原因,这个anyfunc类型不能存储在线性内存中。线性内存会把存储的原始内容作为字节暴露出去,并且这会使得wasm内容能够任意的查看和修改原始函数地址,而这在网络上是不被允许的。

+ +

解决方案是在一个表格中存储函数引用,然后作为 代替,传递表格索引——它们只是i32类型值。因此,call_indirect的操作数可以是一个i32类型索引值。

+ +

在wasm中定义一个表格

+ +

那么,我们该如何在表格中放置wasm函数呢?就像数据段能够用来通过字节初始化线性内存区域一样,元素(elem)段能够用来通过函数初始化表格区域:

+ +
(module
+  (table 2 anyfunc)
+  (elem (i32.const 0) $f1 $f2)
+  (func $f1 (result i32)
+    i32.const 42)
+  (func $f2 (result i32)
+    i32.const 13)
+  ...
+)
+ + + +
+

: 未初始化的元素会被设定一个默认的调用即抛出(throw-on-call)值。

+
+ +

在JavaScript中,可以创建这样一个表格实例的等价的函数调用看起来如下所示:

+ +
function() {
+  // table section
+  var tbl = new WebAssembly.Table({initial:2, element:"anyfunc"});
+
+  // function sections:
+  var f1 = function() { … }
+  var f2 = function() { … }
+
+  // elem section
+  tbl.set(0, f1);
+  tbl.set(1, f2);
+};
+ +

使用表格

+ +

接着继续。现在,表格已经定义好了,我们需要用某种方法使用它。让我们使用下面的代码段来做到这一点:

+ +
(type $return_i32 (func (result i32))) ;; if this was f32, type checking would fail
+(func (export "callByIndex") (param $i i32) (result i32)
+  get_local $i
+  call_indirect $return_i32)
+ + + +

你也可以在命令调用的时候显式地声明call_indirect的参数,就像下面这样:

+ +
(call_indirect $return_i32 (get_local $i))
+ +

在更高层面,像JavaScript这样更具表达力的语言,你可以设想使用一个数组(或者更有可能的是对象)来完成相同的事情。伪代码看起来像这样:tbl[i]()。

+ +

回到类型检查。因为WebAssembly是带有类型检查的,并且anyfunc的含义是任何函数签名,所以,我们必须在调用点提供假定的被调用函数签名。这里,我们包含了一个$return_i32类型来告诉程序期望的是一个返回值为i32类型的函数。如果被调用函数没有一个匹配的签名(比如说返回值是f32类型的),那么,程序会抛出WebAssembly.RuntimeError异常。

+ +

那么,是什么把call_indirect指令和我们要是用的表格联系起来的呢?答案是,现在每一个模块实例只允许唯一一个表格存在,这也就是call_indirect指令隐式地使用的表格。在将来,当多表格被允许了,我们需要在代码行中指明一个某种形式的表格标识符:

+ +
call_indirect $my_spicy_table $i32_to_void
+ +

完整的模块看起来如下所示并且能够在我们的wasm-table.wat示例文件中找到:

+ +
(module
+  (table 2 anyfunc)
+  (func $f1 (result i32)
+    i32.const 42)
+  (func $f2 (result i32)
+    i32.const 13)
+  (elem (i32.const 0) $f1 $f2)
+  (type $return_i32 (func (result i32)))
+  (func (export "callByIndex") (param $i i32) (result i32)
+    get_local $i
+    call_indirect $return_i32)
+)
+ +

我们使用下面的JavaScript把它加载到一个网页中:

+ +
fetchAndInstantiate('wasm-table.wasm').then(function(instance) {
+  console.log(instance.exports.callByIndex(0)); // 返回42
+  console.log(instance.exports.callByIndex(1)); // 返回13
+  console.log(instance.exports.callByIndex(2));
+  // 返回一个错误,因为在表格中没有索引值2
+});
+ +
+

:你可以在GitHub上找到这个例子 wasm-table.html (实时查看)。

+
+ +
+

:就像内存一样,表格也能够从JavaScript中创建 (参考 WebAssembly.Table())并且能够导入和导出到其他wasm模块。

+
+ +

改变表格和动态链接

+ +

因为JavaScript对于函数引用有完全的存取权限,所以,从JavaScript中通过grow()get()set()方法能够改变表格对象。

+ +

因为表格是可变的,所以,它们能够用来实现复杂的加载时和运行时动态链接。当程序被动态地链接,多个实例共享相同的内存和表格。这与原生应用程序的多个.dll共享一个进程地址空间是等价的。

+ +

为了看看实际情况,我们会创建一个包含一个内存对象和一个表格对象的导入对象,并且把这个导入对象传递到多个instantiate()调用中去。

+ +

我们的.wat看起来像这样:

+ +

shared0.wat:

+ +
(module
+  (import "js" "memory" (memory 1))
+  (import "js" "table" (table 1 anyfunc))
+  (elem (i32.const 0) $shared0func)
+  (func $shared0func (result i32)
+   i32.const 0
+   i32.load)
+)
+ +

shared1.wat:

+ +
(module
+  (import "js" "memory" (memory 1))
+  (import "js" "table" (table 1 anyfunc))
+  (type $void_to_i32 (func (result i32)))
+  (func (export “doIt”) (result i32)
+   i32.const 0
+   i32.const 42
+   i32.store  ;; store 42 at address 0
+   i32.const 0
+   call_indirect $void_to_i32)
+)
+ +

运行逻辑如下:

+ +
    +
  1. 函数shared0func在shared0.wat中定义并存储在我们的导出表格对象(table)中。
  2. +
  3. 该函数先创建一个常量值为0,然后执行i32.load指令。用给定的内存索引,去加载存储到内存对象中的值,给定的索引值为0。—— 这样,会隐式地将之前的值出栈。所以,shared0func加载并返回了存储在内存对象索引0处的值。
  4. +
  5. 在shared1.wat中,我们导出了一个名为doIt的函数——这个函数创建了两个常量值,分别为0和42,然后使用i32.store指令把给定的值存储在指定索引位置的内存对象中。同样的,该指令会把这些值出栈,所以,结果就是把42存储在内存索引0处。
  6. +
  7. 在这个函数的最后一部分,我们创建了常量值0,然后调用表格中索引0处的函数,该函数正是我们之前在shared0.wat中的使用元素代码段(elem block)存储的shared0func。
  8. +
  9. shared0func在被调用之后会加载我们在shared1.wat中使用i32.store指令存储在内存中的42。
  10. +
+ +
+

:上面的表达式会隐式地把这些值出栈,但是,你可以在使用指令的时候进行显式地声明。例如:

+ +
(i32.store (i32.const 0) (i32.const 42))
+(call_indirect $void_to_i32 (i32.const 0))
+
+ +

在转换为汇编之后,我们可以在JavaScript中通过下面的代码使用shared0.wasm和shared1.wasm:

+ +
var importObj = {
+  js: {
+    memory : new WebAssembly.Memory({ initial: 1 }),
+    table : new WebAssembly.Table({ initial: 1, element: "anyfunc" })
+  }
+};
+
+Promise.all([
+  fetchAndInstantiate('shared0.wasm', importObj),
+  fetchAndInstantiate('shared1.wasm', importObj)
+]).then(function(results) {
+  console.log(results[1].exports.doIt());  // prints 42
+});
+ +

每一个将被编译的模块都可以导入相同的内存和表格对象,这也就是共享相同的线性内存和表格的“地址空间”。

+ +
+

:你可以在GitHub上找到这个例子shared-address-space.html (或者实时运行)。

+
+ +

总结

+ +

以上我们概括浏览了,关于WebAssembly文本格式的主要部分,以及它们是如何映射到WebAssembly JS API中的。 

+ +

另见

+ + diff --git a/files/zh-cn/webassembly/using_the_javascript_api/index.html b/files/zh-cn/webassembly/using_the_javascript_api/index.html new file mode 100644 index 0000000000..91b318c700 --- /dev/null +++ b/files/zh-cn/webassembly/using_the_javascript_api/index.html @@ -0,0 +1,281 @@ +--- +title: 使用WebAssembly JavaScript API +slug: WebAssembly/Using_the_JavaScript_API +tags: + - API + - JavaScript + - WebAssembly + - wasm + - 内存 + - 实例化 + - 开发者工具 + - 编译 + - 表格 +translation_of: WebAssembly/Using_the_JavaScript_API +--- +
{{WebAssemblySidebar}}
+ +

如果您已经使用Emscripten等工具编译了另一种语言的模块,或者自己加载并运行代码,那么下一步是了解如何使用WebAssembly JavaScript API的其他功能。这篇文章告诉你你需要知道什么。

+ +
+

:如果你不熟悉本文中提到到基础概念并且需要更多的解释,先阅读 WebAssembly概念 然后再回来。

+
+ +

一个简单的例子

+ +

让我们通过一步一步的例子来了解如何在WebAssembly 中使用 Javascript API,和如何在网页中加载一个 wasm 模块。

+ +
+

:你可以发现同样的代码在 webassembly-examples GitHub 仓库.

+
+ +

准备工作

+ +
    +
  1. 首先需要一个 wasm 模块!下载 simple.wasm 文件到本机的一个新的目录下.
  2. +
  3. 确保本机使用的是支持 webassembly 的浏览器。Firefox 52+ 和 Chrome 57+ 是默认支持 webassembly 的.
  4. +
  5. 然后, 创建一个简单的HTML 文件命名为 index.html 和并且你的本机的 wasm 文件处于同一目录下 ( 如果你没有模板可以使用我们提供的 simple template ).
  6. +
  7. 现在, 为了帮助我们理解发生了什么, 让我们来看看这个 wasm 模块的文本表示(也可以在将WebAssembly文本格式转换为wasm见到): +
    (module
    +  (func $i (import "imports" "imported_func") (param i32))
    +  (func (export "exported_func")
    +    i32.const 42
    +    call $i))
    +
  8. +
  9. 在第二行, 你将看到导入有一个两级命名空间 —— 内部函数 $i 是从 imports.imported_func 导入的. 编写要导入到wasm模块的对象时,我们需要在JavaScript中反映这个两级命名空间. 创建一个 <script></script> 节点在你的HTML 文件中, 并且添加下面的代码: +
    var importObject = {
    +  imports: {
    +      imported_func: function(arg) {
    +        console.log(arg);
    +      }
    +    }
    +  };
    +
  10. +
+ +

如上所述,  我们在 imports.imported_func 中有我们导入的函数.

+ +
+

:使用 ES6箭头函数 将会更加简洁:

+ +
let importObject = {
+    imports: {
+        imported_func: arg => console.log(arg),
+    }
+};
+
+
+ +

具体哪种风格由你决定.

+ +

加载并使用 wasm 模块

+ +

当我们导入了对象后, 我们将获取 wasm 文件, 使其在 array buffer 可用, 然后就可以使用其导出的函数.

+ +

在第一个块下面添加以下代码到你的脚本中:

+ +
fetch('simple.wasm')
+.then(res =>
+  res.arrayBuffer()
+).then(bytes =>
+  WebAssembly.instantiate(bytes, importObject)
+).then(results => {
+  results.instance.exports.exported_func();
+});
+ +
+

:我们已经非常详细地解释了这种语法如何工作通过加载和运行WebAssembly代码。如果不确定,请回到那里进行复习。

+
+ +

这样做的结果是执行我们导出的 WebAssembly 函数 exported_func,这样又调用了另一个我们导入的 JavaScript 函数 imported_func, 它将WebAssembly实例(42)中提供的值记录到控制台. 如果你保存实例代码并且在支持 WebAssembly 的浏览器中运行,你将看到此操作。

+ +
+

:WebAssembly 在 Firefox 52+ 和 Chrome 57+/latest Opera 是默认支持的(你也可以运行 wasm 代码 在 Firefox 47+ 通过将 about:config 中的 javascript.options.wasm flag 设置为 enabling , 或者在 Chrome (51+) 以及 Opera (38+) 通过访问 chrome://flags 并且将 Experimental WebAssembly flag 设置为 enabling.)

+
+ +

这是一个冗长的,令人费解的例子并且实现了很少的功能, 但它确实有助于说明这是可能的 —— 在 Web 应用中与 JavaScript 一起使用 WebAssembly 代码. 正如我们一直说的,  WebAssembly 并不旨在替代 JavaScript; 两者可以一起工作,借鉴对方的优势。

+ +

在开发者工具查看wasm 

+ +

在 Firefox 54+,  Developer Tool Debugger Panel 有用于公开网页中包含的任何 wasm 代码的文本表示的功能. 为了查看它们, 要查看它,您可以转到 Debugger Panel 然后单击 “xxx > wasm” .

+ +

+ +

从 Firfox 开始,除了将WebAssembly视为文本,开发者可以使用文本格式调试 (打断点, 检查调用堆栈, 单步调试等等.) WebAssembly 代码. 通过这个视频使用Firefox开发者工具调试WebAssembly预览。

+ +

内存

+ +

在WebAssembly的底层内存模型中,内存被表示为称为 线性内存 的无类型字节的连续范围,通过模块中的加载和存储指令读取和写入。  在这个内存模型中, 任何加载或存储都可以访问整个线性存储器中的任何字节,这是忠实地表示C / C ++概念(如指针)所必需的。

+ +

然后,和原生 C/C++ 程序不同的是可用内存范围跨越整个进程,特定WebAssembly实例可访问的内存被限制在由WebAssembly Memory对象包含的一个特定的 —— 可能非常小的范围内。

+ +

在 JavaScript 中,内存实例可以被认为是可调整大小的ArrayBuffer,就像ArrayBuffers一样,一个Web应用程序可以创建许多独立的内存对象。 您可以使用 {{jsxref("WebAssembly.Memory()")}} 构造函数创建一个,它将参数作为初始大小和(可选)最大大小)。

+ +

我们通过一个快速的例子来开始探索。

+ +
    +
  1. +

    创建另一个简单的 HTML 页面 (复制我们的 simple template) 并且命名为 memory.html。添加一个 <script></script> 节点到页面中。

    +
  2. +
  3. +

    在脚本的顶部添加下面的一行代码来创建一个内存实例:

    + +
    var memory = new WebAssembly.Memory({initial:10, maximum:100});
    + +

    初始和最大的单位是 WebAssembly pages ——这些页面的大小固定为64KB。这意味着上述内存实例的初始大小为640KB,最大大小为6.4MB。

    + +

    WebAssembly内存通过简单地提供一个返回ArrayBuffer的缓冲区getter / setter来显示它的字节。例如,要直接将42写入线性内存的第一个单词,你可以这样做:

    + +
    new Uint32Array(memory.buffer)[0] = 42;
    + +

    你也可以得到刚才的值通过:

    + +
    new Uint32Array(memory.buffer)[0]
    +
  4. +
  5. +

    现在尝试这个演示 —— 保存目前为止添加的内容,将其加载到浏览器中,然后尝试在JavaScript控制台中输入上述两行。

    +
  6. +
+ +

增加内存

+ +

一个内存实例的大小可以通过 {{jsxref("Memory.prototype.grow()")}} 来增加,再次以 WebAssembly pages 为单位指定参数:

+ +
memory.grow(1);
+ +

如果在创建内存实例时提供了最大值,则尝试超过此最大值将抛出 {{jsxref("WebAssembly.RangeError")}} 异常。 引擎利用这个提供的上限来提前预留内存,这样可以使调整大小更有效率。

+ +

Note: 由于 {{domxref("ArrayBuffer")}} 的byteLength是不可变的,所以在成功 {{jsxref("Memory.prototype.grow()")}} 操作之后,缓冲区getter将返回一个新的ArrayBuffer对象 新的byteLength)和任何先前的ArrayBuffer对象变成“分离”,或者与先前指向的底层内存断开连接。

+ +

和函数一样,线性内存可以在模块内部进行定义或者导入。类似地,模块还可以可选地导出其内存。这这意味着JavaScript可以通过创建new WebAssembly.Memory 并将其作为导入或通过接收内存导出传递给WebAssembly实例的内存来访问(通过 Instance.prototype.exports).

+ +

更复杂的内存示例

+ +

让我们通过看一个更复杂的内存示例——一个对整数数组进行求和的WebAssembly模块——来明确上面的概念。你可以在这里memory.wasm找到示例。

+ +
    +
  1. +

    像前面那样在相同的目录下复制一份memory.wasm。

    + +
    +

    :你可以在这里memory.wat找到模块的文本表示形式。

    +
    +
  2. +
  3. +

    回到你的示例文件memory.html,像前面那样获取、编译和实例化你的wasm模块——在你的脚本代码底部加入下面的代码:

    + +
    fetch('memory.wasm').then(response =>
    +  response.arrayBuffer()
    +).then(bytes =>
    +  WebAssembly.instantiate(bytes)
    +).then(results => {
    +  // 在这里加入你的代码
    +});
    +
  4. +
  5. +

    因为该模块导出了它的内存,给定该模块的一个实例,我们可以使用一个导出函数accumulate()在该模块实例的线性内存(mem)中创建和填入一个输入数组。在前面指明的地方加入如下代码:

    + +
    var i32 = new Uint32Array(results.instance.exports.mem.buffer);
    +for (var i = 0; i < 10; i++) {
    +  i32[i] = i;
    +}
    +
    +var sum = results.instance.exports.accumulate(0, 10);
    +console.log(sum);
    +
  6. +
+ +

注意我们是如何在内存对象的缓存上创建了 {{domxref("Uint32Array")}}视图,而不是在内存对象本身这么做。

+ +

内存导入与函数导入很像,只是内存对象取代了JavaScript函数作为了传入值。内存导入在下面两方面很有用:

+ + + +
+

:你可以在这里memory.html (或实时运行) 找到我们的完整例子——这个版本使用了fetchAndInstantiate()函数。

+
+ +

表格

+ +

WebAssembly表格是一个可变大小的带类型的引用数组,其中的引用可以被JavaScript和WebAssembly代码存取。然而,内存提供的是一个可变大小的带类型的原始字节数组。所以,把引用存储在内存中是不安全。由于安全、可移植性和稳定性等原因,作为引擎信任的引用值是千万不能被直接读写的。

+ +

表格有一个元素类型,其限制了可以存储在表格的引用类型。在当前的WebAssembly版本中,只有一种WebAssembly代码所需要的引用类型——函数——也就是唯一合法的元素类型。在将来的版本中,更多的元素类型会被加入。

+ +

函数引用对于编译诸如C/C++这类拥有函数指针的语言来说是必要的。在C/C++的原生实现中,函数指针是通过函数代码在进程的虚地址空间的原始地址表示的,并且由于前面提到的安全原因,它是不能被直接存储在线性内存中的。取而代之的是,函数引用被存储在表格之中。它们的整数索引可以存储在线性内存中并进行传递。

+ +

当调用一个函数指针的时候,WebAssembly调用函数提供索引。在进行索引和调用索引到的函数引用之前,可以对该索引进行表格的边界检查。因而,目前的表格是一个相当底层的用来安全地和可移植地编译底层编程语言特性的基本类型。

+ +

表格可以通过Table.prototype.set()和 Table.prototype.grow()进行更改,它们会更新表格中的一个值和增加可以存储在表格的大小。这允许间接可调用函数集合可以随着时间而改变,其对于动态链接技术来说是必要的。这些更改对于JavaScript和wasm模块来说是立即生效的。同时,在JavaScript可以通过Table.prototype.get()得到最新值。

+ +

表格示例

+ +

让我们看一个简单的表格示例——一个WebAssembly模块,该模块创建并导出了一个带有两个元素的表格:元素0返回13,元素1返回42。你可以在table.wasm中找到该示例。

+ +
    +
  1. +

    在一个新的目录中复制一份table.wasm。

    + +
    +

    :你可以在table.wat中查看模块的文本表示。

    +
    +
  2. +
  3. +

    创建一份HTML 模板的新副本并将其命名为table.html.

    +
  4. +
  5. +

    如前所示,获取、编译并且实例化你的wasm模块——将下面的代码放入到HTML body底部的<script>节点里面:

    + +
    fetch('table.wasm').then(response =>
    +  response.arrayBuffer()
    +).then(bytes =>
    +  WebAssembly.instantiate(bytes)
    +).then(results => {
    +  // 在这里添加你的代码
    +});
    +
  6. +
  7. +

    现在,让我们获取表格中的数据——将下面的代码放入到指定的位置:

    + +
    var tbl = results.instance.exports.tbl;
    +console.log(tbl.get(0)());  // 13
    +console.log(tbl.get(1)());  // 42
    +
  8. +
+ +

这段代码获取获取了存储在表格中的每一个函数引用,然后实例化它们从而将它们拥有的值打印到控制台——注意每一各函数引用是如何使用Table.prototype.get()函数获取的以及在其后面增加一对小括号从而真正的调用该函数。

+ +
+

:你可以在table.html (或实时查看运行)找到我们完整的示例——这个版本使用了fetchAndInstantiate()函数。

+
+ +

多样性

+ +

现在,我们已经展示了WebAssembly的主要组成模块的使用,这里是提到多样性概念的好地方。这为WebAssembly提供了大量的关于架构效率的优势:

+ + + +

你可以在我们的理解文本格式一本中看到多样性的应用——参考修改表格和动态链接部分(TBD)。

+ +

总结

+ +

本文带你了解了使用WebAssembly的JavaScript API的基本知识,包括在JavaScript上下文中导入一个WebAssembly模块、使用该模块的函数以及在JavaScript中使用WebAssembly的内存和表格。同时,我们也介绍了多样性的概念。

+ +

另见

+ + + +
+
+
-- cgit v1.2.3-54-g00ecf