IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    [WebAssembly]初学笔记 使用Embind在Javascript与C++之间交互

    罗佳(博主)发表于 2024-08-29 17:57:01
    love 0

    如果有看不明白的地方请先看前置说明文章

    Embind是emscripten提供的又一种在js和c++之间的交互方案,其提供更加丰富的交互方式,不止是前面的笔记中介绍的那种简单的函数调用。

    Embind库API参考文档地址:https://emscripten.org/docs/api_reference/bind.h.html

    官方指导文档地址:https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html,下面简单概括一下这个库包含的功能。

    1. 在js环境中绑定c++中的(绑定指的是把另一个语言中的概念映射为当前语言中类似的概念):

    • 函数
    • 类(结构体) 和 类的属性 以及 方法
    • 枚举和常量
    • 内存空间(以类型化数组形式获取)

    2. 在c++环境中:使用`val`类操作js中的任意对象。


    绑定C++概念在Javascript调用

    使用embind库的程序在编译时需要添加链接参数:-lembind

    绑定函数

    在EMSCRIPTEN_BINDINGS宏中使用 function 定义要绑定的函数:

    #include <emscripten/bind.h>  //引入库定义
    #include <iostream>
    using namespace emscripten;
    
    int call_console(float a, int b,const std::string &text) {
    	std::cout << "a: " << a << "\nb: " << b << "\nt: " << text << std::endl;
    	return 123456;
    }
    
    EMSCRIPTEN_BINDINGS(my_module) {
    	// 这里定义的名字"my_module"并不会出现在导出的实例中,即使不写也可以编译,只是一个用来给人看的标签
    	function("call_console", &call_console);//名为call_console方法将被添加到wasm实例对象上
    }

    注意上面的示例中传入的字符串使用了string而非char*,使用char*能编译但无法调用此函数,运行时报的错误不明确,所以这里我也没法解释为什么不能用。

    执行命令编译:emcc -sMODULARIZE=1 -sASSERTIONS -lembind -o dist/test.mjs test.cpp

    在start.js中调用一下(点击展开)
    (async () => {
    	const { default: wasm } = await import('./dist/test.mjs');
    	const instance = await wasm();
    	// console.log(instance);
    	console.log('调用结果:',instance.call_console(1.1,2,"poipoi啦啦啦"));
    	/* 控制台输出:
    		a: 1.1
    		b: 2
    		t: poipoi啦啦啦
    		调用结果: 123456
    	*/
    })();

    导出枚举和常量

    使用enum_<枚举类型>("绑定在实例上的名称")导出枚举,使用constant("绑定在实例上的名称", 任意embind支持的类型)导出常量,embind支持的类型见这里。

    这两个比较简单,放在一起举例:

    #include <emscripten/bind.h> //引入库定义
    using namespace emscripten;
    using std::string;
    
    enum 枚举1 {
    	枚举1的1,
    	枚举1的2
    };
    enum class 枚举2 {
    	枚举2的1=6,
    	枚举2的2=7
    };
    EMSCRIPTEN_BINDINGS(my_module) {
    	enum_<枚举1>("ENUM1") //ENUM1将被添加到wasm实例对象上
    		.value("ENUM1_ONE", 枚举1的1)
    		.value("ENUM1_TWO", 枚举1的2);
    	enum_<枚举2>("ENUM2") //ENUM2将被添加到wasm实例对象上
    		.value("ENUM2_ONE", 枚举2::枚举2的1)
    		.value("ENUM2_TWO", 枚举2::枚举2的2);
    	constant("TEXT", (string)"啊啊啊啊啊");   //TEXT将被添加到wasm实例对象上(注意导出的字符串需要为string)
    	constant("NUMBER", 12345);               //NUMBER将被添加到wasm实例对象上
    }

    执行命令编译:emcc -sMODULARIZE=1 -sASSERTIONS -lembind -o dist/test.mjs test.cpp

    在start.js中调用一下(点击展开)
    (async () => {
    	const { default: wasm } = await import('./dist/test.mjs');
    	const instance = await wasm();
    	console.log(instance);
    	/* 从控制台输出中可以看到instance对象上多出了wo'men以下内容:
    		{
    			ENUM1: [Function: ctor] {
    				values: { '0': ctor { }, '1': ctor { } },
    				ENUM1_ONE: ctor { },
    				ENUM1_TWO: ctor { }
    			},
    			ENUM2: [Function: ctor] {
    				values: { '6': ctor { }, '7': ctor { } },
    				ENUM2_ONE: ctor { },
    				ENUM2_TWO: ctor { }
    			},
    			TEXT: '啊啊啊啊啊',
    			NUMBER: 12345
    		}
    	但这个枚举导出的形式我没太明白怎么使用它,照理说 instance.ENUM2.ENUM2_ONE 的值应该为c++中定义的6才比较合理,但这里却是ctor { }
    	*/
    })();

    绑定结构体为元组或对象

    这部分的教程文档在这里,但是它省略了一个函数的定义,导致我迷惑了很久,这里我放一个完整的例子在这。主要注意代码中使用value_array和value_object导出的结构定义。

    #include <emscripten/bind.h> //引入库定义
    using namespace emscripten;
    using std::string;
    
    struct Position { // 一个坐标结构体
    	float x;
    	float y;
    };
    Position earth(float x, float y) { // 一个函数,返回坐标结构体
    	return Position{x, y};
    }
    struct Person {    // 一个人结构体
    	string name;   // 名字
    	string gender; // 性别
    	float age;     // 年龄
    };
    Person ikun(string name, float age) { // 一个函数,返回人结构体
    	return Person{name, "武装直升机", age};
    }
    
    EMSCRIPTEN_BINDINGS(my_module) {
    	// 注意:这里只是定义函数返回该结构体时的数据表示方式,此结构体不会出现在wasm实例上
    	value_array<Position>("Position")//使用此方式定义结构体,结构体将被表示为一个js数组
    		.element(&Position::x)
    		.element(&Position::y);
    	// 注意:这里只是定义函数返回该结构体时的数据表示方式,此结构体不会出现在wasm实例上
    	value_object<Person>("Person")//使用此方式定义结构体,结构体将被表示为一个js对象
    		.field("name", &Person::name)
    		.field("gender", &Person::gender)
    		.field("age", &Person::age);
    
    	function("ikun", &ikun);//在wasm实例上添加一个返回Person结构体的ikun函数
    	function("earth", &earth);//在wasm实例上添加一个返回Position结构体的earth函数
    }

    编译一下:emcc -sMODULARIZE=1 -sASSERTIONS -lembind -o dist/test.mjs test.cpp

    在start.js中调用一下(点击展开)
    (async () => {
    	const { default: wasm } = await import('./dist/test.mjs');
    	const instance = await wasm();
    	console.log('persion:', instance.ikun('菜', 2.5));
    	console.log('earth:', instance.earth(310.12345, 180.3215));
    	/* 控制台输出:
    		persion: { name: '菜', gender: '武装直升机', age: 2.5 }
    		earth: [ 310.1234436035156, 180.32150268554688 ]
    	*/
    })();

    绑定内存

    Embind可以通过typed_memory_view函数返回val类把wasm内的内存块映射为js中的TypedArray,不同的指针类型会被映射为不同的类型化数组,到这里我又得说一下emscripten的文档真的是稀烂,好多api根本就没写在文档里,连typed_memory_view函数也没有,非得去翻源码才知道它支持多种类型的数组,否则光看教程以为只能用uint8数组呢。

    映射内存的方式很简单,直接放示例。

    首先把test.cpp修改如下:

    #include <emscripten/bind.h>
    #include <emscripten/val.h>
    #include <iostream>
    using namespace emscripten;
    using std::cout, std::endl;
    
    //可以映射各种类型的buffer出来,这里列举3种
    size_t bufferLength = 10;
    uint8_t *byteBuffer = new uint8_t[10]; // 申请一块内存
    int32_t *int32Buffer = new int32_t[10]; // 申请一块内存
    float *floatBuffer = new float[10]; // 申请一块内存
    
    val getUint8Buffer() { // 返回val类把内存块映射为js的typed_array
    	return val(typed_memory_view(bufferLength, byteBuffer));
    }
    val getInt32Buffer() { // 返回val类把内存块映射为js的typed_array
    	return val(typed_memory_view(bufferLength, int32Buffer));
    }
    val getFloatBuffer() { // 返回val类把内存块映射为js的typed_array
    	return val(typed_memory_view(bufferLength, floatBuffer));
    }
    
    void showUint8BufferByte(size_t index) { // 用于之后查看js中修改的内存数据是否作用到wasm中
    	cout << "print from wasm: byteBuffer[" << index << "] = " << (unsigned int)byteBuffer[index] << endl;
    }
    
    EMSCRIPTEN_BINDINGS(my_module) {
    	function("getUint8Buffer", &getUint8Buffer);
    	function("getInt32Buffer", &getInt32Buffer);
    	function("getFloatBuffer", &getFloatBuffer);
    	function("showUint8BufferByte", &showUint8BufferByte);
    }

    然后编译一下:emcc -sMODULARIZE=1 -sASSERTIONS -lembind -o dist/test.mjs test.cpp

    通过start.js获取内存,修改后查看修改结果:(点击展开)
    (async () => {
    	const { default: wasm } = await import('./dist/test.mjs');
    	const instance = await wasm();
    	const uint8Buffer=instance.getUint8Buffer();//获取到wasm模块内存映射出来的buffer
    	const int32Buffer=instance.getInt32Buffer();//获取到wasm模块内存映射出来的buffer
    	const floatBuffer=instance.getFloatBuffer();//获取到wasm模块内存映射出来的buffer
    
    	console.log('print from js uint8Buffer:',uint8Buffer);//查看一下当前buffer的内容
    	console.log('print from js int32Buffer:',int32Buffer);//查看一下当前buffer的内容
    	console.log('print from js floatBuffer:',floatBuffer);//查看一下当前buffer的内容
    	uint8Buffer.fill(255);//把这块内存填充数字255
    	instance.showUint8BufferByte(5);//使用从wasm导出的函数,查看第6个字节的内容
    	/* 控制台输出:
    		print from js uint8Buffer: Uint8Array(10) [
    			0, 0, 0, 0, 0,
    			0, 0, 0, 0, 0
    		]
    		print from js int32Buffer: Int32Array(10) [
    			0, 0, 0, 0, 0,
    			0, 0, 0, 0, 0
    		]
    		print from js floatBuffer: Float32Array(10) [
    			0, 0, 0, 0, 0,
    			0, 0, 0, 0, 0
    		]
    		print from wasm: byteBuffer[5] = 255
    	*/
    })();

    数据类型的转换

    对于所有通过js调用的c++函数返回值和通过一些方法导出的数据类型都按照这个地址的列表规则进行转换:https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#built-in-type-conversions

    比较特殊的是,c++中函数返回的std::map和std::vector将在js中被创建为功能类似的对象(不是js原生的Map),std::optional类型返回值则是按照原本的可转换类型返回,只是函数返回空时js的调用结果将是undefined。根据教程中代码的情况来看,函数中返回的都是位于局部作用域的std::map、std::vector、std::optional对象的拷贝,因此在js中对这类对象的操作不会再反映到wasm里,仅起到一个方便导出数据的作用。

    绑定类

    绑定类可以在js中new出对应的对象,可以像普通的js类一样使用。由于绑定类的注意点太多,且方法比较复杂,因此在这里暂时不做介绍,如果有需求可以直接看官方的指导文档:

    https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#classes

    使用绑定的类在一些情况下要进行手动的内存管理,这点和原生js类的使用习惯不太相同,可能容易导致内存泄露,因此我也不会去使用它。


    在C++中使用emscripten::val类操作Javascript数据

    相对于在c++中绑定各种东西给js调用,在c++中操作js中的各种数据就简单很多,因为只要使用val这个类就可以完成所有操作。

    教程文档的地址在这里:https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#using-val-to-transliterate-javascript-to-c

    先上一个示例再进行介绍,假设我们要实现和以下js代码相同的逻辑:

    //注1:这段js代码需要启用顶层await才能运行
    //注2:这段代码为了和c++的代码对应,所以看起来会有点啰嗦
    const fetch=globalThis.fetch; //获取全局对象的fetch函数
    if(!fetch){
    	console.log("没有fetch方法!");
    	return 1;
    }
    console.log("开始fetch数据");
    let response =await fetch("https://danmaku.luojia.me/player/?id=24"); //从指定网址获取数据
    console.log("已获取数据!");
    console.log("body:");
    console.log(await response.text()); //把获取的网页源码显示在控制台

    编写我们的test.cpp,把以上js代码转写为c++就是下面这样:

    #include <emscripten/bind.h>
    #include <emscripten/val.h> //引入val库
    #include <iostream>
    using namespace emscripten;//val类在这个命名空间中
    using std::cout, std::endl, std::string;
    
    int main() {
    	val fetch = val::global("fetch");//通过val::global静态方法从js环境的全局对象中获取fetch方法
    	if (!fetch.as<bool>()) {//使用 val对象.as<目标类型>() 可以进行类型转换
    		cout << "没有fetch方法!";
    		return 1;
    	}
    	cout << "开始fetch数据" << endl;
    	val response = fetch(string("https://danmaku.luojia.me/player/?id=24")).await(); 
    	//↑ 代表js函数的val对象可以直接使用括号带参数调用,注意所有传给js的字符串需要使用string类型
    	cout << "已获取数据!" << endl;
    	cout << "body:" << endl
    		 << response.call<val>("text").await().as<string>() << endl; 
    	//↑ 代表js对象的val对象通过call方法调用对象上的方法,这里在类型模板中指定返回值也是val对象,
    	//↑ 因为response的text方法返回的是个js Promise对象,需要用val表示并await()才能获得结果
    	return 0;
    }

    编译一下,由于以上代码使用了await,所以编译参数要加上-sASYNCIFY,完整的命令是emcc -sMODULARIZE=1 -sASSERTIONS -sASYNCIFY -lembind -o dist/test.mjs test.cpp

    然后在start.js中直接加载wasm即可,main中的代码会自动执行:

    (async () => {
    	const { default: wasm } = await import('./dist/test.mjs');
    	const instance = await wasm();
    	setTimeout(()=>{},10000);//为了防止main函数执行完直接退出,这里加个timeout拖延时间
    	/* 控制台中将输出以下内容:
    		开始fetch数据
    		已获取数据!
    		body:
    		[获取到的网页代码]
    	*/
    })();

    这个过程中wasm环境将调用所在js线程环境中的各种方法来实现功能。

    val类位于emscripten命名空间下,使用前要记得 using namespace emscripten或 using emscripten::val。

    使用 val类 表示的 js对象或函数 和js中用法很像,因为该类对一些操作符进行了重载,除了一些无法重载的关键字使用了其它方式代替,下面进行简单的列举,具体用法可以看文档。

      • 获取js环境全局对象属性:val::global("属性名"),获取的可以是任何类型的属性,比如数值、字符串、函数、对象等
      • 创建一个空的js数组:val::array()
      • 创建一个空的js对象:val::object()
      • 创建一个js字符串(从utf8字符串字面量):val::u8string(const char *s)
      • 创建一个js字符串(从utf16字符串字面量):val::u16string(const char16_t *s)
      • 创建js中的undefined:val::undefined()
      • 创建js中的null:val::null()
      • 重新对val对象实例赋值:任意val对象=任意可以用来创建val类的数据类型
      • js对象的hasOwnProperty方法:表示js对象类型的val对象.hasOwnProperty("属性名")
      • js对象的typeof方法:任意val对象.typeof()
      • new对象:表示js类或构造函数的val对象.new_(...传递给constructor的参数)
      • 获取对象的属性:表示js对象的val对象["属性名"]
      • 设置对象的属性:表示js对象的val对象.set("属性名",值)
      • 调用函数:表示js函数的val对象(...函数参数)
      • 调用对象方法:表示js对象的val对象.call<返回值类型>("调用的方法名",...函数参数)(调用对象方法和一般函数不一样的地方是它的this需要指向该对象,所以不能直接像上一条那样调用)
      • 强制类型转换:任意val对象.as<目标类型>()
      • await:代表Promise或其它thenable的val对象.await(),注意使用await需要添加-sASYNCIFY编译参数

    文档里还有个co_await也是用来await promise类型val的,但我没能编译,不知道为啥。

    至此embind库(有官方教程)的功能基本介绍完了。



沪ICP备19023445号-2号
友情链接