1.前言

上一篇博客介绍了Emscripten中的胶水代码,通常我们会在js中调用定义在C/C++中的函数,此时就涉及到js如何向C/C++传递参数。本文主要介绍Emscripten中JS与C/C++互相调用的方式。在阅读之前,读者最好对WebAssembly有所了解,并且有一定的使用Emscripten的经验。
本文所用到的示例已在github上开源。

2.内存模型

2.1.Module.asm.memory

通过Emscripten处理后,C/C++代码直接通过地址访问的数据全部在内存中,该内存空间实际是Emscripten提供的ArrayBuffer对象。我们可以在js中通过 Module.asm.memory 访问到这个对象。实际上js可以访问到C/C++所使用的内存地址,但C/C++却不能访问到js所使用的内存地址。参考《C/C++面向WebAssembly编程》一书,这种模型被成为单向透明的内存模型

C/C++能直接访问的数据事实上被限制在Module.buffer内部,JavaScript环境中的其他对象无法被C/C++直接访问。但在JavaScript中可以访问C/C++内存,通过获取C/C++中的变量地址进而获取到C/C++中的变量。这种模型被称为单向透明的内存模型。

在上一篇博客介绍胶水代码加载wasm模块时提到了胶水代码通过 receiveInstance 函数将wasm实例的exports挂载到window.Module.asm下。其实 receiveInstance 函数还有一个作用,那就是处理wasm实例导出的memory属性:

// Load the wasm module and create an instance of using native support in the JS engine.// handle a generated wasm instance, receiving its exports and// performing other necessary setup/** @param {WebAssembly.Module=} module*/function receiveInstance(instance, module) {var exports = instance.exports;Module['asm'] = exports;wasmMemory = Module['asm']['memory'];assert(wasmMemory, "memory not found in wasm exports");// This assertion doesn't hold when emscripten is run in --post-link// mode.// TODO(sbc): Read INITIAL_MEMORY out of the wasm file in post-link mode.//assert(wasmMemory.buffer.byteLength === 16777216);updateGlobalBufferAndViews(wasmMemory.buffer);// ...}

其中 updateGlobalBufferAndViews 函数的主要逻辑如下:

function updateGlobalBufferAndViews(buf) {buffer = buf;Module['HEAP8'] = HEAP8 = new Int8Array(buf);Module['HEAP16'] = HEAP16 = new Int16Array(buf);Module['HEAP32'] = HEAP32 = new Int32Array(buf);Module['HEAPU8'] = HEAPU8 = new Uint8Array(buf);Module['HEAPU16'] = HEAPU16 = new Uint16Array(buf);Module['HEAPU32'] = HEAPU32 = new Uint32Array(buf);Module['HEAPF32'] = HEAPF32 = new Float32Array(buf);Module['HEAPF64'] = HEAPF64 = new Float64Array(buf);
}

2.2.Module.HEAPX

在函数 updateGlobalBufferAndViews 中,Int8Array、Int16Array、Int32Array等等都是TypedArray视图。为什么在胶水代码中需要导出如此众多的TypedArray视图,这是因为ArrayBuffer对象代表储存二进制数据的一段内存,不能直接读写,需要通过TypedArray视图或者DataView视图来进行读写。而对于同一段内存根据视图的不同可以有不同的解读方式,因而需要将同一个ArrayBuffer转换为不同的TypedArray对象。这些TypedArray对象都可以直接在JS中通过 Module.HEAPX 的方式获取。
如果C中定义了一个方法 get_int_ptr 返回一个int值的地址,则在JS中可以这样获取其值:

// 获取变量地址
var int_ptr = Module._get_int_ptr();
// 通过Module.HEAP32[int_ptr >> 2]获取了该地址对应的int32值
// 由于Module.HEAP32每个元素占用4字节,因此int_ptr需除以4(既右移2位)方为正确的索引
var int_value = Module.HEAP32[int_ptr >> 2];

3.JS与C/C++互相调用的方式

总的来说,JS与C/C++互相调用的方式有两种:

  • 通过Number类型的参数直接传递
  • 通过内存间接传递

3.1.通过数值类型的参数

js与C/C++有各自的数据体系,但Number是两者的交集,如果在js或者C/C++中直接调用对方的函数,那面可以将Number作为参数和返回值。
js的Number类型其实是64位浮点数,可以精确表达32位及以下整型数、32位浮点数、64位浮点数,但C/C++中的number其实还有64位整型数,这意味着JavaScript与C/C++相互直接调用时,不能使用64位整型数作为参数或返回值。如果直接调用时传递的数据不是number,则会导致传参失败。

3.1.1.JS调用C/C++函数

由于C/C++是强类型语言,因此来自js的Number传入时,会发生隐式类型转换:

  • 若目标类型为int,将执行向0取整
  • 若目标类型为float,类型转换时有可能损失精度

尝试如下代码:

#include <stdio.h>EM_PORT_API(void) print_int(int a) {printf("C{print_int() a:%d}\n", a);
}EM_PORT_API(void) print_float(float a) {printf("C{print_float() a:%f}\n", a);
}EM_PORT_API(void) print_double(double a) {printf("C{print_double() a:%lf}\n", a);
}

其中,EM_PORT_API 是C函数的函数导出宏,需要将下列代码添加到C文件的顶部,否则编译器很有可能会认为定义的函数没有被调用而将其干掉:

// 定义函数导出宏
// __EMSCRIPTEN__宏用于探测是否是Emscripten环境
// __cplusplus用于探测是否C++环境
// EMSCRIPTEN_KEEPALIVE是Emscripten特有的宏,用于告知编译器后续函数在优化时必须保留,并且该函数将被导出至JavaScript
#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

在JS中做如下调用:

Module._print_int(3.4)
Module._print_int(4.6)
Module._print_int(-3.4)
Module._print_int(-4.6)
Module._print_float(2000000.03125)
Module._print_double(2000000.03125)

控制台打印:

C{print_int() a:3}
C{print_int() a:4}
C{print_int() a:-3}
C{print_int() a:-4}
C{print_float() a:2000000.000000}
C{print_double() a:2000000.031250}

3.1.2.C/C++调用JS函数

通过将js函数注入C/C++,可以在C/C++中向js函数传递Number。不过这种做法稍微麻烦一些,需要将待注入的js函数单独维护在一个js文件中,比如我们将待注入的js函数放到pkg.js中:

mergeInto(LibraryManager.library, {// c将传入两个int,js返回intjs_add: function (a, b) {console.log('js_add')return a + b},// c将传入两个float,js返回floatjs_addF: function (a, b) {console.log('js_addF')return a + b},// c将传入一个int,js没有返回js_console_log_int: function (param) {console.log('js_console_log_int:' + param)},// c将传入一个float,js没有返回js_console_log_float: function (param) {console.log('js_console_log_float:' + param)},// c将传入一个字符串,测试js能否拿到字符串js_console_log_string: function (param) {console.log('js_console_log_string', param)}
})

注意,我们在mergeInto函数的第二个参数中,将需要注入的函数定义为对象的方法。mergeInto将该对象合并到LibraryManager.library中,LibraryManager.library是JavaScript注入C环境的库。
在编译时添加参数 --js-library 表示将js函数注入C,后接js文件地址:

emcc ../index.c -o index.js -s WASM=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']" -s "EXPORTED_FUNCTIONS=['_malloc', '_free', '_main']" --js-library ../pkg.js

在C/C++中需要先声明定义在js的函数,然后才能使用:

// c调用js函数
EM_PORT_API(int) js_add(int a, int b);
EM_PORT_API(float) js_addF(float a, float b);
EM_PORT_API(void) js_console_log_int(int param);
EM_PORT_API(void) js_console_log_float(float param);
EM_PORT_API(void) js_console_log_string(char* str);EM_PORT_API(void) print_the_answer() {int i = js_add(21, 21);float j = js_addF(1.1, 1.1);js_console_log_int(i);js_console_log_float(j);js_console_log_string("Hello, wolrd! 你好,世界!");
}

你可以直接在C/C++中调用print_the_answer,也可以在js中通过 Module._print_the_answer() 来调用,结果都是一样的:

js_add
index.js:1911 js_addF
index.js:1920 js_console_log_int:42
index.js:1916 js_console_log_float:2.200000047683716
index.js:1924 js_console_log_string 1024

3.2.通过内存

JavaScript和C/C++通过内存可以传递number或字符串格式的数据,通常用于需要在JavaScript与C/C++之间交换大块的数据

3.2.1.C/C++向JS

3.2.1.1.传递数值

C/C++向js返回Number的指针,js通过Emscripten为Module.buffer创建的常用类型的TypedArray进行读取。
C代码:

EM_PORT_API(int) g_int = 42;
EM_PORT_API(double) g_double = 3.1415926;EM_PORT_API(int*) get_int_ptr() {return &g_int;
}EM_PORT_API(double*) get_double_ptr() {return &g_double;
}EM_PORT_API(void) print_data() {printf("C{g_int:%d}\n", g_int);printf("C{g_double:%lf}\n", g_double);
}

js代码:

const int_ptr = Module._get_int_ptr()
// 获取了该地址对应的int32值
// 由于Module.HEAP32每个元素占用4字节
// 因此int_ptr需除以4(既右移2位)方为正确的索引
const int_value = Module.HEAP32[int_ptr >> 2]
console.log("JS{int_value:" + int_value + "}")const double_ptr = Module._get_double_ptr()
const double_value = Module.HEAPF64[double_ptr >> 3]
console.log("JS{double_value:" + double_value + "}")// js改动c中定义的变量
Module.HEAP32[int_ptr >> 2] = 13
Module.HEAPF64[double_ptr >> 3] = 123456.789
Module._print_data()

控制台输出:

1 1 2 3 5 8 13 21 34 55

3.2.1.2.传递字符串

传递字符串的逻辑和传递数值是一样的,C/C++向js返回字符串的指针,js调用UTF8ToString将其转化为js字符串。
C代码:

// 向js传递字符串
EM_PORT_API(const char*) get_string() {static const char str[] = "Hello, wolrd! 你好,世界!";return str;
}

js:

// C函数get_string()返回了一个字符串的地址
const ptr = Module._get_string()
// 调用UTF8ToString将其转换为js字符串
const str = UTF8ToString(ptr)
console.log(typeof(str))
console.log(str)

控制台打印:

string
index.js:72 Hello, wolrd! 你好,世界!

3.2.2.JS向C/C++

3.2.2.1.传递数值

js调用c中的malloc函数分配内存,该函数返回一个指针,C/C++通过该指针获取对应的内存地址。
js:

const count = 50
// 调用c malloc方法分配内存
const ptr = _malloc(4 * count)for (let i = 0; i < count; i++){Module.HEAP32[ptr / 4 + i] = i + 1
}console.log(Module._sum(ptr, count))
Module._free(ptr)

C:

// 求数组前count项的和
EM_PORT_API(int) sum(int* ptr, int count) {int total = 0;for (int i = 0; i < count; i++){total += ptr[i];}return total;
}

控制台输出:1275
需要注意的是,如果要在js代码中使用_malloc,需要在编译时增加参数 EXPORTED_FUNCTIONS,将一些C函数导出

emcc ../index.c -o index.js -s WASM=1 -s "EXPORTED_FUNCTIONS=['_malloc', '_free', '_main']" --js-library ../pkg.js

上述命令导出malloc/free/main三个C函数

3.2.2.2.传递字符串

js使用allocateUTF8()将字符串传入C/C++内存,该方法返回一个指针,C/C++通过该指针获取对应的内存地址。
js:

// 使用allocateUTF8()将字符串传入C/C内存
const ptr = allocateUTF8("你好,Emscripten!")
Module._print_string(ptr)
_free(ptr)

C:

// 打印js通过内存传递的字符串
EM_PORT_API(void) print_string(char* str) {printf("%s\n", str);
}

控制台输出:你好,Emscripten!

4.ccall/cwrap

通过上面的例子可以看出,C/C++和js互相传递数据时,如果通过数值或者内存的形式进行传递,过程比较繁琐。为了简化调用过程,Emscripten提供了ccall/cwrap两个函数用于js调用C/C++函数。

4.1.ccall

ccall的语法如下:

const result = Module.ccall(ident, returnType, argTypes, args)

需要传递的参数如下:

  • ident :C导出函数的函数名(不含“_”下划线前缀)
  • returnType:C导出函数的返回值类型,可以为’boolean’、‘number’、‘string’、‘null’,分别表示函数返回值为布尔值、数值、字符串、无返回值
  • argTypes :C导出函数的参数类型的数组。参数类型可以为’number’、‘string’、‘array’,分别代表数值、字符串、数组
  • args:参数数组

以调用上一节C中定义的 print_string 函数为例,采用ccall进行调用的话,只需一行代码:

Module.ccall('print_string', 'null', ['string'], ['你好,Emscripten!'])

需要注意的是,如果要在js代码中使用ccall,需要在编译时增加参数 EXPORTED_RUNTIME_METHODS,将一运行时的函数导出

emcc ../index.c -o index.js -s WASM=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']" -s "EXPORTED_FUNCTIONS=['_malloc', '_free', '_main']" --js-library ../pkg.js

4.2.cwrap

ccall虽然封装了字符串等数据类型,但调用时仍然需要填入参数类型数组、参数列表等,为此cwrap进行了进一步封装:

const func = Module.cwrap(ident, returnType, argTypes)

参数:

  • ident :C导出函数的函数名(不含“_”下划线前缀)
  • returnType:C导出函数的返回值类型,可以为’boolean’、‘number’、‘string’、‘null’,分别表示函数返回值为布尔值、数值、字符串、无返回值
  • argTypes:C导出函数的参数类型的数组。参数类型可以为’number’、‘string’、‘array’,分别代表数值、字符串、数组

返回值:封装后的方法
同样的,我们需要在编译时将cwrap导出:

emcc ../index.c -o index.js -s WASM=1 -s "EXPORTED_RUNTIME_METHODS=['ccall', 'cwrap']" -s "EXPORTED_FUNCTIONS=['_malloc', '_free', '_main']" --js-library ../pkg.js

还是以调用C中定义的 print_string 函数为例:

const printString = Module.cwrap('print_string', 'null', ['string'])
printString('你好,Emscripten!')

我们只需要调用cwrap封装print_string函数一次,后续调用只需要传递参数即可,用法上有点像bind。

4.3.ccall/cwrap潜在风险

参考《C/C++面向WebAssembly编程》一书,使用ccall/cwrap其实存在潜在风险:

虽然ccall/cwrap可以简化字符串参数的交换,但这种便利性是有代价的——当输入参数类型为’string’/'array’时,ccall/cwrap在C环境的栈上分配了相应的空间,并将数据拷入了其中,然后调用相应的导出函数。
相对于堆来说,栈空间是很稀缺的资源,因此使用ccall/cwrap时需要格外注意传入的字符串/数组的大小,避免爆栈。

5.C/C++调用JS的其他方式

js可以通过ccall/cwrap很方便的调用C/C++。在C/C++中,也有一些方法可以直接调用js代码,主要包括:

  • EM_ASM宏内联JavaScript代码
  • emscripten_run_script

5.1.EM_ASM宏

EM_ASM宏只能执行嵌入的jst代码, 无法传入参数或获取返回结果:

#include <emscripten.h>int main(int argc, char ** argv) {EM_ASM(console.log('From EM_ASM', [{a: true}]));
}

我们可以在EM_ASM里面编写任何js代码,可以使用任何js支持的数据类型。

5.2.emscripten_run_script

使用emscripten_run_script时,需要先在C/C++中声明emscripten_run_script:

void emscripten_run_script(const char *script);int main(int argc, char ** argv) {emscripten_run_script("console.log('From emscripten_run_script', [{a: true}]);");
}

在emscripten_run_script内,我们可以通过字符串的形式编写任意js代码,该方法没有返回值。如果想获取返回值,可以使用 emscripten_run_script_int 或 emscripten_run_script_string 获取整型或者字符串类型的返回,两个函数的参数和emscripten_run_script一致。

6.参考

C/C++面向WebAssembly编程

Emscripten之JS与C/C++互相调用相关推荐

  1. (WebAssembly)JS/微信小程序,调用C/C++

    JS调用C库函数 1.将.c文件编译成WebAssembly,具体步骤参考:编译 C/C++ 为 WebAssembly - WebAssembly | MDN,这个比较简单,文章中比较详细的步骤 2 ...

  2. js调用c语言程序设计,HTML页面,测试JS对C函数的调用简单实例

    HTML页面,测试JS对C函数的调用 //http://www.w3schools.com/jsref/event_onclick.asp //document.write('Hello World! ...

  3. [js] 举例说明js中什么是尾调用优化

    [js] 举例说明js中什么是尾调用优化 写在前面 上次介绍了什么是尾调用以及怎么准确快速的判别一个函数调用是否为尾调用.那么,我们判别尾调用的意义是什么呢?做什么事情总归有个目的,那么今天我们就来系 ...

  4. java 防止js注入_在WebView中如何让JS与Java安全地互相调用

    在现在安卓应用原生开发中,为了追求开发的效率以及移植的便利性,使用WebView作为业务内容展示与交互的主要载体是个不错的折中方案.那么在 这种Hybrid(混合式) App中,难免就会遇到页面JS需 ...

  5. jquery/js实现一个网页同时调用多个倒计时(最新的)

    jquery/js实现一个网页同时调用多个倒计时(最新的) 最近需要网页添加多个倒计时. 查阅网络,基本上都是千遍一律的不好用. 自己按需写了个.希望对大家有用. 有用请赞一个哦! //js //js ...

  6. Node.js使用ffi-napi,ref-array-napi,ref-struct-napi调用动态库

    0x01 概述 使用electron开进行桌面程序的开发,似乎成了WEB前端开发人员转桌面程序开发的首选.近期有一些使用在electron中使用加密锁的需求,学习了一下在Node.js中通过ffi-n ...

  7. Android JS 通过JSBridge(BridgeWebView)相互调用详解

    一.JSBridge GitHub链接 https://github.com/lzyzsd/JsBridge 二.AndroidStudio配置JsBridge 1.根目录Gradle配置 maven ...

  8. JS中定时器和延时调用学习笔记

    JS中定时器和延时调用 在JS中希望一个函数重复的执行,可以为该函数设置一个定时装置 方法: setInterval(); 该方法可以将一个函数,每隔一段时间被调用一次 有两个参数 第一个是回调函数, ...

  9. 使用js在前端web页面调用打印机

    使用js在前端web页面调用打印机 最近参与了一个比较老的项目改造,需要使用到打印机相关的技术. 由于打印机也是比较老旧,所以没有SDK相关的应用,虽然有桌面的插件,但是需要集成到web中来,最终找到 ...

最新文章

  1. CSS与HTML结合
  2. Linux中字符设备注册方式,3.4. 字符设备注册
  3. 用HttpListener做web服务器,简单解析post方式过来的参数、上传的文件
  4. Debug enterprise search menu
  5. linux 延时一微秒_让我们暂停一微秒
  6. 如何学好初中计算机,初中生怎么学习方法好 十大方法告诉你
  7. Java写文件导致io过高_161108、Java IO流读写文件的几个注意点
  8. 跟随者数字解码_跟随模式的数字
  9. vue函数input输入值即请求,优化为用户输入完成之后再请求
  10. 4.9 利用对应的泛型替换Hashtable[转]
  11. Intel Core Enhanced Core架构/微架构/流水线 (9) - 执行单元发射口旁路时延
  12. 出于安全考虑,谷歌禁用三款 Linux web 浏览器登录其服务
  13. ajax提交表单序列化(serialize())数据
  14. 给大学生的劝告,你们为何应该开始接触 Unix/Linux
  15. 基于ARMA-偏tGARCH和DCC-GARCH模型测算CoVaR——R语言实现
  16. iOS TestFlight 使用详解
  17. 自动控制原理(2) - 线性化和传递函数
  18. AI巨头们建造的“新世界”,进展如何?
  19. scrapy-splash安装使用
  20. 苹果电脑上几个不错的数学分析工具

热门文章

  1. android mvp模式鸿洋,Android上的MVP模式
  2. 手机九宫格拼音打字教学——新手教程
  3. Affinity Photo 区域转曲线,钢笔转选取
  4. TCP和UDP的区别小结和应用场合
  5. AES-128算法实现(附C++源码)
  6. python excel 微信_Python Excel微信数据转换 分析提取微信零钱数据
  7. 老照片电子修复后丢失能找回吗
  8. Python基础(二十四):面向对象核心知识
  9. 哈希在线计算工具_哈希:开发人员的绝佳工具
  10. 小七新Android逆向,小七Android逆向脱壳课程