JavaScript内核与高级编程之:`JavaScript` 的 `WebAssembly`:如何将 `C++` 代码编译成 `Wasm`,并在 `JavaScript` 中调用。

各位老铁,双击666,今天要跟大家唠唠嗑,不对,是聊聊硬核的 WebAssembly。咱们的目标是:把 C++ 代码编译成浏览器能跑的 Wasm,再用 JavaScript 像使唤丫鬟一样使唤它。

第一部分:WebAssembly 是个啥?

WebAssembly (简称 Wasm),你可以把它想象成一个轻量级的虚拟机,但这个虚拟机不是跑操作系统那种,而是专门跑代码的。它的特点是:

  • 快!JavaScript 快得多,因为它是编译型的,直接运行机器码。
  • 安全! 在沙箱里运行,不会直接访问你的电脑。
  • 可移植! 几乎所有现代浏览器都支持。

简单来说,Wasm 就是为了解决 JavaScript 在性能密集型应用上的不足而生的。比如,游戏、图像处理、音视频编码等等。

第二部分:EmscriptenC++Wasm 的桥梁

要让 C++ 代码变成 Wasm,我们需要一个工具,这个工具就是 EmscriptenEmscripten 是一个 LLVM 编译器,它可以把 C++ 代码编译成 Wasm 字节码,还能生成一些 JavaScript 代码,方便我们在 JavaScript 中调用 Wasm

2.1 安装 Emscripten

安装 Emscripten 的方法有很多,这里推荐使用 Emscripten SDK (emsdk)。

  1. 下载 emsdk: 你可以从 Emscripten 的官网下载最新版本的 emsdk

  2. 解压 emsdk 到你喜欢的目录。

  3. 打开命令行,进入 emsdk 目录,然后执行以下命令:

    ./emsdk install latest
    ./emsdk activate latest
    source ./emsdk_env.sh

    注意:在 Windows 上,你需要使用相应的 Windows 命令。

  4. 验证安装:执行 emcc -v,如果能看到 Emscripten 的版本信息,就说明安装成功了。

2.2 编写 C++ 代码

咱们先来写一个简单的 C++ 函数,计算两个数的和。

// add.cpp
#include <iostream>

extern "C" {
  int add(int a, int b) {
    return a + b;
  }

  void print_message(const char* message) {
    std::cout << message << std::endl;
  }
}
  • extern "C": 这个关键字告诉编译器,按照 C 的方式编译这个函数。因为 C++C 的函数命名方式不同,如果不加这个,Emscripten 就找不到这个函数。
  • print_message: 这个函数用来在 C++ 中打印消息,方便调试。

2.3 编译 C++ 代码

接下来,用 EmscriptenC++ 代码编译成 Wasm

emcc add.cpp -s EXPORTED_FUNCTIONS="['_add', '_print_message']" -s MODULARIZE=1 -s 'EXPORT_NAME="MyModule"' -o add.js

这条命令有点长,咱们来解释一下:

  • emcc: Emscripten 的编译器。
  • add.cpp: 要编译的 C++ 文件。
  • -s EXPORTED_FUNCTIONS="['_add', '_print_message']": 指定要导出的函数。_add_print_messageC++ 函数的名称,前面加一个下划线是因为 Emscripten 会自动给函数名加下划线。
  • -s MODULARIZE=1: 将生成的 Wasm 代码封装成一个 JavaScript 模块。
  • -s 'EXPORT_NAME="MyModule"': 指定模块的名称为 MyModule
  • -o add.js: 指定输出文件名为 add.jsEmscripten 会生成两个文件:add.js (JavaScript胶水代码) 和 add.wasm (WebAssembly 字节码)。

第三部分:在 JavaScript 中调用 Wasm

现在,我们已经有了 add.jsadd.wasm 文件,接下来就可以在 JavaScript 中调用 Wasm 函数了。

3.1 创建 HTML 文件

先创建一个 HTML 文件,引入 add.js

<!DOCTYPE html>
<html>
<head>
  <title>WebAssembly Example</title>
</head>
<body>
  <h1>WebAssembly Example</h1>
  <script src="add.js"></script>
  <script>
    MyModule().then(function(Module) {
      // 调用 Wasm 函数
      var result = Module.add(10, 20);
      console.log("Result: " + result);

      // 调用 print_message 函数
      Module.print_message("Hello from WebAssembly!");
    });
  </script>
</body>
</html>
  • MyModule().then(): Emscripten 生成的 JavaScript 代码会返回一个 Promise,我们需要用 .then() 方法来等待 Wasm 模块加载完成。
  • Module.add(10, 20): 调用 Wasm 中的 add 函数。
  • Module.print_message("Hello from WebAssembly!"): 调用 Wasm 中的 print_message 函数。

3.2 运行 HTML 文件

用浏览器打开 HTML 文件,你就可以在控制台中看到输出结果了。

Result: 30
Hello from WebAssembly!

第四部分:更复杂的数据类型:字符串和指针

上面的例子只是简单的整数运算,如果我们要传递字符串或者更复杂的数据类型,该怎么办呢?

4.1 传递字符串

Wasm 本身不支持字符串,所以我们需要手动分配内存,把字符串复制到 Wasm 的内存空间中,然后把指针传递给 Wasm 函数。

修改 C++ 代码:

// string_example.cpp
#include <iostream>
#include <string>

extern "C" {
  const char* greet(const char* name) {
    std::string greeting = "Hello, " + std::string(name) + "!";
    char* result = new char[greeting.length() + 1];
    strcpy(result, greeting.c_str());
    return result;
  }

  void free_string(char* str) {
    delete[] str;
  }
}
  • greet: 接收一个字符串,返回一个问候语。
  • free_string: 释放 greet 函数分配的内存。 很重要,否则会内存泄漏。

编译 C++ 代码:

emcc string_example.cpp -s EXPORTED_FUNCTIONS="['_greet', '_free_string', '_malloc', '_free']" -s MODULARIZE=1 -s 'EXPORT_NAME="StringModule"' -o string_example.js
  • _malloc_free: 我们需要导出 mallocfree 函数,因为我们需要在 JavaScript 中分配和释放 Wasm 的内存。

修改 HTML 文件:

<!DOCTYPE html>
<html>
<head>
  <title>String Example</title>
</head>
<body>
  <h1>String Example</h1>
  <script src="string_example.js"></script>
  <script>
    StringModule().then(function(Module) {
      // 传递字符串
      var name = "World";
      var namePtr = Module.allocateUTF8(name); // 分配内存并复制字符串

      var greetingPtr = Module.greet(namePtr); // 调用 Wasm 函数
      var greeting = Module.UTF8ToString(greetingPtr); // 将 Wasm 内存中的字符串转换为 JavaScript 字符串

      Module._free(namePtr); // 释放 namePtr
      Module._free(greetingPtr); // 释放 greetingPtr

      console.log(greeting); // 输出问候语
    });
  </script>
</body>
</html>
  • Module.allocateUTF8(name): Emscripten 提供的函数,用于在 Wasm 内存中分配空间并复制字符串。
  • Module.UTF8ToString(greetingPtr): Emscripten 提供的函数,用于将 Wasm 内存中的字符串转换为 JavaScript 字符串。
  • Module._free: 释放 malloc 分配的内存。

4.2 指针和数组

传递数组和指针的原理类似,都需要在 JavaScript 中分配 Wasm 内存,把数据复制到 Wasm 内存中,然后把指针传递给 Wasm 函数。

修改 C++ 代码:

// array_example.cpp
#include <iostream>

extern "C" {
  int sum_array(int* arr, int size) {
    int sum = 0;
    for (int i = 0; i < size; i++) {
      sum += arr[i];
    }
    return sum;
  }
}

编译 C++ 代码:

emcc array_example.cpp -s EXPORTED_FUNCTIONS="['_sum_array', '_malloc', '_free']" -s MODULARIZE=1 -s 'EXPORT_NAME="ArrayModule"' -o array_example.js

修改 HTML 文件:

<!DOCTYPE html>
<html>
<head>
  <title>Array Example</title>
</head>
<body>
  <h1>Array Example</h1>
  <script src="array_example.js"></script>
  <script>
    ArrayModule().then(function(Module) {
      // 传递数组
      var array = [1, 2, 3, 4, 5];
      var arrayPtr = Module._malloc(array.length * 4); // 分配内存 (int 是 4 字节)

      // 将 JavaScript 数组复制到 Wasm 内存中
      for (var i = 0; i < array.length; i++) {
        Module.setValue(arrayPtr + i * 4, array[i], 'i32');
      }

      var sum = Module.sum_array(arrayPtr, array.length); // 调用 Wasm 函数
      Module._free(arrayPtr); // 释放内存

      console.log("Sum: " + sum); // 输出结果
    });
  </script>
</body>
</html>
  • Module._malloc(array.length * 4): 分配内存,大小为数组长度乘以 4 (因为 int 是 4 字节)。
  • Module.setValue(arrayPtr + i * 4, array[i], 'i32'): 将 JavaScript 数组的元素复制到 Wasm 内存中。 i32 表示 32 位整数。

第五部分:WebAssembly 的优势和劣势

5.1 优势

优势 描述
性能 JavaScript 快得多,尤其是在性能密集型应用中。
安全 在沙箱中运行,不会直接访问你的电脑。
多语言支持 可以用 C++RustGo 等多种语言编写代码。
可移植性 几乎所有现代浏览器都支持。
代码复用 可以复用现有的 C++ 代码,而无需重写。

5.2 劣势

劣势 描述
学习曲线 相比 JavaScript,学习 WebAssembly 需要掌握更多的知识,比如 C++Emscripten 等。
调试难度 调试 WebAssembly 代码比调试 JavaScript 代码更困难。
生态系统 WebAssembly 的生态系统还不够完善,很多常用的库和工具还没有 WebAssembly 版本。
内存管理 需要手动管理内存,容易出现内存泄漏等问题。
DOM 操作 WebAssembly 不能直接操作 DOM,需要通过 JavaScript 来操作。

第六部分:总结

WebAssembly 是一项很有前途的技术,它可以让 Web 应用拥有更好的性能和更强大的功能。虽然学习曲线比较陡峭,但是掌握 WebAssembly 绝对是一件值得投资的事情。

希望今天的讲座能帮助大家入门 WebAssembly。 以后有机会再跟大家聊聊更高级的 WebAssembly 应用。 感谢各位老铁!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注