彻底弄懂Js和Wasm间各种数据类型的传递

我们在使用wasm进行前端应用开发的过程中,可能会遇到各种各样数据的传递问题,比如js向wasm传递数据,wasm向js返回数据等等,本文将从从最简单的数字类型开始一步一步深入彻底弄懂js和wasm间的各类数据该怎么传递和获取。

一、调用wasm中导出方法的函数

在js中,我们可以通过以下五种方式调用wasm中导出的方法:

1、_函数名

2、Module.ccall

const ret = Module.ccall(ident, returnType, argTypes, args);

参数释义如下:

ident:C导出函数的函数名(不含“_”下划线前缀);

returnType:C导出函数的返回值类型,可以为'boolean'、'number'、'string'、'null',分别表示函数返回值为布尔值、数值、字符串、无返回值;

argTypes:C导出函数的参数类型的数组。参数类型可以为'number'、'string'、'array',分别代表数值、字符串、数组;

args:参数数组;

使用方式:

const ret = Module.ccall('add', 'number', ['number', 'number'], [1, 2]);

Emscripten从v1.38开始,ccall/cwrap辅助函数默认没有导出,在编译时需要通过-s EXPORTED_RUNTIME_METHODS="['ccall', 'cwrap']"选项显式导出。

3、Module.cwrap

const func = Module.cwrap(ident, returnType, argTypes);
const ret = func(args);

参数释义如下:

ident:C导出函数的函数名(不含“_”下划线前缀);

returnType:C导出函数的返回值类型,可以为'boolean'、'number'、'string'、'null',分别表示函数返回值为布尔值、数值、字符串、无返回值;

argTypes:C导出函数的参数类型的数组。参数类型可以为'number'、'string'、'array',分别代表数值、字符串、数组;

返回值:函数

使用方式:

const func = Module.cwrap('add', 'number', ['number', 'number']);
const ret = func(1, 2);

Emscripten从v1.38开始,ccall/cwrap辅助函数默认没有导出,在编译时需要通过-s EXPORTED_RUNTIME_METHODS="['ccall', 'cwrap']"选项显式导出。

4、Module._函数名

5、Module.asm.函数名

上面我们了解了js调用wasm导出方法的5种方式,接下来我们开始介绍js和wasm间各种数据类型的传递。

二、数字类型的传递

C函数定义:

int add(int n1, int n2) {
    int ret = n1 + n2;
    return ret;
}

C函数传递数字:

const r1 = _add(1, 2); // 3
const r2 = Module._add(1, 2); // 3
const r3 = Module.ccall('add', 'number', ['number', 'number'], [1, 2]); // 3
const r4 = Module.cwrap('add', 'number', ['number', 'number'])(1, 2); // 3

获取C函数返回的数字:

由于返回的数字是默认支持的类型,r1r2r3r4即为结果。

三、字符串类型的传递

C函数定义:

#include <string.h>
#include <malloc.h>

char * concat_str(char *s1, char *s2) {
    char *ret = (char *)malloc((strlen(s1) + strlen(s2)) * sizeof(char));
    strcat(ret, s1);
    strcat(ret, s2);
    return ret;
}

向C函数内传递字符串

const r1 = Module.ccall('concat_str', 'string', ['string', 'string'], ['Hello ', 'World']); // Hello World
const r2 = Module.cwrap('concat_str', 'string', ['string', 'string'])('Hello ', 'World'); // Hello World

获取C函数返回的字符串:

由于返回的数字是ccallcwrap默认支持的类型,r1r2即为结果。

上面没有直接使用函数名去调用C函数,因为不支持将字符串通过函数调用的方式直接传递给C函数,和我们预期是不一样的,感兴趣的可以去试一下,但是该如何使用函数名调用C函数呢,这里简单演示一下,作为后面数组传递的基础。

C函数传递字符串

const s1 = 'Hello ';
const s2 = 'World';
const s1Arr = new TextEncoder().encode(s1);
const buf1 = Module._malloc(s1Arr.byteLength);
Module.HEAPU8.set(s1Arr, buf1);
const s2Arr = new TextEncoder().encode(s2);
const buf2 = Module._malloc(s2Arr.byteLength);
Module.HEAPU8.set(s2Arr, buf2);
const r1 = _concat_str(buf1, buf2); // 地址

这里我们将字符串s1s2转为类型化数组Uint8Array,然后通过wasm提供的内存分配函数申请了内存,并放入了内存中,数据放到内存后将分配内存后得到的地址传递给C函数_concat_str即可。

获取C函数返回的字符串:

这里需要注意的是返回结果r1并不是预期的字符串而是一个内存地址,我们需要根据内存地址去获取返回结果:

const memoryView = new Uint8Array(Module.asm.memory.buffer);
const arr = [];
let i = r1;
while (memoryView[i] !== 0) {
    arr.push(memoryView[i]);
    i ++;
}
const ret = new TextDecoder().decode(new Uint8Array(arr)); 
console.log(ret); // Hello World

四、一维纯数字数组类型的传递

C函数定义:

int * mapArray(uint8_t *arr, int arrSize) {
    int *ret = (int *)malloc(arrSize * sizeof(int));
    for (int i=0; i<arrSize; i++) {
        ret[i] = arr[i] * 2;
    }
    return ret;
}

C函数传递一维数组:

const arr = [1, 3, 2, 5, 4];
const r1 = Module.ccall('mapArray', 'number', ['array', 'number'], [arr, arr.length]); // 地址
const r2 = Module.cwrap('mapArray', 'number', ['array', 'number'])(arr, arr.length); // 地址

如果想通过函数名直接向C函数传递数据,可以参考上面通过函数名直接向C函数传递字符串的方法。

获取C函数返回的一维数组:

const { BYTES_PER_ELEMENT } = Float32Array;
const pointer = r1;
const data = new Float32Array(Module.asm.memory.buffer, pointer, arr.length);
const ret = Array.from(data);
console.log(ret); // [2, 6, 4, 10, 8]

这里的做法和上面获取直接调用函数名返回的字符串基本类似,但是没有字符串灵活,我们通过\0字符来判断字符串的结束进而拿到整个字符串而不需要提前知道返回字符串的长度,但是对于数组的话我们需要提前知道返回的数组的大小,才能准确的从内存中拿到返回结果。

注意:C语言中int类型数组对应Uint32Arrayfloat类型数组对应Float32Arraydouble类型数组对应Float64Array,在获取结果时一定要使用正确的类型化数组,否则获取的结果将是错误的。

五、二维纯数字数组类型的传递

C函数定义:

double ** mapArray2(uint8_t *arr, int row, int col) {
    static double **ret;
    ret = (double **)malloc(row * col * sizeof(double *));
    for (int i=0; i<row; i++) {
        ret[i] = (double *)malloc(col * sizeof(double));
        for (int j=0; j<col; j++) {
            ret[i][j] = arr[i * row + j] * 2;
            printf("%d:%d=%f\n", i, j, ret[i][j]);
        }
    }
    return ret;
}

向C函数传递二维数组:

由于 C 编写的 WASM 模块只能接受一维数组,因此需要将二维数组转换为一维数组然后再传递。

const arr = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];
const r1 = Module.ccall('mapArray2', 'number', ['array', 'number', 'number'], [arr.flat(), arr.length, arr[0].length]); // 地址
const r2 = Module.cwrap('mapArray2', 'number', ['array', 'number', 'number'])(arr.flat(), arr.length, arr[0].length); // 地址

获取C函数返回的二维数组:

这里就会麻烦一点,因为二维数组每一行对应的数据都存在一片单独的内存上,而且不一定是连续的,所以我们需要将每一行的起始内存地址返回,我们稍微修改一下C代码:

double **ret;

double * getLineAddress(int row) {
    return ret[row];
}

double ** mapArray2(uint8_t *arr, int row, int col) {
    ret = (double **)malloc(row * col * sizeof(double *));
    for (int i=0; i<row; i++) {
        ret[i] = (double *)malloc(col * sizeof(double));
        for (int j=0; j<col; j++) {
            ret[i][j] = arr[i * row + j] * 2;
        }
    }
    return ret;
}

在全局定义结果变量,然后提供一个获取每行地址的方法:getLineAddress

然后使用js获取C函数返回的结果:

const { BYTES_PER_ELEMENT } = Float64Array;
const ret = [];
for (let i=0; i<arr.length; i++) {
    const pointer = _getLineAddress(i);
    const row = new Float64Array(Module.asm.memory.buffer, pointer, arr[0].length);
    ret.push(Array.from(row));
}
console.log(ret); // [[2, 4, 6], [,8, 10, 12],[14, 16, 18]]

六、字符串数组的传递

C函数定义

char ** mapStringArray(char **strings, int length) {
  for (int i = 0; i < length; i++) {
    printf("%s\n", strings[i]);
  }
  return strings;
}

向C函数传递二维数组:

const strings = ['Hello', 'world', '!'];

// 将字符串存到内存中并获得字符串指针
const pointers = strings.map((str) => {
  const buf = Module._malloc(str.length + 1);
  Module.writeStringToMemory(str, buf);
  return buf;
});

// 非常重要,将内存中存储的字符串重新存储到连续的内存
const STRING_BYTE = 4;
const ptr = Module._malloc(pointers.length * STRING_BYTE);
for (let i = 0; i < pointers.length; i++) {
  Module.setValue(ptr + i * STRING_BYTE, pointers[i], 'i32');
}

const pointer = Module.ccall('mapStringArray', 'null', ['number', 'number'], [ptr, strings.length]);

在上面的方法中,我们首先将字符串存到内存中并得到了字符串指针数组,然后将字符串重新存储到了连续的内存中,这一步非常重要,直接影响C函数是否能正常取到字符串的值,需要注意C语言存储字符串时每个字符占4字节。

获取C函数返回的字符串数组:

const ret = [];
for (let i=0; i<strings.length; i++) {
    const ptr = Module.getValue(pointer + i * 4, 'i32');
    if (ptr === 0) {
        break;
    }
    const str = Module.UTF8ToString(ptr);
    ret.push(str);
}
console.log(ret); // ['Hello', 'world', '!']

注意:上面使用的writeStringToMemorysetValuegetValueUTF8ToString需要通过-s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap", "writeAsciiToMemory", "writeStringToMemory", "setValue", "getValue", "UTF8ToString"]' 导出。

不管是一维数组还是多位数组我们都需要提前知道返回数组的大小才能正确取得数据,如果输入的数组大小和输出的数组大小不一致时就很难正确的去除数据,所以我感觉C函数直接使用数组结构返回结果不是太灵活,其实我们可以换一种思路,比如将二维数组转换成字符串传递,接收到数据后再解析,类似下面这样:

2,3,1,2,3,4,5,6

通过上面的字符串描述一个2 * 3的二维数组,然后我们将其转换成真实的数组即可。

js的转换方法:

function str2Arr(str) {
    const arr = str.split(',')
        .map(m => Number(m));
    const ret = [];
    const row = arr[0];
    const col = arr[1];
    let index = 2;
    for (let i=0; i<row; i++) {
        ret[i] = [];
        for(let j=0; j<col; j++) {
            ret[i][j] = arr[index++];
        }
    }
    return ret;
}

str2Arr('2,3,1,2,3,4,5,6');

C语言的转换方法:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int * split_int(char *str, char *sep) {
    static int ret[100];
    char *token;
    token = strtok(str, sep);
    int i = 0;
    while (token != NULL) {
        ret[i++] = atoi(token);
        token = strtok(NULL, sep);
    }
    return ret;
}

int ** str_2_matrix(char *str, int *r, int *c) {
    int *arr = split_int(str, ",");
    int row = arr[0];
    int col = arr[1];
    if (r != NULL) {
        *r = row;
    }
    if (c != NULL) {
        *c = col;
    }
    int index = 2;
    static int **ret;
    ret = (int **)malloc(row * sizeof(int *));
    for (int i=0; i<row; i++) {
        ret[i] = (int *)malloc(col * sizeof(int));
        for (int j=0; j<col; j++) {
            ret[i][j] = arr[index++];
        }
    }
    return ret;
}

int main() {
    char s[] = "2,3,1,2,3,4,5,6";
    int r, c;
    int **ret = str_2_matrix(s, &r, &c);
    for (int i=0; i<r; i++) {
        for (int j=0; j<c; j++) {
            printf("%6d", ret[i][j]);
        }
        printf("\n");
    }
    return 0;
}

本文到此就结束了,希望大家在对接wasm应用的过程中不再迷茫。

系列文章导航:

第1节: 用canvas实现弹性挂件

第2节: Javascript实现寻径算法

第3节: Javascript拖拽拼图

第4节: 函数节流的实际应用

第5节: js简单实现依赖注入

第6节: Canvas实现微信红包图片

第7节: js懒加载的实现原理

第8节: js按位运算符及其妙用

第9节: Javascript操作Cookie

第10节: js平面旋转

  • 支付宝二维码 支付宝
  • 微信二维码 微信
相关文章