各位老铁,双击666,今天要跟大家唠唠嗑,不对,是聊聊硬核的 WebAssembly
。咱们的目标是:把 C++
代码编译成浏览器能跑的 Wasm
,再用 JavaScript
像使唤丫鬟一样使唤它。
第一部分:WebAssembly
是个啥?
WebAssembly
(简称 Wasm
),你可以把它想象成一个轻量级的虚拟机,但这个虚拟机不是跑操作系统那种,而是专门跑代码的。它的特点是:
- 快! 比
JavaScript
快得多,因为它是编译型的,直接运行机器码。 - 安全! 在沙箱里运行,不会直接访问你的电脑。
- 可移植! 几乎所有现代浏览器都支持。
简单来说,Wasm
就是为了解决 JavaScript
在性能密集型应用上的不足而生的。比如,游戏、图像处理、音视频编码等等。
第二部分:Emscripten
:C++
到 Wasm
的桥梁
要让 C++
代码变成 Wasm
,我们需要一个工具,这个工具就是 Emscripten
。Emscripten
是一个 LLVM
编译器,它可以把 C++
代码编译成 Wasm
字节码,还能生成一些 JavaScript
代码,方便我们在 JavaScript
中调用 Wasm
。
2.1 安装 Emscripten
安装 Emscripten
的方法有很多,这里推荐使用 Emscripten SDK
(emsdk)。
-
下载
emsdk
: 你可以从Emscripten
的官网下载最新版本的emsdk
。 -
解压
emsdk
到你喜欢的目录。 -
打开命令行,进入
emsdk
目录,然后执行以下命令:./emsdk install latest ./emsdk activate latest source ./emsdk_env.sh
注意:在 Windows 上,你需要使用相应的 Windows 命令。
-
验证安装:执行
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++
代码
接下来,用 Emscripten
把 C++
代码编译成 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_message
是C++
函数的名称,前面加一个下划线是因为Emscripten
会自动给函数名加下划线。-s MODULARIZE=1
: 将生成的Wasm
代码封装成一个JavaScript
模块。-s 'EXPORT_NAME="MyModule"'
: 指定模块的名称为MyModule
。-o add.js
: 指定输出文件名为add.js
。Emscripten
会生成两个文件:add.js
(JavaScript胶水代码) 和add.wasm
(WebAssembly 字节码)。
第三部分:在 JavaScript
中调用 Wasm
现在,我们已经有了 add.js
和 add.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
: 我们需要导出malloc
和free
函数,因为我们需要在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++ 、Rust 、Go 等多种语言编写代码。 |
可移植性 | 几乎所有现代浏览器都支持。 |
代码复用 | 可以复用现有的 C++ 代码,而无需重写。 |
5.2 劣势
劣势 | 描述 |
---|---|
学习曲线 | 相比 JavaScript ,学习 WebAssembly 需要掌握更多的知识,比如 C++ 、Emscripten 等。 |
调试难度 | 调试 WebAssembly 代码比调试 JavaScript 代码更困难。 |
生态系统 | WebAssembly 的生态系统还不够完善,很多常用的库和工具还没有 WebAssembly 版本。 |
内存管理 | 需要手动管理内存,容易出现内存泄漏等问题。 |
DOM 操作 | WebAssembly 不能直接操作 DOM ,需要通过 JavaScript 来操作。 |
第六部分:总结
WebAssembly
是一项很有前途的技术,它可以让 Web
应用拥有更好的性能和更强大的功能。虽然学习曲线比较陡峭,但是掌握 WebAssembly
绝对是一件值得投资的事情。
希望今天的讲座能帮助大家入门 WebAssembly
。 以后有机会再跟大家聊聊更高级的 WebAssembly
应用。 感谢各位老铁!