Emscripten 提供了许多方法来在 JavaScript 和编译后的 C 或 c++ 之间连接和交互,我们先来看看 js 调用 WASM 的情况。

一、使用 ccallcwrap

callall() 调用带有指定参数的编译过的 C 函数 并返回结果,而 cwrap() 封装了编译过的 C 函数并返回一个可以正常调用的 JavaScript 函数。因此,如果计划多次调用一个编译后的函数,cwrap() 会更有用。

例如下面的 C main.cpp 文件:

#include <math.h>

extern "C" {

    int int_sqrt(int x) {
        return sqrt(x);
    }

}

使用下面的命令进行编译:

emcc main.cpp -o function.html -s EXPORTED_FUNCTIONS=_int_sqrt -s EXPORTED_RUNTIME_METHODS=ccall,cwrap

EXPORTED_FUNCTIONS 告诉编译器哪些函数我们想要导出(不指定的函数会被删掉),EXPORTED_RUNTIME_METHODS 告诉编译器我们需要用到的运行时方法 ccallcwrap,否则这些方法也会被优化掉

编译后就可以在 js 中通过 cwrap 使用了:

int_sqrt = Module.cwrap('int_sqrt', 'number', ['number'])
int_sqrt(12)    // return 3
int_sqrt(28)    // return 5

第一个参数是被 wrap 的 C 函数的名字(没有下划线),第二个参数是函数返回值在类型(如果没有返回值,使用 JavaScript 的 null 类型),第三个参数是一个参数数组(如果没有参数,可以省略)。

JavaScript 的 number 类型对应于 C 语言的 intfloatpointer类型

JavaScript 的 string 类型对应于 C 语言的字符串类型 char*

JavaScript 的 array 或类型化数组对应于 C 语言的数组类型;对于类型化数组,它必须是 Uint8Array 或 Int8Array

在 JavaScript 中使用 ccall 调用:

// Call C from JavaScript
var result = Module.ccall('int_sqrt', // name of C function
  'number',     // return type
  ['number'],   // argument types
  [28]);        // arguments

// result is 5

如果要 导出 JS 库函数(例如 SRC/Library*.js 文件中的某些内容),那么除了 EXPORTED_FUNCTIONS 之外,还需要将其添加到 DEFAULT_LIBRARY_FUNCS_TO_INCLUDE 编译参数中,因为后者将强制添加的方法被包含在构建中。

二、js 直接调用导出的接口

直接调用编译成的 js 接口函数(在 C 或 C++ 接口函数名的前面添加 _ )的方式比使用 ccall、cwrap 的调用方式运行更快。

传递给函数和从函数接收的参数必须是基本的数据类型:

  • 整数和浮点数可以直接进行传递
  • 指针也可以直接传递
  • JavaScript 字符串类型 someString 可以通过使用 ptr = allocateUTF8(someString) 转换为 char *注意: 向指针的转换会分配内存,之后需要通过调用 free(ptr) 来释放内存(在JavaScript 中调用 _free(ptr)
  • 从 C/C++ 接收的 char* 可以使用 UTF8ToString(ptr) 转换为 JavaScript 字符串。

案例一: 从 js 中传入 string 类型的参数

C++ 接口代码:

// 接收 js 的 json 字符串,解析 data 项,并将内容打印到控制台
int EMSCRIPTEN_KEEPALIVE jsonParse(const char *jsonStr)
{
    // string str = R"({"data": "test json"})";
    auto j = nlohmann::json::parse(jsonStr);
    if (j.find("data") != j.end())
    {
        auto v = j["data"].get<string>();
        cout << v << endl;
    }

    return 0;
}

js 端代码:

let jsonstr = JSON.stringify({data:"Hello World!"});
const ptr = allocateUTF8(jsonstr);
Module._jsonParse(ptr);
_free(ptr);

案例二: 在 js 中接收返回的 char* 类型

C++ 接口代码:

char* EMSCRIPTEN_KEEPALIVE outString(char* p){
    char s[] = ", Hi there!";
    strcat(p, s);
    return p;
}

js 端代码:

const ptr1 = allocateUTF8("zhang3");
var retPtr = Module._outString(ptr1);
var resValue = UTF8ToString(retPtr);
console.log(resValue);
_free(retPtr);

案例三: 在 js 中开辟内存,调用 C++ 接口并为内存赋值

C++ 接口代码:

int EMSCRIPTEN_KEEPALIVE outString2(char* p){
    char s[] = "Hi there!";
    strcpy(p, s);
    return 0;
}

js 端代码:

var ptr = _malloc(512);
Module._outString2(ptr);
var str = UTF8ToString(ptr);
console.log(str);
_free(ptr);

参考:

  1. Interacting with code