WebAssembly是一种运行在浏览器虚拟机中的新的机器码指令格式,简称wasm。emscripten就是可以将C/C++,JAVA等高级语言代码库编译为wasm的一种编译工具。为了能够在前端高效应用大量现存的C/C++库完成音视频,游戏,机器视觉等计算密集型的功能。简单的说,因为浏览器只能跑js代码,wasm的出现就是为了其他语言的代码也可以在浏览器里运行,emscripten就是可以将其他语言编译成wasm的编译工具。
之前接到一个需求,将服务器的一部分逻辑放到客户端,但是这部分代码有涉及使用protobuf。于是在emcc编译时要链接到对应的lib库,而之前用make编译好的libprotobuf并不能用。
解决方案
这里说下处理方式:
找到protobuf源码包用emscripten编译,不要直接用protobuf官方github的源码包!网上看到3.9.1有一个人为emscripten加了patch,我用的这个。这一步是为了编译出emcc可以链接的libprotobuf和include头文件。参考地址,相关指令:
1
2
3
4
5
6
7
8
9git clone https://github.com/protocolbuffers/protobuf protobuf-wasm
cd protobuf-wasm
git checkout v3.9.1
git clone https://github.com/kwonoj/protobuf-wasm wasm-patches
cd wasm-patches && git checkout 4bba8b2f38b5004f87489642b6ca4525ae72fe7f && cd ..
git apply wasm-patches/*.patch
./autogen.sh
emconfigure ./configure CXXFLAGS="-O3"
emmake make从protobuf官方的github下载3.9.1(为了和上面版本对齐)的源码包,正常用make编译出protoc,这一步是为了用protoc将proto文件编成xxx.h和xxx.cc
- 编译项目,链接时指定相关目录,pb相关的include头文件用第一步生成的,lib也是。比如我的编译指令(注意链接lib库前面不加参数!):
em++ src/handler/.cpp src/module/.cpp src/util/.cpp src/gen_protocol/.cc src/story_line/StoryLine/src/.cpp src/story_line/StoryLine/src/loader/.cpp -I ./src/ -I ./src/story_line/StoryLine/include/ -I ./src/story_line/StoryLine/src/ -I ./src/story_line/StoryLine/src/loader/ -I ./src/handler/ -I ./src/module/ -I ./src/util/ -I ./src/story_line/thirdparty/ -I /home/chauncyli/protobuf-wasm/src/ /home/chauncyli/protobuf-wasm/src/.libs/libprotobuf.a -std=c++11 -o 2130test.js –embed-file res -s WASM=1 –bind -g3
坑点
编译报错
- 有无数的undefined reference的报错,检查下用到的包是不是因为一个是用的c++编译器,一个用的c编译器,c++编译器是会改变原来函数/变量名的(加前后缀)。通过报错信息可以很清晰的看出来
protoc编译出现:
google/protobuf/descriptor.proto: File not found.
illusion.proto: Import “google/protobuf/descriptor.proto” was not found or had errors.
illusion.proto:8:8: “google.protobuf.MessageOptions” is not defined.
illusion.proto: “google.protobuf.MessageOptions” is not defined.
illusion.proto: “google.protobuf.MessageOptions” is not defined.
illusion.proto: “google.protobuf.MessageOptions” is not defined.
illusion.proto: “google.protobuf.MessageOptions” is not defined.
illusion.proto: “google.protobuf.MessageOptions” is not defined.
illusion.proto: “google.protobuf.MessageOptions” is not defined.
illusion.proto: “google.protobuf.MessageOptions” is not defined.
illusion.proto: “google.protobuf.MessageOptions” is not defined.
illusion.proto:33:8: “google.protobuf.FieldOptions” is not defined.
illusion.proto: “google.protobuf.FieldOptions” is not defined.
illusion.proto: “google.protobuf.FieldOptions” is not defined.
illusion.proto: “google.protobuf.FieldOptions” is not defined.
illusion.proto: “google.protobuf.FieldOptions” is not defined.
illusion.proto: “google.protobuf.FieldOptions” is not defined.
illusion.proto: “google.protobuf.FieldOptions” is not defined是因为protoc文件与其include下的google目录不在同一个目录下,可以拷贝protoc到与google同一级目录下
protoc编译出现:
–proto_path passed empty directory name. (Use “.” for current directory.)
需要cd到proto文件的目录下,然后执行protoc编译!有点搓,即使在其他目录下-I指定proto文件cd也貌似没用
文件加载。
C/C++代码中有读取文件的话,emscripten也是支持的,且C/C++中不需要修改代码。emscripten的文件加载有两种方式:preload 和 embed(–preload-file,–embed-file),embed是将文件序列化后嵌入到生成的js代码中;preload是将文件分别进行打包到emscripten自己的virtual file system中,C/C++使用层无感知,js也无感知。embed的效率远不及preload,所以embed比较适用于少量的小文件。
使用–preload-file打包文件的话生成的除了js胶水代码文件和wasm字节码文件,还会有一个xxx.data,这个就是我们打包的文件,如果打包的是一个文件夹,则该data文件内容就是文件夹下所有的文件内容拼接到一起。可以使用@符合改变文件路径的映射,比如打包当前目录下的res1/, C/C++中读取路径是”/res2/“,编译时参数可以为
--preload-file res1/@/res2/
返回值
- C/C++和js通信如果用的是二进制还有点不同,C/C++用char数组来进行存储,js侧用的是uint数组,如此一来,如果C/C++返回一个二进制字符串,则js侧接收到如果中间有’\0’会进行一个裁剪,后面的内容会丢弃(可以加len验证),这样再进行反序列化就会出错,目前我的做法是在C/C++层将char数组遍历挨个减去’0’转成uint,然后将uint数组传给js,这样可以正常反序列化,可能还有别的更好的方式,目前想到的是这种方法。
返回自定义对象。函数可以返回自定义的class/struct,需要在EMSCRIPTEN_BINDINGS中进行注册.比如返回一个struct:
1
2
3
4
5
6
7
8
9
10
11
12
13
14typedef struct Res {
Res() {
succeed = true;
value = "";
error_msg = "";
ivalue.clear();
value_len = 0;
}
bool succeed;
std::string value;
std::string error_msg;
std::vector<uint32_t> ivalue;
uint32_t value_len;
} Res;EMSCRIPTEN_BINDINGS中需要注册该struct的所有成员:
1
2
3
4
5
6
7emscripten::class_<Res>("Res")
.constructor()
.property("succeed", &Res::succeed)
.property("error_msg", &Res::error_msg)
.property("value", &Res::value)
.property("value_len", &Res::value_len)
.property("ivalue", &Res::ivalue);但是返回值中不可以有数组或者指针,不管直接返回还是在struct中都不可以。
- 返回vector/map。这两种类型emscripten提供注册函数可以注册,js中即可以直接使用,参考地址:
1
2
3
4
5
6
7
8
9emscripten::register_vector<uint32_t>("vector<uint32_t>");
emscripten::register_map<uint32_t, int>("map<uint32_t, int>");
```
#### 参数
- C/C++中有参数引用,函数中可以直接修改这个引用的值,这样函数返回时可以减少一次值拷贝,效率比较高。但是可能js不支持,经验证,所有涉及函数修改参数引用的值时编译都会报错`internal::BindingType<Args>::fromWireType(args)`,只能是const的引用来对该参数只读操作。指针类似,涉及修改参数的都会报错。
#### 使用
我是将C/C++代码写到一个类中,让客户端童鞋可以用js通过我定义的类的一些接口实现数据交互。这里需要注册一下类以及对应的接口,比如:
emscripten::class_
.constructor()
.function(“InitPlayer”, &CSStorylineHandler::InitPlayer, emscripten::allow_raw_pointers())
.function(“GetParseString”, &CSStorylineHandler::GetParseString, emscripten::allow_raw_pointers())
.function(“GetCurrentPath”, &CSStorylineHandler::GetCurrentPath, emscripten::allow_raw_pointers());
1 | 这样在客户端通过定义这个类的对象就可以访问这里注册的接口了: |
var obj = new Module.CSStorylineHandler()
obj.InitPlayer()
var res = obj.GetParseString(arg1, arg2)
var path = obj.GetCurrentPath(arg1, arg2)
1 |
|
docker pull chauncyli/emscripten_ci:0.0.1
docker container run image_id -it /bin/bash
#记得source一下emsdk
source emsdk/emsdk_env.sh
```
感觉emscripten应用前景很广,比如一些库有C/C++实现,如果JS的性能不好完全可以用emscripten将C/C++的库编译成对应的wasm,然后在浏览器中直接用js代码调用对应的C/C++接口(参考地址)就可以达到接近C/C++原生的运行速度。