
3.7 ccall()/cwrap()
3.4节提到,JavaScript调用C/C++时只能使用number类型作为参数,因此如果参数是字符串、数组等非number类型,则需要拆分为以下步骤。
1)使用Module._malloc()函数在Module堆中分配内存,获取地址ptr。
2)将字符串/数组等数据复制到内存的ptr处。
3)将ptr作为参数,调用C/C++函数进行处理。
4)使用Module._free()释放ptr。
由此可见,调用过程相当烦琐,尤其当非number类型参数个数较多时,JavaScript侧的调用代码会急剧膨胀。为了简化调用过程,Emscripten提供了ccall()/cwrap()封装函数。本节将介绍这两个封装函数。
3.7.1 ccall()
ccall()用于在JavaScript中调用导出的C函数,该方法会自动完成类型为字符串、数组的参数及返回值的转换及传递。
函数声明:var result = Module.ccall(ident, returnType, argTypes, args)
参数:
·ident,C导出函数的函数名(不含“_”下划线前缀)。
·returnType,C导出函数的返回值类型,可以为boolean、number、string、null,分别表示函数返回值为布尔值、数值、字符串、无返回值。
·argTypes,C导出函数的参数类型的数组。参数类型可以为number、string、array,分别表示数值、字符串、数组。
·args,参数数组。
例如,C导出函数如下:
//ccall_wrap.cc EM_PORT_API(double) add(double a, int b) { return a + (double)b; }
使用下列命令编译:
emcc ccall_wrap.cc -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'cwrap']" -o ccall_wrap.js
提示
Emscripten从v1.38开始,ccall()/cwrap()辅助函数默认没有导出,在编译时需要通过-s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'cwrap']"选项显式导出。
在JavaScript中,我们可以使用以下方法调用:
//ccall_wrap.html var result = Module.ccall('add', 'number', ['number', 'number'], [13.0, 42]);
也可以直接调用Module._add():
var result = Module._add(13, 42);
ccall的优势在于可以直接使用字符串/Uint8Array/Int8Array作为参数。
例如,C导出函数如下:
//ccall_wrap.cc EM_PORT_API(void) print_string(const char* str) { printf("C:print_string(): %s\n", str); }
其中,print_string()的输入参数为字符串。在JavaScript中使用以下方法调用:
//ccall_wrap.html var str = 'The answer is:42'; Module.ccall('print_string', 'null', ['string'], [str]);
使用Uint8Array作为参数的例子如下:
//ccall_wrap.cc EM_PORT_API(int) sum(uint8_t* ptr, int count) { int total = 0, temp; for (int i = 0; i < count; i++){ memcpy(&temp, ptr + i * 4, 4); total += temp; } return total; } //ccall_wrap.html var count = 50; var buf = new ArrayBuffer(count * 4); var i8 = new Uint8Array(buf); var i32 = new Int32Array(buf); for (var i = 0; i < count; i++){ i32[i] = i + 1; } result = Module.ccall('sum', 'number', ['array', 'number'], [i8, count]);
提示
上述例子的C代码中,我们使用memcpy(&temp, ptr + i * 4, 4)获取自然数列的第i个元素的值。使用该方法的原因是:输入地址ptr有可能未对齐。更多对齐信息详见4.2节。
如果C导出函数返回了无须释放的字符串(静态字符串,或存放在由C代码自行管理的地址中的字符串),在JavaScript中可使用ccall()调用,直接获取返回的字符串,例如:
//ccall_wrap.cc EM_PORT_API(const char*) get_string() { const static char str[] = "This is a test."; return str; } //ccall_wrap.html console.log(Module.ccall('get_string', 'string'));
3.7.2 cwrap()
ccall()虽然封装了字符串等数据类型,但调用时仍然需要填入参数数组、参数列表等,为此对cwrap()进行了进一步封装。
函数申明:var func = Module.cwrap(ident, returnType, argTypes)
参数:
·ident,C导出函数的函数名(不含下划线前缀)。
·returnType,C导出函数的返回值类型,可以为boolean、number、string、null,分别表示函数返回值为布尔值、数值、字符串、无返回值。
·argTypes,C导出函数的参数类型的数组。参数类型可以为number、string、array,分别表示数值、字符串、数组。
返回值:封装方法。
例如,3.7.1节中的C导出函数可以按下列方式进行封装,代码如下:
//ccall_wrap.html var c_add = Module.cwrap('add', 'number', ['number', 'number']); var c_print_string = Module.cwrap('print_string', 'null', ['string']); var c_sum = Module.cwrap('sum', 'number', ['array', 'number']); var c_get_string = Module.cwrap('get_string', 'string');
C导出函数add()/print_string()/sum()/get_string()分别被封装为c_add()/c_print_string()/c_sum()/c_get_string(),这些封装方法与普通的JavaScript方法一样可以被直接使用:
//ccall_wrap.html console.log(c_add(25.0, 41)); c_print_string(str); console.log(c_get_string()); console.log(c_sum(i8, count));
3.7.3 ccall()/cwrap()的潜在风险
下面列出的是Emscripten为ccall()/cwrap()函数生成的相关胶水代码,有兴趣的读者可以尝试分析。相关函数说明如下。
1)getCFunc():根据ident获取C导出函数。
2)stackSave():保存栈指针。
3)arrayToC()/stringToC():将array/string参数复制到栈空间中。
4)func.apply():调用C导出函数。
5)convertReturnValue():根据returnType将返回值转为对应类型。
6)stackRestore():恢复栈指针。
// Returns the C function with a specified identifier (for C++, you need to do manual name mangling) function getCFunc(ident) { var func = Module['_' + ident]; // closure exported function assert(func, 'Cannot call unknown function ' + ident + ', make sure it is exported'); return func; } var JSfuncs = { // Helpers for cwrap -- it can't refer to Runtime directly because it might // be renamed by closure, instead it calls JSfuncs['stackSave'].body to find // out what the minified function name is. 'stackSave': function() { stackSave() }, 'stackRestore': function() { stackRestore() }, // type conversion from js to c 'arrayToC' : function(arr) { var ret = stackAlloc(arr.length); writeArrayToMemory(arr, ret); return ret; }, 'stringToC' : function(str) { var ret = 0; if (str !== null && str !== undefined && str !== 0) { // null string // at most 4 bytes per UTF-8 code point, +1 for the trailing '\0' var len = (str.length << 2) + 1; ret = stackAlloc(len); stringToUTF8(str, ret, len); } return ret; } }; // For fast lookup of conversion functions var toC = { 'string': JSfuncs['stringToC'], 'array': JSfuncs['arrayToC'] }; // C calling interface. function ccall(ident, returnType, argTypes, args, opts) { function convertReturnValue(ret) { if (returnType === 'string') return Pointer_stringify(ret); if (returnType === 'boolean') return Boolean(ret); return ret; } var func = getCFunc(ident); var cArgs = []; var stack = 0; assert(returnType !== 'array', 'Return type should not be "array".'); if (args) { for (var i = 0; i < args.length; i++) { var converter = toC[argTypes[i]]; if (converter) { if (stack === 0) stack = stackSave(); cArgs[i] = converter(args[i]); } else { cArgs[i] = args[i]; } } } var ret = func.apply(null, cArgs); ret = convertReturnValue(ret); if (stack !== 0) stackRestore(stack); return ret; } function cwrap(ident, returnType, argTypes, opts) { return function() { return ccall(ident, returnType, argTypes, arguments, opts); } }
通过ccall()/cwrap()胶水代码可知,虽然ccall()/cwrap()可以简化字符串参数的交换,但这种便利性是有代价的,即当输入参数类型为string/array时,ccall()/cwrap()在C环境的栈上需要分配相应的空间,并将数据复制到分配的空间中,然后调用相应的导出函数。相对于堆来说,栈空间是很稀缺的资源,因此使用ccall()/cwrap()时需要格外注意传入的字符串/数组的大小,避免栈溢出。
上述例子的输出如图3-10所示。