Skip to content

Latest commit

 

History

History
607 lines (476 loc) · 29.4 KB

File metadata and controls

607 lines (476 loc) · 29.4 KB

五、创建和加载 WebAssembly 模块

我们在第 4 章安装所需依赖项中传递给emcc命令的标志产生了一个单独的.wasm文件,可以使用原生的WebAssembly对象在浏览器中加载和实例化。C 代码是一个非常简单的例子,旨在测试编译器,而不必适应包含的库或网络程序集的限制。通过利用 Emscripten 的一些功能,我们可以以最小的性能损失克服 C / C++ 代码中 WebAssembly 的一些限制。

在本章中,我们将介绍与使用 Emscripten 的粘合代码相对应的编译和加载步骤。我们还将描述编译/输出严格的.wasm文件并使用浏览器的WebAssembly对象加载它们的过程。

本章的目标是理解以下内容:

  • C 代码的编译过程,利用了 Emscripten 的 JavaScript“胶水”代码
  • 如何在浏览器中加载 Emscripten 模块
  • 只输出.wasm文件的 C 代码的编译过程(没有“胶水”代码)
  • 如何在 VS 代码中配置构建任务
  • 如何使用全局WebAssembly对象在浏览器中编译加载一个 Wasm 模块

用 Emscripten 胶水代码编译 C 语言

第 4 章安装所需的依赖项中,您编写并编译了一个简单的三线程序,以确保您的 Emscripten 安装是有效的。我们向emcc命令传递了几个只需要输出一个.wasm文件的标志。通过向emcc命令传递其他标志,我们可以在.wasm文件和一个 HTML 文件旁边输出 JavaScript 粘合代码来处理加载过程。在本节中,我们将编写一个更复杂的 C 程序,并用 Emscripten 提供的输出选项编译它。

编写示例 C 代码

在我们在第 4 章安装所需依赖项中介绍的示例中,我们没有包含任何头文件或传递任何函数。因为代码的目的仅仅是测试编译器安装是否有效,所以没有太多的必要。Emscripten 提供了许多额外的功能,使我们能够用 JavaScript 与 C 和 C++ 代码交互,反之亦然。其中一些功能是特定于环境的,不符合核心规范或其应用编程接口。在第一个例子中,我们将利用 Emscripten 的一个移植库和 Emscripten 的 API 提供的一个函数。

下面的程序使用一个简单直接媒体层 ( SDL2 )在画布上无限循环地对角移动一个矩形。取自https://github.com/timhutton/sdl-canvas-wasm,不过我把它从 C++ 转换成了 C,对代码稍作修改。该部分的代码位于learn-webassembly存储库的/chapter-05-create-load-module文件夹中。按照以下说明用 Emscripten 编译 C。

在你的/book-examples文件夹中创建一个名为/chapter-05-create-load-module的文件夹。在这个名为with-glue.c的文件夹中创建一个新文件,并用以下内容填充它:

/*
 * Converted to C code taken from:
 * https://github.com/timhutton/sdl-canvas-wasm
 * Some of the variable names and comments were also
 * slightly updated.
 */
#include <SDL2/SDL.h>
#include <emscripten.h>
#include <stdlib.h>

// This enables us to have a single point of reference
// for the current iteration and renderer, rather than
// have to refer to them separately.
typedef struct Context {
  SDL_Renderer *renderer;
  int iteration;
} Context;

/*
 * Looping function that draws a blue square on a red
 * background and moves it across the <canvas>.
 */
void mainloop(void *arg) {
    Context *ctx = (Context *)arg;
    SDL_Renderer *renderer = ctx->renderer;
    int iteration = ctx->iteration;

    // This sets the background color to red:
    SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
    SDL_RenderClear(renderer);

    // This creates the moving blue square, the rect.x
    // and rect.y values update with each iteration to move
    // 1px at a time, so the square will move down and
    // to the right infinitely:
    SDL_Rect rect;
    rect.x = iteration;
    rect.y = iteration;
    rect.w = 50;
    rect.h = 50;
    SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);
    SDL_RenderFillRect(renderer, &rect);

    SDL_RenderPresent(renderer);

    // This resets the counter to 0 as soon as the iteration
    // hits the maximum canvas dimension (otherwise you'd
    // never see the blue square after it travelled across
    // the canvas once).
    if (iteration == 255) {
        ctx->iteration = 0;
    } else {
        ctx->iteration++ ;
    }
}

int main() {
    SDL_Init(SDL_INIT_VIDEO);
    SDL_Window *window;
    SDL_Renderer *renderer;

    // The first two 255 values represent the size of the <canvas>
    // element in pixels.
    SDL_CreateWindowAndRenderer(255, 255, 0, &window, &renderer);

    Context ctx;
    ctx.renderer = renderer;
    ctx.iteration = 0;

    // Call the function repeatedly:
    int infinite_loop = 1;

    // Call the function as fast as the browser wants to render
    // (typically 60fps):
    int fps = -1;

    // This is a function from emscripten.h, it sets a C function
    // as the main event loop for the calling thread:
    emscripten_set_main_loop_arg(mainloop, &ctx, fps, infinite_loop);

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();

    return EXIT_SUCCESS;
}

main()功能末尾的emscripten_set_main_loop_arg()是可用的,因为我们在文件顶部包含了emscripten.h。由于文件顶部的#include <SDL2/SDL.h>,前缀为SDL_的变量和函数是可用的。如果您在<SDL2/SDL.h>语句下看到一条弯弯曲曲的红色错误线,您可以忽略它。这是因为 SDL 的include路径不在你的c_cpp_properties.json文件中。

编译示例 C 代码

既然我们已经写好了 C 代码,我们就需要编译它。您必须传递给emcc命令的一个必需标志是-o <target>,其中<target>是所需输出文件的路径。该文件的扩展名不仅仅是输出该文件;它会影响编译器做出的一些决定。下表摘自位于的 Emscripten 的emcc文档:

| 延伸 | 输出 | | <name>.js | JavaScript 粘合代码(如果指定了s WASM=1标志,则为.wasm)。 | | <name>.html | HTML 和单独的 JavaScript 文件(<name>.js)。拥有独立的 JavaScript 文件可以缩短页面加载时间。 | | <name>.bc | LLVM 位码(默认)。 | | <name>.o | LLVM 位代码(与.bc相同)。 | | <name>.wasm | 仅 Wasm 文件(带有从第 4 章指定的标志,用于安装所需的依赖项)。 |

您可以忽略.bc.o文件扩展名——我们不需要输出 LLVM 位代码。.wasm扩展不在emcc 工具参考页面上,但是如果您传递了正确的编译器标志,它是一个有效的选项。这些输出选项会影响我们编写的 C/C++ 代码。

输出带有粘合代码的 HTML

如果您为输出指定了一个 HTML 文件扩展名(例如,-o with-glue.html),您将得到一个with-glue.htmlwith-glue.jswith-glue.wasm文件(假设您也指定了-s WASM=1)。如果你在 C/C++ 源文件中有一个main()函数,它会在 HTML 加载后立即执行该函数。让我们编译我们的示例 C 代码,看看这是如何操作的。要用 HTML 文件和 JavaScript 粘合代码编译它,请将cd放入/chapter-05-create-load-module文件夹并运行以下命令:

emcc with-glue.c -O3 -s WASM=1 -s USE_SDL=2 -o with-glue.html

第一次运行这个命令时,Emscripten 将下载并构建SDL2库。完成此操作可能需要几分钟,但您只需等待一次。Emscripten 缓存了这个库,因此后续的构建会更快。构建完成后,您将在文件夹中看到三个新文件:HTMLJavaScriptWasm文件。运行以下命令将文件本地serve:

serve -l 8080

如果您打开浏览器直到http://127.0.0.1:8080/with-glue.html,您应该会看到以下内容:

Emscripten loading code running in the browser

蓝色矩形应该从红色矩形的左上角对角移动到右下角。由于您在 C 文件中指定了一个main()函数,Emscripten 知道它应该立即执行它。如果在 VS 代码中打开with-glue.html文件,滚动到文件底部,会看到加载代码。你不会看到任何对WebAssembly对象的引用;这是在 JavaScript 粘合代码文件中处理的。

输出不带 HTML 的粘合代码

Emscripten 在 HTML 文件中生成的加载代码包含错误处理和其他有帮助的功能,以确保模块在执行main()功能之前被加载。如果您为输出文件的扩展名指定.js,您将不得不创建一个 HTML 文件并自己编写加载代码。在下一节中,我们将更详细地研究加载代码。

正在加载电子脚本模块

加载一个利用 Emscripten 的粘合代码的模块并与之交互与 WebAssembly 的 JavaScript API 有很大的不同。这是因为 Emscripten 提供了与 JavaScript 代码交互的附加功能。在本节中,我们将讨论在输出一个 HTML 文件时 Emscripten 提供的加载代码,并回顾在浏览器中加载 Emscripten 模块的过程。

预先生成的加载代码

如果在运行emcc命令时指定-o <target>.html,Emscripten 会生成一个 HTML 文件,并自动添加代码将模块加载到文件末尾。以下是不包括每个Module函数内容的 HTML 文件中的加载代码:

var statusElement = document.getElementById('status');
var progressElement = document.getElementById('progress');
var spinnerElement = document.getElementById('spinner');

var Module = {
  preRun: [],
  postRun: [],
  print: (function() {...})(),
  printErr: function(text) {...},
  canvas: (function() {...})(),
  setStatus: function(text) {...},
  totalDependencies: 0,
  monitorRunDependencies: function(left) {...}
};

Module.setStatus('Downloading...');

window.onerror = function(event) {
  Module.setStatus('Exception thrown, see JavaScript console');
  spinnerElement.style.display = 'none';
  Module.setStatus = function(text) {
    if (text) Module.printErr('[post-exception status] ' + text);
  };
};

Module对象中的功能用于检测和解决错误,监控Module的加载状态,并可选地在相应的粘合代码文件中的run()方法执行之前或之后执行一些功能。下面代码片段中显示的canvas函数从加载代码之前在 HTML 文件中指定的 DOM 中返回<canvas>元素:

canvas: (function() {
  var canvas = document.getElementById('canvas');
  canvas.addEventListener(
    'webglcontextlost',
    function(e) {
      alert('WebGL context lost. You will need to reload the page.');
      e.preventDefault();
    },
    false
  );

  return canvas;
})(),

这段代码便于检测错误并确保Module被加载,但出于我们的目的,我们不需要那么啰嗦。

编写自定义加载代码

Emscripten 生成的加载代码提供了有用的错误处理。如果您在生产中使用 Emscripten 的输出,我建议您包含它,以确保您正确处理错误。然而,我们实际上并不需要所有的代码来利用我们的Module。让我们编写一些简单得多的代码并测试一下。首先,让我们将 C 文件编译成没有 HTML 输出的粘合代码。为此,请运行以下命令:

emcc with-glue.c -O3 -s WASM=1 -s USE_SDL=2 -s MODULARIZE=1 -o custom-loading.js

-s MODULARIZE=1编译器标志允许我们使用类似 Promise 的 API 来加载我们的Module。编译完成后,在名为custom-loading.html/chapter-05-create-load-module文件夹中创建一个文件,并用以下内容填充:

<!doctype html>
<html lang="en-us">
<head>
  <title>Custom Loading Code</title>
</head>
<body>
  <h1>Using Custom Loading Code</h1>
  <canvas id="canvas"></canvas>
  <script type="application/javascript" src="custom-loading.js"></script>
  <script type="application/javascript">
    Module({
      canvas: (() => document.getElementById('canvas'))(),
    })
      .then(() => {
        console.log('Loaded!');
      });
  </script>
</body>
</html>

加载代码现在使用 ES6 的箭头函数语法作为画布加载函数,这减少了所需的代码行。通过在/chapter-05-create-load-module文件夹中运行serve命令启动本地服务器:

serve -l 8080

当您在浏览器中导航到http://127.0.0.1:8080/custom-loading.html时,您应该会看到以下内容:

Custom loading code running in the browser

当然,我们运行的函数并不是很复杂,但是它展示了加载 Emscripten 的Module的基本需求。我们将在第 6 章与 JavaScript 交互和调试中更详细地检查Module对象,但是现在请注意,加载过程不同于我们将在下一节中介绍的 WebAssembly。

不用粘合代码编译 C 语言

如果我们想根据官方规范使用 WebAssembly,而没有 Emscripten 提供的额外功能,我们需要向emcc命令传递一些标志,并确保我们编写的代码可以相对容易地被 WebAssembly 使用。在编写示例 C 代码部分,我们编写了一个程序,绘制了一个在红色画布上对角移动的蓝色矩形。它利用了 Emscripten 的一个移植库,SDL2。在本节中,我们将编写和编译一些不依赖于 Emscripten 的助手方法和移植库的 C 代码。

WebAssembly 的代码

在我们进入我们将用于我们的 WebAssembly 模块的 C 代码之前,让我们尝试一个实验。打开/chapter-05-create-load-module文件夹中的命令行界面,尝试运行以下命令:

emcc with-glue.c -Os -s WASM=1 -s USE_SDL=2 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 -o try-with-glue.wasm

编译完成后,您应该会在 VS Code 的文件浏览器面板中看到一个try-with-glue.wasm文件。右键单击该文件,然后选择“显示网络程序集”。对应的 Wat 表示的开头应该类似于下面的代码:

(module
  (type $t0 (func (param i32)))
  (type $t1 (func (param i32 i32 i32 i32 i32) (result i32)))
  (type $t2 (func (param i32) (result i32)))
  (type $t3 (func))
  (type $t4 (func (param i32 i32) (result i32)))
  (type $t5 (func (param i32 i32 i32 i32)))
  (type $t6 (func (result i32)))
  (type $t7 (func (result f64)))
  (import "env" "memory" (memory $env.memory 256))
  (import "env" "table" (table $env.table 4 anyfunc))
  (import "env" "memoryBase" (global $env.memoryBase i32))
  (import "env" "tableBase" (global $env.tableBase i32))
  (import "env" "abort" (func $env.abort (type $t0)))
  (import "env" "_SDL_CreateWindowAndRenderer" (func $env._SDL_CreateWindowAndRenderer (type $t1)))
  (import "env" "_SDL_DestroyRenderer" (func $env._SDL_DestroyRenderer (type $t0)))
  (import "env" "_SDL_DestroyWindow" (func $env._SDL_DestroyWindow (type $t0)))
  (import "env" "_SDL_Init" (func $env._SDL_Init (type $t2)))
  (import "env" "_SDL_Quit" (func $env._SDL_Quit (type $t3)))
  (import "env" "_SDL_RenderClear" (func $env._SDL_RenderClear (type $t2)))
  (import "env" "_SDL_RenderFillRect" (func $env._SDL_RenderFillRect (type $t4)))
  (import "env" "_SDL_RenderPresent" (func $env._SDL_RenderPresent (type $t0)))
  (import "env" "_SDL_SetRenderDrawColor" (func $env._SDL_SetRenderDrawColor (type $t1)))
  (import "env" "_emscripten_set_main_loop_arg" (func $env._emscripten_set_main_loop_arg (type $t5)))
  ...

如果您想在浏览器中加载并执行它,您必须将一个importObj对象传递给网络组件的instantiate()compile()函数,以及一个包含这些import "env"函数的env对象。Emscripten 用胶水代码在幕后为我们处理所有这些,这使它成为一个非常有价值的工具。然而,我们可以通过使用 DOM 来替换 SDL2 功能,同时仍然在 c 中跟踪矩形的位置。

我们将以不同的方式编写 C 代码,以确保我们只需将几个函数传递到importObj.env对象中即可执行代码。在/chapter-05-create-load-module文件夹中创建一个名为without-glue.c的文件,并用以下内容填充:

/*
 * This file interacts with the canvas through imported functions.
 * It moves a blue rectangle diagonally across the canvas
 * (mimics the SDL example).
 */
#include <stdbool.h>

#define BOUNDS 255
#define RECT_SIDE 50
#define BOUNCE_POINT (BOUNDS - RECT_SIDE)

// These functions are passed in through the importObj.env object
// and update the rectangle on the <canvas>:
extern int jsClearRect();
extern int jsFillRect(int x, int y, int width, int height);

bool isRunning = true;

typedef struct Rect {
  int x;
  int y;
  char direction;
} Rect;

struct Rect rect;

/*
 * Updates the rectangle location by 1px in the x and y in a
 * direction based on its current position.
 */
void updateRectLocation() {
    // Since we want the rectangle to "bump" into the edge of the
    // canvas, we need to determine when the right edge of the
    // rectangle encounters the bounds of the canvas, which is why
    // we're using the canvas width - rectangle width:
    if (rect.x == BOUNCE_POINT) rect.direction = 'L';

    // As soon as the rectangle "bumps" into the left side of the
    // canvas, it should change direction again.
    if (rect.x == 0) rect.direction = 'R';

    // If the direction has changed based on the x and y
    // coordinates, ensure the x and y points update
    // accordingly:
    int incrementer = 1;
    if (rect.direction == 'L') incrementer = -1;
    rect.x = rect.x + incrementer;
    rect.y = rect.y + incrementer;
}

/*
 * Clear the existing rectangle element from the canvas and draw a
 * new one in the updated location.
 */
void moveRect() {
    jsClearRect();
    updateRectLocation();
    jsFillRect(rect.x, rect.y, RECT_SIDE, RECT_SIDE);
}

bool getIsRunning() {
    return isRunning;
}

void setIsRunning(bool newIsRunning) {
    isRunning = newIsRunning;
}

void init() {
    rect.x = 0;
    rect.y = 0;
    rect.direction = 'R';
    setIsRunning(true);
}

我们将从 C 代码中调用函数来确定 xy 坐标。setIsRunning()功能可以用来暂停矩形的移动。现在我们的 C 代码已经准备好了,让我们编译它。在 VS 代码终端中,cd进入/chapter-05-create-load-module文件夹,运行以下命令:

emcc without-glue.c -Os -s WASM=1 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 -o without-glue.wasm

编译完成后,您可以右键单击生成的without-glue.wasm文件,并选择“显示网络组件”来查看 Wat 表示。对于import "env"项,您应该在文件顶部看到以下内容:

(module
  (type $t0 (func (param i32)))
  (type $t1 (func (result i32)))
  (type $t2 (func (param i32 i32 i32 i32) (result i32)))
  (type $t3 (func))
  (type $t4 (func (result f64)))
  (import "env" "memory" (memory $env.memory 256))
  (import "env" "table" (table $env.table 8 anyfunc))
  (import "env" "memoryBase" (global $env.memoryBase i32))
  (import "env" "tableBase" (global $env.tableBase i32))
  (import "env" "abort" (func $env.abort (type $t0)))
  (import "env" "_jsClearRect" (func $env._jsClearRect (type $t1)))
  (import "env" "_jsFillRect" (func $env._jsFillRect (type $t2)))
  ...

我们需要在importObj对象中传递_jsClearRect_jsFillRect函数。我们将在使用 JavaScript 交互代码的 HTML 文件部分介绍如何做到这一点。

在 VS 代码中使用构建任务进行编译

emcc命令有点冗长,对于不同的文件,必须在命令行上手动运行这个命令可能会很麻烦。为了加快编译过程,我们可以使用 VS Code 的 Tasks 特性为我们将要使用的文件创建一个构建任务。要创建构建任务,请选择任务|配置默认构建任务…,选择从模板创建任务选项,并选择其他以在.vscode文件夹中生成一个简单的tasks.json文件。更新文件内容以包含以下内容:

{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Build",
      "type": "shell",
      "command": "emcc",
      "args": [
        "${file}",
        "-Os",
        "-s", "WASM=1",
        "-s", "SIDE_MODULE=1",
        "-s", "BINARYEN_ASYNC_COMPILATION=0",
        "-o", "${fileDirname}/${fileBasenameNoExtension}.wasm"
       ],
      "group": {
        "kind": "build",
        "isDefault": true
       },
       "presentation": {
         "panel": "new"
       }
     }
  ]
}

label值只是运行任务时引用的一个名称。typecommand值表示它应该在 shell(终端)中运行emcc命令。args值是要传递给emcc命令的一组参数(基于空间分隔)。"${file}"参数告诉 VS Code 编译当前打开的文件。"${fileDirname}/${fileBasenameNoExtension}.wasm"参数表示.wasm输出将与当前打开的文件同名(扩展名为.wasm,应该放在当前打开文件的活动文件夹中。如果不指定${fileDirname},输出文件将放在根文件夹中(而不是本例中的/chapter-05-create-load-module)。

group对象表示该任务是默认的构建步骤,所以如果使用快捷键Cmd/Ctrl+Shift+B,这就是将要运行的任务。"new"presentation.panel值告诉 VS 代码在构建步骤运行时打开一个新的命令行界面实例。这是个人喜好,可以省略。

一旦文件被完全填充,您可以保存并关闭tasks.json文件。为了测试它,首先删除你在前面部分用emcc命令生成的without-glue.wasm文件。接下来,选择 Tasks | Run Build Task…或使用键盘快捷键Cmd/Ctrl+Shift+B,确保您已将光标置于文件中打开without-glue.c并运行构建任务。集成终端中的新面板将执行编译,一两秒钟后将出现一个without-glue.wasm文件。

获取并实例化一个 Wasm 文件

现在我们有了一个 Wasm 文件,我们需要一些 JavaScript 代码来编译和执行它。我们必须遵循几个步骤来确保代码可以在浏览器中成功使用。在本节中,我们将编写一些常见的 JavaScript 加载代码,我们可以在其他示例中重用这些代码,创建一个演示 Wasm 模块使用的 HTML 文件,并在浏览器中测试结果。

常见的 JavaScript 加载代码

我们将在几个示例中获取并实例化一个.wasm文件,因此将 JavaScript 加载代码移动到一个公共文件是有意义的。实际的获取和实例化代码只有几行,但是不得不重复重新定义 Emscripten 期望的importObj对象是浪费时间。我们将在一个通用的可访问文件中提供这些代码,以加快代码编写过程。在/book-examples文件夹中新建一个名为/common的文件夹,并添加一个名为load-wasm.js的文件,内容如下:

/**
 * Returns a valid importObj.env object with default values to pass
 * into the WebAssembly.Instance constructor for Emscripten's
 * Wasm module.
 */
const getDefaultEnv = () => ({
  memoryBase: 0,
  tableBase: 0,
  memory: new WebAssembly.Memory({ initial: 256 }),
  table: new WebAssembly.Table({ initial: 2, element: 'anyfunc' }),
  abort: console.log
});

/**
 * Returns a WebAssembly.Instance instance compiled from the specified
 * .wasm file.
 */
function loadWasm(fileName, importObj = { env: {} }) {
  // Override any default env values with the passed in importObj.env
  // values:
  const allEnv = Object.assign({}, getDefaultEnv(), importObj.env);

  // Ensure the importObj object includes the valid env value:
  const allImports = Object.assign({}, importObj, { env: allEnv });

  // Return the result of instantiating the module (instance and module):
  return fetch(fileName)
    .then(response => {
      if (response.ok) return response.arrayBuffer();
      throw new Error(`Unable to fetch WebAssembly file ${fileName}`);
    })
    .then(bytes => WebAssembly.instantiate(bytes, allImports));
}

getDefaultEnv()功能为 Emscripten 的 Wasm 模块提供所需的importObj.env内容。我们希望能够通过任何额外的进口,这就是为什么使用Object.assign()声明。随着 Wasm 模块预期的任何其他导入的增加,Emscripten 的 Wasm 输出将总是需要"env"对象的这五个导入语句:

(import "env" "memory" (memory $env.memory 256))
(import "env" "table" (table $env.table 8 anyfunc))
(import "env" "memoryBase" (global $env.memoryBase i32))
(import "env" "tableBase" (global $env.tableBase i32))
(import "env" "abort" (func $env.abort (type $t0)))

我们需要将这些传递到instantiate()函数中,以确保 Wasm 模块加载成功,否则浏览器会抛出错误。现在我们已经准备好了加载代码,让我们继续看 HTML 和矩形渲染代码。

网页

我们需要一个带有<canvas>元素和 JavaScript 代码的 HTML 页面来与 Wasm 模块进行交互。在/chapter-05-create-load-module文件夹中创建一个名为without-glue.html的文件,并用以下内容填充:

<!doctype html>
<html lang="en-us">
<head>
  <title>No Glue Code</title>
  <script type="application/javascript" src="../common/load-wasm.js"></script>
</head>
<body>
  <h1>No Glue Code</h1>
  <canvas id="myCanvas" width="255" height="255"></canvas>
  <div style="margin-top: 16px;">
    <button id="actionButton" style="width: 100px; height: 24px;">
      Pause
    </button>
  </div>
  <script type="application/javascript">
    const canvas = document.querySelector('#myCanvas');
    const ctx = canvas.getContext('2d');

    const env = {
      table: new WebAssembly.Table({ initial: 8, element: 'anyfunc' }),
      _jsFillRect: function (x, y, w, h) {
        ctx.fillStyle = '#0000ff';
        ctx.fillRect(x, y, w, h);
      },
      _jsClearRect: function() {
        ctx.fillStyle = '#ff0000';
        ctx.fillRect(0, 0, 255, 255);
      },
    };

    loadWasm('without-glue.wasm', { env }).then(({ instance }) => {
      const m = instance.exports;
      m._init();

      // Move the rectangle by 1px in the x and y every 20 milliseconds:
      const loopRectMotion = () => {
        setTimeout(() => {
          m._moveRect();
          if (m._getIsRunning()) loopRectMotion();
        }, 20)
      };

      // Enable you to pause and resume the rectangle movement:
      document.querySelector('#actionButton')
        .addEventListener('click', event => {
          const newIsRunning = !m._getIsRunning();
          m._setIsRunning(newIsRunning);
          event.target.innerHTML = newIsRunning ? 'Pause' : 'Start';
          if (newIsRunning) loopRectMotion();
        });

      loopRectMotion();
    });
  </script>
</body>
</html>

这段代码将复制我们在前面几节中创建的带有一些附加功能的 SDL 示例。当矩形碰到右下角时,它会改变方向。您还可以使用<canvas>元素下的按钮暂停和恢复矩形的移动。您可以看到我们如何将_jsFillRect_jsClearRect函数传递到importObj.env对象中,以便它们可以被 Wasm 模块引用。

端上来

让我们在浏览器中测试我们的代码。从 VS Code 终端,确保您在/book-examples文件夹中,并运行命令启动本地服务器:

serve -l 8080

重要的是你在/book-examples文件夹中。如果您尝试仅在/chapter-05-create-load-module文件夹中提供代码,您将无法使用loadWasm()功能。如果你打开浏览器到http://127.0.0.1:8080/chapter-05-create-load-module/without-glue.html,你应该会看到这个:

Without glue code example running in the browser

尝试按下暂停按钮;标题应改为开始,矩形应停止移动。再次单击它应该会导致矩形再次开始移动。

摘要

在这一章中,我们介绍了使用 Emscripten 粘合代码和 Wasm 模块的模块的编译和加载过程。通过利用 Emscripten 的一些内置特性,例如移植的库和助手方法,我们能够展示 Emscripten 提供的优势。我们讨论了一些可以传递给emcc命令的编译器标志,以及这将如何影响您的输出。通过利用 VS 代码的任务特性,我们能够设置一个构建命令来加速构建过程。我们还回顾了在没有粘合代码的情况下编译和加载 Wasm 模块的过程。我们编写了一些可重用的 JavaScript 代码来加载模块,以及与我们编译的 Wasm 模块交互的代码。

第 6 章与 JavaScript 的交互和调试中,我们将讲述在浏览器中与 JavaScript 的交互和调试技术。

问题

  1. SDL 代表什么?
  2. 除了 JavaScript、HTML 和 Wasm 之外,你还能为emcc命令生成什么其他带有-o标志的输出类型?
  3. 使用 Emscripten 预先生成的加载代码有什么好处?
  4. 你必须在 C/C++ 文件中给你的函数起什么名字才能确保它在浏览器中自动执行编译后的输出?
  5. 为什么在使用移植库时,我们不能只使用 Wasm 文件输出,而不使用“胶水”代码?
  6. VS Code 中运行默认构建任务的快捷键是什么?
  7. 为什么我们需要 Wasm 加载代码中的getDefaultEnv()方法?
  8. 传递到用 Emscripten 创建的 Wasm 模块的 Wasm 实例化代码中的importObj.env对象需要哪五项?

进一步阅读