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

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所示。

图3-10 ccall()/cwrap()示例运行结果