本文讨论如何在编译为WebAssembly模块后的C/C++程序和js之间进行数据交换。本质上jsWebAssembly共享相同的线性内存,这意味着jsWebAssembly可以在同一内存位置读写数据。

js读取c/c++全局变量

编译后全局变量已经分配好内存,所以可以通过共享线性内存的偏移进行读写。

#ifdef __EMSCRIPTEN__

#include <emscripten.h>

#endif

int g_int = 52;
double g_double = 3.1415926;

#ifdef __cplusplus
extern "C" {
#endif

#ifdef __EMSCRIPTEN__
EMSCRIPTEN_KEEPALIVE
#endif
int* get_int_ptr() {
    return &g_int;
}

#ifdef __EMSCRIPTEN__
EMSCRIPTEN_KEEPALIVE
#endif
double* get_double_ptr() {
    return &g_double;
}

#ifdef __cplusplus
}
#endif

上面的代码定义了两个全局变量,同时定义了两个函数分别取出相应的地址。由于有全局变量,js需要在示例化模块时分配内存:

const memory = new WebAssembly.Memory({initial: 1});

有个小细节需要注意,内存地址是按字节做基本单位来分配的,js根据不同的类型数组,取到的内存地址要做不同的换算,比如按Int32Array读取共享线性内存时,取到的变量地址要除以4,因为Int32ArrayArrayBuffer转成了元素占用4个字节的数组。如果用Uint8Array则不用做换算:

// 每个元素占1个字节
let buffer = new Uint8Array(res.instance.exports.memory.buffer);
console.log('buffer-value::', buffer[exports.get_int_ptr()]);

// 每个元素占4个字节
let uint32 = new Uint32Array(memory.buffer);
console.log('g_int::', uint32[exports.get_int_ptr() >> 2]);

这里我就用Int32Array做示例:

 const memory = new WebAssembly.Memory({initial: 1});
    WebAssembly.instantiateStreaming(fetch("test.wasm"), {
      env: {
        memory: memory,
        segfault: function() {
          // 处理segfault事件
        },
        alignfault: function() {
          // 处理alignfault事件
        }
      },
      wasi_snapshot_preview1: {
        fd_write: function() {
          // 处理fd_write事件
        }
      }
    }).then(res => {
      console.log('res::', res);
      const exports = res.instance.exports
      const memory = exports.memory;

      const HEAP32 = new Int32Array(memory.buffer);
      const HEAPF64 = new Float64Array(memory.buffer);

      const int_ptr = exports.get_int_ptr();
      let int_value = HEAP32[int_ptr >> 2]
      console.log('int_value::', int_value);

      const double_ptr = exports.get_double_ptr();
      let double_value = HEAPF64[double_ptr >> 3];
      console.log('double_value::', double_value);

      console.log('int_value + double_value = ', int_value + double_value);

      // 修改值
      HEAP32[int_ptr >> 2] = 100;
      HEAPF64[double_ptr >> 3] = 3.14;

      int_value = HEAP32[int_ptr >> 2];
      double_value = HEAPF64[double_ptr >> 3];
      console.log('int_value + double_value = ', int_value + double_value);

c/c++读取js分配的内存

要在js分配内存, 那么emcc编译代码时不能带上SIDE_MODULE配置,编译时需要加上-s EXPORTED_FUNCTIONS="['_malloc', '_free']"

下面是一段从js读取的数组,返回求和的函数:

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
// #include <stdlib.h>

#ifdef __EMSCRIPTEN__
EMSCRIPTEN_KEEPALIVE
#endif
int sum(int* ptr, int length) {
    int total = 0;
    for(int i = 0; i < length; i++){
        total += ptr[i];
    }
    return total;
}

js通过malloc分配内存时,默认也是按一个字节分组,所以也要做对应的换算,代码如下:

const memory = new WebAssembly.Memory({initial: 1});
        const table = new WebAssembly.Table({
            initial: 0,
            maximum: 0,
            element: 'anyfunc' // 表示可以存储任何函数类型
        });

        WebAssembly.instantiateStreaming(fetch('sum.wasm'), {
            env: {
                memory: memory,
                __table_base: 1024,
                __memory_base: 1024,
                __indirect_function_table: table,
            },
        }).then(res => {
            console.log('res::', res);
            const exports = res.instance.exports;
            const memory = exports.memory;
            const HEAP32 = new Int32Array(memory.buffer);

            const count = 50;

            const ptr = exports.malloc(count * 4);
            console.log('ptr::', ptr);

            for(let i = 0; i < count; i++) {
                HEAP32[(ptr >> 2) + i] = i + 1;
            }

            for(let i = 0; i < count; i++) {
                console.log(HEAP32[(ptr >> 2) + i]);
            }

            console.log('sum::', exports.sum(ptr, count));

            exports.free(ptr);

        })

总的来说,本文为希望在Web环境中利用C/C++的强大功能并启用与js通信的开发者提供了有用的指南。