WebAssembly简介

WebAssembly是一个可移植、体积小、加载快并且兼容 Web 的全新二进制格式,其文件后缀名为wasm,是由主流浏览器厂商组成的 W3C 社区团体 制定的一个新的规范。

wasm示意图

主流浏览器已经支持WebAssembly

wasm浏览器支持情况

wasm浏览器支持情况

环境安装

wasm是一种二进制格式,主流的C/C++、Go以及Rust都可以编译wasm,Emscripten就是一个用于将C/C++编译为wasm的编译器工具集。

安装方式可以参考 Emscripten安装指南,如果是Mac用户并安装了brew,可以直接brew install emscripten进行安装,更为便捷。

1
2
3
4
5
6
7
~ emcc -v
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 1.38.44
clang version 6.0.1 (emscripten 1.38.44 : 1.38.44)
Target: x86_64-apple-darwin19.0.0
Thread model: posix
InstalledDir: /usr/local/Cellar/emscripten/1.38.44/libexec/llvm/bin
shared:INFO: (Emscripten: Running sanity checks)

如果运行emcc -v可以正常,表示Emscripten已经正常安装成功

样例

永远的hello world

1
2
3
4
5
6
// hello.c
#include <stdio.h>
int main(int argc, char **argv) {
printf("hello world\n");
return 0;
}

运行emcc hello.c -o hello.html 进行编译,编译后会生成三个文件。

1
2
3
4
├── hello.c
├── hello.html
├── hello.js
└── hello.wasm

运行 emrun --port 8080 .可以自动打开浏览器

hell world

也可以运行 emcc hello.c -o hello.js,这样不生成hello.html文件,可以通过nodejs直接运行

1
2
3
~ emcc hello.c -o hello.js
~ node hello.js
hello world

函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// fib.c
#include <stdio.h>
#include <emscripten.h>

void print();

// EMSCRIPTEN_KEEPALIVE是一个宏,用于标识函数名不被修改
int EMSCRIPTEN_KEEPALIVE fib(int n){
if(n == 0 || n == 1)
return 1;
else
return fib(n - 1) + fib(n - 2);
}

void greet() {
printf("hello world\n");
}

运行emcc fib.c -o fib.js -s EXPORTED_FUNCTIONS='["_greet"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap", "ccall"]'进行编译

接下来,我们来调用fib.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script src="fib.js"></script>
<script>
Module.onRuntimeInitialized = () => {
// 通过Module.cwrap调用
const fib = Module.cwrap('fib', 'number', ['number'])
console.log(`fib(10)=${fib(4)}`)

// 通过Module.ccall调用
const result = Module.ccall('fib', 'number', ['number'], [4])
console.log(`fib(10)=${result}`)

// 调用greet
const showGreet = Module.cwrap('greet', null, null)
showGreet()

}
</script>

cwrapccall都是调用wasm里面的原生函数,cwrap的参数为cwrap(functionName, returnType, argsType), ccall的参数为ccall(functionName, returnType, argsType, parameters)。前者返回一个函数,后者直接 返回执行结果

传递指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// pointer.c

#include <stdio.h>
#include <emscripten.h> // note we added the emscripten header

// 交换数字
void EMSCRIPTEN_KEEPALIVE swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}

// 数组里面的每个数字加1
void EMSCRIPTEN_KEEPALIVE addOne(int* input_ptr, int* output_ptr, int len){
int i;
for(i = 0; i < len; i++) {
output_ptr[i] = input_ptr[i] + 1;
}
}

// 统计字符串里面某个数字的出现次数
int EMSCRIPTEN_KEEPALIVE countOccurrences(const char * str, int len, char target) {
int i, count = 0;
for(i = 0; i < len; i++){
if(str[i] == target){
count++;
}
}
return count;
}

运行emcc pointer.c -o pointer.js -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap", "ccall", "getValue", "setValue"]'进行编译。这次我们用Node.js来执行pointer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// pointer_demo.js
const m = require('./pointer.js')

const mallocByteBuffer = len => {
const ptr = m._malloc(len)
const heapBytes = new Uint8Array(m.HEAPU8.buffer, ptr, len)
return heapBytes
}

const mallocUInt32Buffer = len => {
const ptr = m._malloc(len * 4)
const heapBytes = new Uint32Array(m.HEAPU8.buffer, ptr, len)
return heapBytes
}

const free = nativeBuffer => {
if (typeof nativeBuffer === 'number') {
m._free(nativeBuffer)
} else if (nativeBuffer && nativeBuffer.buffer) {
nativeBuffer.buffer instanceof ArrayBuffer && m._free(nativeBuffer.byteOffset)
}
}

// 交换数字
const swapNumbers = ({first = 0, second = 0} = {}) => {
var aPtr = m._malloc(4)
var bPtr = m._malloc(4)
m.setValue(aPtr, first, 'i32')
m.setValue(bPtr, second, 'i32')
m.ccall('swap', null, ['number', 'number'], [aPtr, bPtr])
return {
first: m.getValue(aPtr, 'i32'),
second: m.getValue(bPtr, 'i32')
}
}

// 数组数字加1
const plusArrays = (numbers = []) => {
// 传递数组
const length = numbers.length
const inputBuffer = mallocUInt32Buffer(length)
const outputBuffer = mallocUInt32Buffer(length)
// 填充数据
inputBuffer.set(numbers)
m.ccall('addOne', null, ['number', 'number', 'number'], [inputBuffer.byteOffset, outputBuffer.byteOffset, length])
const outputArray = new Int32Array(outputBuffer.buffer, outputBuffer.byteOffset, length)
free(inputBuffer)
free(outputBuffer)
return Array.from(outputArray)
}

// 检测字符串中某个字符出现的次数
const countOccurrences = (words, target = '') => {
// 传递字符串
const countOccurrences = m.cwrap("countOccurrences", "number", ["number", "number", "number"])
const array = words.split('').map(v => v.charCodeAt(0))
const length = array.length
const inputBuffer = mallocByteBuffer(length)
inputBuffer.set(array)
const counts = countOccurrences(inputBuffer.byteOffset, length, target.charCodeAt(0))
free(inputBuffer)
return counts
}

m.onRuntimeInitialized = () => {
const numbersObj = swapNumbers({ first: 4, second: 5 })
console.log(numbersObj)

const plusNumbers = plusArrays([1, 2, 3, 4, 5])
console.log(plusNumbers)

const count = countOccurrences('WebAssembly', 's')
console.log(`count=${count}`)
}

运行结果:

1
2
3
4
~ node pointer_demo.js
{ first: 5, second: 4 }
[ 2, 3, 4, 5, 6 ]
count=2

调用C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// binding.cpp
#include <emscripten.h>
#include <emscripten/bind.h>
#include <iostream>

using namespace emscripten;


class Person {
public:
Person(std::string name, int age) : name(name), age(age) {}

void plusAge() { ++age; }

int getAge() const { return age; }

void setAge(int age_) { age = age_; }

void toString() {
std::cout << "(name=" << name << ", age=" << age << ")" << std::endl;
}

static std::string getStringFromInstance(const Person &instance) {
return instance.name;
}
private:
int age;
std::string name;
};

// Binding code
EMSCRIPTEN_BINDINGS(person) {
class_<Person>("Person")
.constructor<std::string, int>()
.function("plusAge", &Person::plusAge)
.function("toString", &Person::toString)
.property("age", &Person::getAge, &Person::setAge)
.class_function("getStringFromInstance", &Person::getStringFromInstance);
}

运行 emcc --bind binding.cpp -o binding.js。这里利用了Emscriptenembind功能,它可以直接把C++里面的类或者函数映射为Javascript里面的对象。

1
2
3
4
5
6
7
8
9
10
11
12
//binding_demo.js
const m = require('./binding.js')

m.onRuntimeInitialized = () => {
const Person = m.Person
const person = new Person('larry', 20)
person.plusAge()
person.plusAge()
console.log(`person.age = ${person.age}`)
console.log(Person.getStringFromInstance(person))
person.toString()
}

执行结果

1
2
3
4
~  node binding_demo.js
person.age = 22
larry
(name=larry, age=22)

其它

更多的内容请参考Emscripten官网。

本文涉及到的样例请点击下载