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

3.1 JavaScript调用C函数

一个具备实用功能的WebAssembly模块必然提供了供外部调用的函数接口。在2.3节中,我们展示了通过Module._main()函数调用C/C++入口main()函数的方法。本节将介绍普通C函数导出以供JavaScript调用的方法。

3.1.1 定义函数导出宏

为了方便函数导出,我们需要先定义一个函数导出宏。该宏需要完成以下功能。

1)使用C风格的符号修饰。C++引入了多态、重载、模板等特性,使C++语言环境下的符号修饰策略(即函数、变量在最终编译成果中的名字的生成规则)非常复杂,并且不同的C++编译器有着各自的符号修饰策略。如果不做额外处理,我们在C++中创建函数的时候,很难预知它在最终编译成果中的名称——这与C语言环境完全不同。因此,当我们试图将main()函数之外的全局函数导出至JavaScript语言环境时,必须强制使用C风格的符号修饰,以保证函数名称在C/C++语言环境以及JavaScript语言环境中有统一的对应规则。

2)避免函数因为缺乏引用,而导致在编译链接时被优化器删除。如果某个导出函数仅供JavaScript调用,而在C/C++环境中从未被使用,当开启某些优化选项(比如-O2以上)时,该函数有可能被编译器优化删除,因此需要提前告知编译器:该函数必须保留,不能删除,不能改名。

3)为了保持足够的兼容性,宏需要根据不同的环境——原生代码环境与Emscripten环境、纯C环境与C++环境等,自动切换合适的行为。

提示

main()函数作为C/C++程序的主入口,其符号修饰策略是特殊的——即使在C++环境中不作特殊约束,其最终的符号仍然是_main(),无须按上述第1点进行处理。

本书坚持的理念是,编写既可以在C/C++原生代码中使用,又可以在Emscripten环境中使用的“对编译目标不敏感”的模块。上述第3点要求正是该理念的产物。后续章节同理,不再赘述。

为了满足上述3点要求,定义EM_PORT_API宏如下:


#ifndef EM_PORT_API
#    if defined(__EMSCRIPTEN__)
#        include <emscripten.h>
#        if defined(__cplusplus)
#            define EM_PORT_API(rettype) extern "C" rettype EMSCRIPTEN_KEEPALIVE
#        else
#           define EM_PORT_API(rettype) rettype EMSCRIPTEN_KEEPALIVE
#        endif
#    else
#        if defined(__cplusplus)
#            define EM_PORT_API(rettype) extern "C" rettype
#        else
#           define EM_PORT_API(rettype) rettype
#        endif
#    endif
#endif

在上述代码中:

1)__EMSCRIPTEN__宏用于探测是否是Emscripten环境;

2)__cplusplus用于探测是否是C++环境;

3)EMSCRIPTEN_KEEPALIVE是Emscripten特有的宏,用于告知编译器后续函数在优化时必须保留,并且该函数将被导出至JavaScript环境。

使用EM_PORT_API定义函数声明:


EM_PORT_API(int) Func(int param);

在Emscripten中,上述函数声明最终将被展开,代码如下:


#include <emscripten.h>
extern "C" int EMSCRIPTEN_KEEPALIVE Func(int param);

3.1.2 在JavaScript中调用C导出函数

根据2.3节中对胶水代码的分析,我们知道JavaScript环境中的Module对象已经封装了C环境下的导出函数。封装方法的名字是下划线加上C环境的函数名。例如,创建C文件export1.cc的代码如下:


//export1.cc
#ifndef EM_PORT_API
#    if defined(__EMSCRIPTEN__)
#        include <emscripten.h>
#        if defined(__cplusplus)
#            define EM_PORT_API(rettype) extern "C" rettype EMSCRIPTEN_KEEPALIVE
#        else
#            define EM_PORT_API(rettype) rettype EMSCRIPTEN_KEEPALIVE
#        endif
#    else
#        if defined(__cplusplus)
#            define EM_PORT_API(rettype) extern "C" rettype
#        else
#            define EM_PORT_API(rettype) rettype
#        endif
#    endif
#endif

#include <stdio.h>

EM_PORT_API(int) show_me_the_answer() {
    return 42;
}
EM_PORT_API(float) add(float a, float b) {
    return a + b;
}

使用emcc命令将其编译为wasm:


emcc export1.cc -o export1.js

创建页面export1.html:


<!doctype html>

<html>
  <head>
    <meta charset="utf-8">
    <title>Emscripten:Export1</title>
  </head>
  <body>
    <script>
    Module = {};
    Module.onRuntimeInitialized = function() {
      console.log(Module._show_me_the_answer());
      console.log(Module._add(12, 1.0));
    }
    </script>
    <script src="export1.js"></script>
  </body>
</html>

使用浏览器打开export1.html后,我们将在控制台得到如图3-1所示的输出。

图3-1 在JavaScript中调用C函数

需要注意的是,JavaScript是弱类型语言,在调用函数时,并不要求调用方与被调用方的签名一致,这与C/C++有本质区别。在C环境中,以下调用都不符合语法要求:


int k = show_me_the_answer(10);
float f1 = add(12, 12, 12);
float f2 = add(12);

在JavaScript环境中,如果给出的参数个数多于形参个数,多余的参数将被舍弃(从左至右);如果参数个数少于形参个数,会自动以undefined填充不足的参数,因此下列JavaScript调用都是合法的:


console.log(Module._show_me_the_answer(10));
console.log(Module._add(2, 3, 4));
console.log(Module._add(12));

其输出结果如图3-2所示。

图3-2 JavaScript未按函数声明调用C函数

注意第三个调用,虽然语法没问题,但是由于缺少的参数以undefined填充而不是0值填充,因此函数返回结果为NaN。

提示

图3-2中使用了2.3节介绍的onRuntimeInitialized回调方法注入测试代码。为了省略无关信息、方便阅读,在不产生歧义的情况下后续章节将不再重复列出回调方法注入的完整代码以及EM_PORT_API宏的定义。