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进行安装,更为便捷。

~ 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

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

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

├── hello.c
├── hello.html
├── hello.js
└── hello.wasm

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

hell world

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

~ emcc hello.c -o hello.js
~ node hello.js
hello world

函数调用

// 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

<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)。前者返回一个函数,后者直接 返回执行结果

传递指针

// 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

// 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}`)
}

运行结果:

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

调用C++

// 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里面的对象。

//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()
}

执行结果

~  node binding_demo.js
person.age = 22
larry
(name=larry, age=22)

其它

更多的内容请参考Emscripten官网。本文涉及到的样例请点击下载


文章作者: Asyncoder
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Asyncoder !
  目录