Files
Knowledge/0 编程/Js 和 Wasm间各种数据类型的传递_23013RK75C_Dec-26-2235-2024_1.md

217 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
**一、调用wasm中导出方法的函数**
在js中,我们可以通过以下五种方式调用wasm中导出的方法:
1、**_函数名**
2、**Module.ccall**
| | |
| --- | -------------------------------------------------------------- |
| 1 | `const ret = Module.ccall(ident, returnType, argTypes, args);` |
参数释义如下:
`ident`:C导出函数的函数名(不含“_”下划线前缀);
`returnType`:C导出函数的返回值类型,可以为'`boolean`'、'`number`'、'`string`'、'`null`',分别表示函数返回值为布尔值、数值、字符串、无返回值;
`argTypes`:C导出函数的参数类型的数组。参数类型可以为'`number`'、'`string`'、'`array`',分别代表数值、字符串、数组;
`args`:参数数组;
使用方式:
| | |
|---|---|
|1|`const ret = Module.ccall('add', 'number', ['number', 'number'], [1, 2]);`|
**Emscripten从**`v1.38`**开始,**`ccall/cwrap`**辅助函数默认没有导出,在编译时需要通过**`-s EXPORTED_RUNTIME_METHODS="['ccall', 'cwrap']"`**选项显式导出。**
3、**Module.cwrap**
| | |
|---|---|
|1<br><br>2|`const func = Module.cwrap(ident, returnType, argTypes);`<br><br>`const ret = func(args);`|
参数释义如下:
`ident`:C导出函数的函数名(不含“_”下划线前缀);
`returnType`:C导出函数的返回值类型,可以为'`boolean`'、'`number`'、'`string`'、'`null`',分别表示函数返回值为布尔值、数值、字符串、无返回值;
`argTypes`:C导出函数的参数类型的数组。参数类型可以为'`number`'、'`string`'、'`array`',分别代表数值、字符串、数组;
返回值:函数
使用方式:
| | |
|---|---|
|1<br><br>2|`const func = Module.cwrap('add', 'number', ['number', 'number']);`<br><br>`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函数定义:**
| | |
|---|---|
|1<br><br>2<br><br>3<br><br>4|`int add(int n1, int n2) {`<br><br>    `int ret = n1 + n2;`<br><br>    `return ret;`<br><br>`}`|
**向**C函数**传递数字:**
| | |
|---|---|
|1<br><br>2<br><br>3<br><br>4|`const r1 = _add(1, 2); // 3`<br><br>`const r2 = Module._add(1, 2); // 3`<br><br>`const r3 = Module.ccall('add', 'number', ['number', 'number'], [1, 2]); // 3`<br><br>`const r4 = Module.cwrap('add', 'number', ['number', 'number'])(1, 2); // 3`|
**获取**C函数**返回的数字:**
由于返回的数字是默认支持的类型,`r1`,`r2`,`r3`,`r4`即为结果。
**三、字符串类型的传递**
**C函数定义:**
| | |
|---|---|
|1<br><br>2<br><br>3<br><br>4<br><br>5<br><br>6<br><br>7<br><br>8<br><br>9|`#include <string.h>`<br><br>`#include <malloc.h>`<br><br>`char * concat_str(char *s1, char *s2) {`<br><br>    `char *ret = (char *)malloc((strlen(s1) + strlen(s2)) * sizeof(char));`<br><br>    `strcat(ret, s1);`<br><br>    `strcat(ret, s2);`<br><br>    `return ret;`<br><br>`}`|
**向C函数内传递**字符串**:**
| | |
|---|---|
|1<br><br>2|`const r1 = Module.ccall('concat_str', 'string', ['string', 'string'], ['Hello ', 'World']); // Hello World`<br><br>`const r2 = Module.cwrap('concat_str', 'string', ['string', 'string'])('Hello ', 'World'); // Hello World`|
**获取**C函数**返回的字符串:**
由于返回的数字是`ccall`,`cwrap`默认支持的类型,`r1`,`r2`即为结果。
上面没有直接使用函数名去调用C函数,因为不支持将字符串通过函数调用的方式直接传递给C函数,和我们预期是不一样的,感兴趣的可以去试一下,但是该如何使用函数名调用C函数呢,这里简单演示一下,作为后面数组传递的基础。
**向**C函数**传递**字符串**:**
| | |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1<br><br>2<br><br>3<br><br>4<br><br>5<br><br>6<br><br>7<br><br>8<br><br>9 | `const s1 = 'Hello ';`<br><br>`const s2 = 'World';`<br><br>`const s1Arr = new TextEncoder().encode(s1);`<br><br>`const buf1 = Module._malloc(s1Arr.byteLength);`<br><br>`Module.HEAPU8.set(s1Arr, buf1);`<br><br>`const s2Arr = new TextEncoder().encode(s2);`<br><br>`const buf2 = Module._malloc(s2Arr.byteLength);`<br><br>`Module.HEAPU8.set(s2Arr, buf2);`<br><br>`const r1 = _concat_str(buf1, buf2); // 地址` |
这里我们将字符串`s1`和`s2`转为类型化数组`Uint8Array`,然后通过wasm提供的内存分配函数申请了内存,并放入了内存中,数据放到内存后将分配内存后得到的地址传递给C函数`_concat_str`即可。
**获取C函数返回的字符串:**
这里需要注意的是返回结果`r1`并不是预期的字符串而是一个内存地址,我们需要根据内存地址去获取返回结果:
| | |
|---|---|
|1<br><br>2<br><br>3<br><br>4<br><br>5<br><br>6<br><br>7<br><br>8<br><br>9|`const memoryView = new Uint8Array(Module.asm.memory.buffer);`<br><br>`const arr = [];`<br><br>`let i = r1;`<br><br>`while (memoryView[i] !== 0) {`<br><br>    `arr.push(memoryView[i]);`<br><br>    `i ++;`<br><br>`}`<br><br>`const ret = new TextDecoder().decode(new Uint8Array(arr));`<br><br>`console.log(ret); // Hello World`|
**四、一维纯数字数组类型的传递**
**C函数定义:**
| | |
|---|---|
|1<br><br>2<br><br>3<br><br>4<br><br>5<br><br>6<br><br>7|`int * mapArray(uint8_t *arr, int arrSize) {`<br><br>    `int *ret = (int *)malloc(arrSize * sizeof(int));`<br><br>    `for (int i=0; i<arrSize; i++) {`<br><br>        `ret[i] = arr[i] * 2;`<br><br>    `}`<br><br>    `return ret;`<br><br>`}`|
**向**C函数**传递一维数组:**
| | |
|---|---|
|1<br><br>2<br><br>3|`const arr = [1, 3, 2, 5, 4];`<br><br>`const r1 = Module.ccall('mapArray', 'number', ['array', 'number'], [arr, arr.length]); // 地址`<br><br>`const r2 = Module.cwrap('mapArray', 'number', ['array', 'number'])(arr, arr.length); // 地址`|
如果想通过函数名直接向C函数传递数据,可以参考上面通过函数名直接向C函数传递字符串的方法。
**获取C函数返回的一维数组:**
| | |
|---|---|
|1<br><br>2<br><br>3<br><br>4<br><br>5|`const { BYTES_PER_ELEMENT } = Float32Array;`<br><br>`const pointer = r1;`<br><br>`const data = new Float32Array(Module.asm.memory.buffer, pointer, arr.length);`<br><br>`const ret = Array.from(data);`<br><br>`console.log(ret); // [2, 6, 4, 10, 8]`|
这里的做法和上面获取直接调用函数名返回的字符串基本类似,但是没有字符串灵活,我们通过`\0`字符来判断字符串的结束进而拿到整个字符串而不需要提前知道返回字符串的长度,但是对于数组的话我们需要提前知道返回的数组的大小,才能准确的从内存中拿到返回结果。
**注意:**C语言中`int`类型数组对应`Uint32Array`,`float`类型数组对应`Float32Array`,`double`类型数组对应`Float64Array`,在获取结果时一定要使用正确的类型化数组,否则获取的结果将是错误的。
**五、二维纯数字数组类型的传递**
**C函数定义:**
| | |
|---|---|
|01<br><br>02<br><br>03<br><br>04<br><br>05<br><br>06<br><br>07<br><br>08<br><br>09<br><br>10<br><br>11<br><br>12|`double ** mapArray2(uint8_t *arr, int row, int col) {`<br><br>    `static double **ret;`<br><br>    `ret = (double **)malloc(row * col * sizeof(double *));`<br><br>    `for (int i=0; i<row; i++) {`<br><br>        `ret[i] = (double *)malloc(col * sizeof(double));`<br><br>        `for (int j=0; j<col; j++) {`<br><br>            `ret[i][j] = arr[i * row + j] * 2;`<br><br>            `printf("%d:%d=%f\n", i, j, ret[i][j]);`<br><br>        `}`<br><br>    `}`<br><br>    `return ret;`<br><br>`}`|
**向C函数传递二维数组:**
由于 C 编写的 WASM 模块只能接受一维数组,因此需要将二维数组转换为一维数组然后再传递。
| | |
|---|---|
|1<br><br>2<br><br>3<br><br>4<br><br>5<br><br>6<br><br>7|`const arr = [`<br><br>    `[1, 2, 3],`<br><br>    `[4, 5, 6],`<br><br>    `[7, 8, 9]`<br><br>`];`<br><br>`const r1 = Module.ccall('mapArray2', 'number', ['array', 'number', 'number'], [arr.flat(), arr.length, arr[0].length]); // 地址`<br><br>`const r2 = Module.cwrap('mapArray2', 'number', ['array', 'number', 'number'])(arr.flat(), arr.length, arr[0].length); // 地址`|
**获取C函数返回的二维数组:**
这里就会麻烦一点,因为二维数组每一行对应的数据都存在一片单独的内存上,而且不一定是连续的,所以我们需要将每一行的起始内存地址返回,我们稍微修改一下C代码:
| | |
|---|---|
|01<br><br>02<br><br>03<br><br>04<br><br>05<br><br>06<br><br>07<br><br>08<br><br>09<br><br>10<br><br>11<br><br>12<br><br>13<br><br>14<br><br>15<br><br>16|`double **ret;`<br><br>`double * getLineAddress(int row) {`<br><br>    `return ret[row];`<br><br>`}`<br><br>`double ** mapArray2(uint8_t *arr, int row, int col) {`<br><br>    `ret = (double **)malloc(row * col * sizeof(double *));`<br><br>    `for (int i=0; i<row; i++) {`<br><br>        `ret[i] = (double *)malloc(col * sizeof(double));`<br><br>        `for (int j=0; j<col; j++) {`<br><br>            `ret[i][j] = arr[i * row + j] * 2;`<br><br>        `}`<br><br>    `}`<br><br>    `return ret;`<br><br>`}`|
在全局定义结果变量,然后提供一个获取每行地址的方法:`getLineAddress`。
然后使用js获取C函数返回的结果:
| | |
|---|---|
|1<br><br>2<br><br>3<br><br>4<br><br>5<br><br>6<br><br>7<br><br>8|`const { BYTES_PER_ELEMENT } = Float64Array;`<br><br>`const ret = [];`<br><br>`for (let i=0; i<arr.length; i++) {`<br><br>    `const pointer = _getLineAddress(i);`<br><br>    `const row = new Float64Array(Module.asm.memory.buffer, pointer, arr[0].length);`<br><br>    `ret.push(Array.from(row));`<br><br>`}`<br><br>`console.log(ret); // [[2, 4, 6], [,8, 10, 12],[14, 16, 18]]`|
**六、字符串数组的传递**
**C函数定义**
| | |
|---|---|
|1<br><br>2<br><br>3<br><br>4<br><br>5<br><br>6|`char ** mapStringArray(char **strings, int length) {`<br><br>  `for (int i = 0; i < length; i++) {`<br><br>    `printf("%s\n", strings[i]);`<br><br>  `}`<br><br>  `return strings;`<br><br>`}`|
**向C函数传递二维数组:**
| | |
|---|---|
|01<br><br>02<br><br>03<br><br>04<br><br>05<br><br>06<br><br>07<br><br>08<br><br>09<br><br>10<br><br>11<br><br>12<br><br>13<br><br>14<br><br>15<br><br>16<br><br>17|`const strings = ['Hello', 'world', '!'];`<br><br>`// 将字符串存到内存中并获得字符串指针`<br><br>`const pointers = strings.map((str) => {`<br><br>  `const buf = Module._malloc(str.length + 1);`<br><br>  `Module.writeStringToMemory(str, buf);`<br><br>  `return buf;`<br><br>`});`<br><br>`// 非常重要,将内存中存储的字符串重新存储到连续的内存`<br><br>`const STRING_BYTE = 4;`<br><br>`const ptr = Module._malloc(pointers.length * STRING_BYTE);`<br><br>`for (let i = 0; i < pointers.length; i++) {`<br><br>  `Module.setValue(ptr + i * STRING_BYTE, pointers[i], 'i32');`<br><br>`}`<br><br>`const pointer = Module.ccall('mapStringArray', 'null', ['number', 'number'], [ptr, strings.length]);`|
在上面的方法中,我们首先将字符串存到内存中并得到了`字符串指针数组`,然后将字符串重新存储到了`连续的内存`中,这一步非常重要,直接影响C函数是否能正常取到字符串的值,需要注意C语言存储字符串时每个字符占4字节。
**获取C函数返回的字符串数组:**
| | |
|---|---|
|01<br><br>02<br><br>03<br><br>04<br><br>05<br><br>06<br><br>07<br><br>08<br><br>09<br><br>10|`const ret = [];`<br><br>`for (let i=0; i<strings.length; i++) {`<br><br>    `const ptr = Module.getValue(pointer + i * 4, 'i32');`<br><br>    `if (ptr === 0) {`<br><br>        `break;`<br><br>    `}`<br><br>    `const str = Module.UTF8ToString(ptr);`<br><br>    `ret.push(str);`<br><br>`}`<br><br>`console.log(ret); // ['Hello', 'world', '!']`|
**注意:**上面使用的`writeStringToMemory`,`setValue`,`getValue`,`UTF8ToString`需要通过`-s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap", "writeAsciiToMemory", "writeStringToMemory", "setValue", "getValue", "UTF8ToString"]'` 导出。
不管是一维数组还是多位数组我们都需要提前知道返回数组的大小才能正确取得数据,如果输入的数组大小和输出的数组大小不一致时就很难正确的去除数据,所以我感觉C函数直接使用数组结构返回结果不是太灵活,其实我们可以换一种思路,比如将二维数组转换成字符串传递,接收到数据后再解析,类似下面这样:
| | |
|---|---|
|1|`2,3,1,2,3,4,5,6`|
通过上面的字符串描述一个`2 * 3`的二维数组,然后我们将其转换成真实的数组即可。
js的转换方法:
| | |
|---|---|
|01<br><br>02<br><br>03<br><br>04<br><br>05<br><br>06<br><br>07<br><br>08<br><br>09<br><br>10<br><br>11<br><br>12<br><br>13<br><br>14<br><br>15<br><br>16<br><br>17|`function str2Arr(str) {`<br><br>    `const arr = str.split(',')`<br><br>        `.map(m => Number(m));`<br><br>    `const ret = [];`<br><br>    `const row = arr[0];`<br><br>    `const col = arr[1];`<br><br>    `let index = 2;`<br><br>    `for (let i=0; i<row; i++) {`<br><br>        `ret[i] = [];`<br><br>        `for(let j=0; j<col; j++) {`<br><br>            `ret[i][j] = arr[index++];`<br><br>        `}`<br><br>    `}`<br><br>    `return ret;`<br><br>`}`<br><br>`str2Arr('2,3,1,2,3,4,5,6');`|
C语言的转换方法:
| | |
|---|---|
|01<br><br>02<br><br>03<br><br>04<br><br>05<br><br>06<br><br>07<br><br>08<br><br>09<br><br>10<br><br>11<br><br>12<br><br>13<br><br>14<br><br>15<br><br>16<br><br>17<br><br>18<br><br>19<br><br>20<br><br>21<br><br>22<br><br>23<br><br>24<br><br>25<br><br>26<br><br>27<br><br>28<br><br>29<br><br>30<br><br>31<br><br>32<br><br>33<br><br>34<br><br>35<br><br>36<br><br>37<br><br>38<br><br>39<br><br>40<br><br>41<br><br>42<br><br>43<br><br>44<br><br>45<br><br>46<br><br>47<br><br>48<br><br>49<br><br>50|`#include <stdio.h>`<br><br>`#include <string.h>`<br><br>`#include <stdlib.h>`<br><br>`int * split_int(char *str, char *sep) {`<br><br>    `static int ret[100];`<br><br>    `char *token;`<br><br>    `token = strtok(str, sep);`<br><br>    `int i = 0;`<br><br>    `while (token != NULL) {`<br><br>        `ret[i++] = atoi(token);`<br><br>        `token = strtok(NULL, sep);`<br><br>    `}`<br><br>    `return ret;`<br><br>`}`<br><br>`int ** str_2_matrix(char *str, int *r, int *c) {`<br><br>    `int *arr = split_int(str, ",");`<br><br>    `int row = arr[0];`<br><br>    `int col = arr[1];`<br><br>    `if (r != NULL) {`<br><br>        `*r = row;`<br><br>    `}`<br><br>    `if (c != NULL) {`<br><br>        `*c = col;`<br><br>    `}`<br><br>    `int index = 2;`<br><br>    `static int **ret;`<br><br>    `ret = (int **)malloc(row * sizeof(int *));`<br><br>    `for (int i=0; i<row; i++) {`<br><br>        `ret[i] = (int *)malloc(col * sizeof(int));`<br><br>        `for (int j=0; j<col; j++) {`<br><br>            `ret[i][j] = arr[index++];`<br><br>        `}`<br><br>    `}`<br><br>    `return ret;`<br><br>`}`<br><br>`int main() {`<br><br>    `char s[] = "2,3,1,2,3,4,5,6";`<br><br>    `int r, c;`<br><br>    `int **ret = str_2_matrix(s, &r, &c);`<br><br>    `for (int i=0; i<r; i++) {`<br><br>        `for (int j=0; j<c; j++) {`<br><br>            `printf("%6d", ret[i][j]);`<br><br>        `}`<br><br>        `printf("\n");`<br><br>    `}`<br><br>    `return 0;`<br><br>`}`|
本文到此就结束了,希望大家在对接wasm应用的过程中不再迷茫。