ccall/cwrap2.4节我们提到,JavaScript调用C/C++时只能使用Number作为参数,因此如果参数是字符串、数组等非Number类型,则需要拆分为以下步骤:
Module._malloc()在Module堆中分配内存,获取地址ptr;Module._free()释放ptr。由此可见调用过程相当繁琐,尤其当非Number参数个数较多时,JavaScript侧的调用代码会急剧膨胀。为了简化调用过程,Emscripten提供了ccall/cwrap封装函数。
ccall语法:
var result = Module.ccall(ident, returnType, argTypes, args);
参数:
'boolean'、'number'、'string'、'null',分别表示函数返回值为布尔值、数值、字符串、无返回值;'number'、'string'、'array',分别代表数值、字符串、数组;例如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
tips 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]);
tips 上述例子的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'));
cwrapccall虽然封装了字符串等数据类型,但调用时仍然需要填入参数类型数组、参数列表等,为此cwrap进行了进一步封装:
var func = Module.cwrap(ident, returnType, argTypes);
参数:
'boolean'、'number'、'string'、'null',分别表示函数返回值为布尔值、数值、字符串、无返回值;'number'、'string'、'array',分别代表数值、字符串、数组;返回值:
例如2.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));
ccall/cwrap潜在风险虽然ccall/cwrap可以简化字符串参数的交换,但这种便利性是有代价的:当输入参数类型为'string'/'array'时,ccall/cwrap在C环境的栈上分配了相应的空间,并将数据拷入了其中,然后调用相应的导出函数。
相对于堆来说,栈空间是很稀缺的资源,因此使用ccall/cwrap时需要格外注意传入的字符串/数组的大小,避免爆栈。
下面列出的是Emscripten为ccall/cwrap生成的相关胶水代码,有兴趣的读者可以尝试分析,其概略流程为:
getCFunc(),根据ident获取C导出函数;stackSave(),保存栈指针;arrayToC()/stringToC(),将array/string参数拷贝到栈空间中;func.apply(),调用C导出函数convertReturnValue(),根据returnType将返回值转为对应类型;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);
}
}
本节例子的输出如下: