面向WebAssembly编程:应用开发方法与实践
上QQ阅读APP看书,第一时间看更新

3.4 JavaScript与C/C++交换数据

3.3节介绍了内存模型和Module.HEAPX的基本用法。本节将深入讨论JavaScript与C/C++如何交换数据。

3.4.1 参数及返回值

在之前章节的例程中,我们有意忽略了一个基础性的问题:JavaScript与C/C++相互调用的时候,参数与返回值究竟是如何传递的?

答案是:JavaScript与C/C++之间只能通过number进行参数和返回值传递。

提示

JavaScript只有一种数值类型:number,即64位浮点数(IEEE 754标准)。number可以精确地表达32位及以下整型数、32位浮点数、64位浮点数(涵盖了大多数C语言的基础数据类型),这意味着JavaScript与C/C++交互时,不能使用64位整型数作为参数或返回值。5.6节将对此进行详细讨论。

从语言角度来说,JavaScript与C/C++有完全不同的数据体系,number类型是二者唯一的交集,因此本质上二者相互调用时,都是在交换number数值类型。

number数值类型从JavaScript传入C/C++有两种途径。

1)JavaScript调用带参数的C导出函数,通过参数传入number。

2)C调用由JavaScript实现的函数(见3.2节),通过注入函数的返回值传入number。

由于C/C++是强类型语言,因此对于来自JavaScript的number传入,会发生隐式类型转换。下面的例子展示了这种隐式类型转换,C代码如下:


//type_conv.cc
#include <stdio.h>

EM_PORT_API(void) print_int(int a) {
    printf("C{print_int() a:%d}\n", a);
}

EM_PORT_API(void) print_float(float a) {
    printf("C{print_float() a:%f}\n", a);
}

EM_PORT_API(void) print_double(double a) {
    printf("C{print_double() a:%lf}\n", a);
}

JavaScript代码如下:


//type_conv.html
  Module._print_int(3.4);
  Module._print_int(4.6);
  Module._print_int(-3.4);
  Module._print_int(-4.6);
  Module._print_float(2000000.03125);
  Module._print_double(2000000.03125);

浏览页面,我们发现控制台输出如图3-5所示。

图3-5 Number传入C/C++后发生隐式类型转换

可见number传入时,若目标数据类型为int,将执行向0取整;若目标数据类型为float,类型转换时有可能损失精度。

3.4.2 通过内存交换数据

当需要在JavaScript与C/C++之间交换大块的数据时,直接使用参数传递数据显然不可行,此时可以通过内存来交换数据。下面的例子展示了在JavaScript环境调用C函数在内存中生成斐波那契数列后的输出,C代码如下:


//fibonacci.cc
#include <stdio.h>
#include <malloc.h>

EM_PORT_API(int*) fibonacci(int count) {
    if (count <= 0) return NULL;

    int* re = (int*)malloc(count * 4);
    if (NULL == re) {
        printf("Not enough memory.\n");
        return NULL;
    }

    re[0] = 1;
    int i0 = 0, i1 = 1;
    for (int i = 1; i < count; i++){
        re[i] = i0 + i1;
        i0 = i1;
        i1 = re[i];
    }

    return re;
}

EM_PORT_API(void) free_buf(void* buf) {
    free(buf);
}

JavaScript代码如下:


//fibonacci.html
  var ptr = Module._fibonacci(10);
  if (ptr == 0) return;
  var str = '';
  for (var i = 0; i < 10; i++){
    str += Module.HEAP32[(ptr >> 2) + i];
    str += ' ';
    }
    console.log(str);
    Module._free_buf(ptr);

浏览页面,我们可以看到控制台输出如图3-6所示。

图3-6 JavaScript通过Module.HEAP访问C/C++堆

提示

在上述例子中,C函数fibonacci()在堆上分配了空间,在JavaScript中调用后需要调用free_buf()将其释放,以免内存泄漏。

注意

Module.HEAP32等对象的名称虽然为“堆”(HEAP),但事实上它们指的是C/C++环境的整个内存空间,因此位于C/C++栈上的数据也可以通过Module.HEAP32等对象来访问。

下面的例子展示了在JavaScript中访问C/C++栈上的数据,C代码如下:


//fib_stack.cc
EM_PORT_API (void) js_print_fib(int* ptr, int count);

EM_PORT_API(void) fibonacci20() {
    static const int count = 20;
    int re[count];

    re[0] = 1;
    int i0 = 0, i1 = 1;
    for (int i = 1; i < count; i++){
        re[i] = i0 + i1;
        i0 = i1;
        i1 = re[i];
    }

    js_print_fib(re, count);
}

C函数fibonacci10()在栈上生成了斐波那契数列的前20项,然后调用JavaScript注入函数js_print_fib()将其打印输出。JavaScript注入函数js_print_fib()的实现代码如下:


//fib_stack_pkg.js
mergeInto(LibraryManager.library, {
    js_print_fib: function (ptr, count) {
        var str = 'js_print_fib: ';
        for (var i = 0; i < count; i++){
            str += Module.HEAP32[(ptr >> 2) + i];
            str += ' ';
        }
        console.log(str);
    }
})

使用以下命令编译得到fib_stack.js/fib_stack.wasm:


emcc fib_stack.cc --js-library fib_stack_pkg.js -o fib_stack.js

在网页中调用fibonacci20()函数:


//fib_stack.html
<script>
Module = {};
Module.onRuntimeInitialized = function() {
  Module._fibonacci20();
}
</script>
<script src="fib_stack.js"></script>

浏览页面,我们可以看到控制台输出如图3-7所示。

图3-7 JavaScript通过Module.HEAP访问C/C++栈

3.4.3 在JavaScript中分配内存

3.4.2节给出的例子都是在C/C++环境中分配内存,在JavaScript中读取。有时候,JavaScript需要将大数据块送入C/C++环境,而C/C++无法预知数据块的大小,此时可以在JavaScript中分配内存并装入数据,然后将数据指针传入,调用C函数进行处理。

这种方法之所以可行,核心原因在于:Emscripten导出了C的malloc()/free()函数。下面的例子展示了如何在JavaScript中分配内存并传入数据供C/C++环境使用。

C函数sum()求传入的int数组的各项之和,代码如下:


//sum.cc
EM_PORT_API(int) sum(int* ptr, int count) {
    int total = 0;
    for (int i = 0; i < count; i++){
        total += ptr[i];
    }
    return total;
}

JavaScript分配了内存,并存入自然数列的前50项,然后调用C函数sum()求数列的和,代码如下:


//js_alloc_mem.html
  var count = 50;
  var ptr = Module._malloc(4 * count);
  for (var i = 0; i < count; i++){
    Module.HEAP32[ptr / 4 + i] = i + 1;
  }
  console.log(Module._sum(ptr, count));
  Module._free(ptr);

浏览网页后,控制台将输出:


1275

提示

C/C++的内存没有GC机制,在JavaScript中使用malloc()函数分配内存结束后,别忘了使用free()函数将其释放。

3.4.4 字符串

字符串是极为常用的数据类型,然而C/C++中的字符串表达方式(0值标志结尾)与JavaScript完全不兼容。幸运的是,Emscripten提供了一组辅助函数用于二者的转换。下面介绍较为常用的两个辅助函数。

1. UTF8ToString()

该函数可以将C/C++的字符串转换为JavaScript字符串。例如,C函数get_string()返回一个字符串的地址:


//strings.cc
EM_PORT_API(const char*) get_string() {
    static const char str[] = "Hello, wolrd! 你好,世界!"
    return str;
}

在JavaScript中获取该字符串地址,并通过UTF8ToString()将其转换为JavaScript字符串:


//strings.html
  var ptr = Module._get_string();
  var str = Pointer_stringify(ptr);
  console.log(typeof(str));
  console.log(str);

浏览网页后,控制台将输出:


string
Hello, wolrd! 你好,世界!

2. allocateUTF8()

该函数将在C/C++内存中分配足够大的空间,并将字符串按UTF8格式复制到分配的内存中。例如,在JavaScript中使用allocateUTF8()将字符串传入C/C++内存,然后调用C函数print_string()打印。JavaScript代码部分如下:


//strings.html
  ptr = allocateUTF8("你好,Emscripten!");
  Module._print_string(ptr);
  _free(ptr);

C代码部分如下:


//strings.cc
EM_PORT_API(void) print_string(char* str) {
    printf("%s\n", str);
}

浏览网页后,控制台将输出:


你好,Emscripten!

此外,Emscripten还提供了AsciiToString()/stringToAscii()/UTF8ArrayToString()/stringToUTF8Array()等一系列辅助函数来处理各种格式的字符串在不同存储对象中的转换。