WebAssembly技术进阶之路(官方文档翻译)

WebAssembly技术

——C/C++Javascript之路

  • 案例描述

随着对无插件预览性能要求的越来越高,原来的Emscripten asm.js技术已显不足,但目前一种新技术——WebAssembly正日趋成熟,WebAssembly可以将低级别编程语言(包括C和C++)编译成二进制字节码,可以较大的提升浏览器的性能。

  • 案例分析和解决

WebAssembly作为一种新兴的Web技术,正在快速演变中,Google、苹果、微软和Mozilla的正在联合开发WebAssembly。WebAssembly(wasm)是一种可用于浏览器中的字节码(bytecode),可使浏览器性能提升20倍。字节码是一种机器可读的指令集,与脚本语言相比,字节码的加载速度更快。WebAssembly项目旨在开发全新的字节码,从而让桌面和移动端浏览器变得更高效。使用WebAssembly,我们可以在浏览器中运行一些高性能、低级别的编程语言,可用它将大型的C和C++代码库比如游戏、物理引擎甚至是桌面应用程序导入Web平台。截至目前为止,我们已经可以在Chrome、Firefox中使用WebAssembly,Edge和Safari对它的支持也基本完成。

  1. WebAssembly编译环境配置

我们使用 Emscripten 将 C 代码编译为 wasm 格式,官方推荐的方式是首先下载 Portable Emscripten SDK for Linux and OS X (emsdk-portable.tar.gz) 然后利用 emsdk 进行安装:

 

$ git clone https://github.com/juj/emsdk.git

$ cd emsdk

$ ./emsdk install latest

$ ./emsdk activate latest

$ source ./emsdk_env.sh --build=Release

新版Emscripten已经包含了-s WASM=1编译选项,通过该选项可以将C/C++代码直接编译成wasm文件。

除此之外,我们还可以通过官方提供的另外两个工具将.js文件转换成.wasm,工具分别为Binaryen和WABT (WebAssembly Binary Toolkit),安装方法可参考官方 Developer’s Guide 和 Advanced Tools

虽然Emscripten能生成asm.js和wasm,但是却不能把asm.js转成wasm。因为它是基于LLVM的,然而asm.js没法编译成LLVM IR (Intermediate Representation)。想要把asm.js编译成WebAssembly,就要用到官方提供的Binaryen和WABT (WebAssembly Binary Toolkit)工具了。原理和编译方法参考官方文档,整个过程如下:

 

Binaryen           WABT

math.js   --->   math.wast   --->   math.wasm

用脚本描述如下:

 

$ asm2wasm math.js -o math.wast

$ wast2wasm math.wast -o math.wasm

  1. 如何将C/C++代码编译成.wasm

使用C语言来编写WebAssembly模块,并将其编译成.wasm文件。这些.wasm文件并不能直接被浏览器识别,所以它们需要一种称为JavaScript胶接代码。

首先编写一段C代码,保存为test.c,代码如下:

 

#include <stdio.h>

#ifdef __cplusplus

extern "C" {

#endif

void myFunction() {

  printf("MyFunction Called\n");

}

#ifdef __cplusplus

}

#endif

使用Emscripten编译器,采用如下指令进行编译,将test.c代码编译成test.wasm文件,同时还会生成一个test.js文件,该文件就相当与胶接代码,在Javascript中加载该文件,既可以调用用C代码中的接口。

 

$ emcc -o test.js test.c -O3 -s WASM=1 -s EXPORTED_FUNCTIONS="['_myFunction']"

各个参数含义如下:

  • emcc——代表Emscripten编译器;
  • test.c——包含C代码的文件;
  • -s WASM=1——指定使用WebAssembly
  • -O3——代码优化级别,3已经是很高的级别了;
  • -o test.js——指定生成包含wasm模块所需的全部胶接代码的JS文件;

更多关于WASM编译请查看官方文档:

https://developer.mozilla.org/en-US/docs/WebAssembly/C_to_wasm

  1. 如何使用Embind绑定C/C++结构体

Embind用于绑定C++函数和类到JavaScript,这样编译代码就能在js中以一种很自然的方式来使用,需要在C/C++代码中添加#include <emscripten/bind.h>头文件。使用EMSCRIPTEN_BINDINGS()块来创建函数、类、值类型、指针(包括原始和智能指针)、枚举和常量的绑定,本节主要介绍如何绑定在C/C++方法中经常作为参数或返回值的结构体;

首先新建一个example.cpp文件,代码如下:

 

#include <emscripten/bind.h>

using namespace emscripten;

struct Point {

    int x;

    int y;

};

Point getPoint() {

    Point point = {0};

       point.x = 100;

       point.x = 200;

      

       return point;

}

EMSCRIPTEN_BINDINGS(my_module) {

    value_object<Point>("Point")

        .field("x", & Point::x)

        .field("y", & Point::y)

        ;

             

       function("_getPoint", &getPoint);

}

使用embind编译上例,请调用emcc的bind选项,编译指令如下:

 

$ emcc --bind -o example.js example.cpp -O3 -s WASM=1 -s EXPORTED_FUNCTIONS="['_myFunction']"

在JavaScript中调用如下:

 

var oPoint = Module._getPoint();

var ix = oPoint.x;

var iy = oPoint.y;

更多关于Embind技术请查看官方文档:https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/embind.html

  1. 如何在Javascript层中调用C/C++接口

JavaScript中调用编译的C函数的最简单的方法是使用ccall()和cwrap()。其中ccall()用具体的参数和返回值调用一个编译的C函数,而cwrap()是一个编译的C函数的包裹,调用它会返回一个JavaScript可以调用的函数。如果打算多次调用一个函数的话,cwrap()用处更大。

例如下面代码是hello_function.cpp文件:

 

#include <math.h>

extern "C" {

int int_sqrt(int x) {

    return sqrt(x);

}

}

使用的编译命令为:

 

$ emcc --bind -o example.js example.cpp -O3 -s WASM=1 -s EXPORTED_FUNCTIONS="['_myFunction']"

编译完,下面使用ccall()在JavaScript中调用C/C++代码中的方法,下面代码就是直接用int_sqrt计算28的开方(结果为5):

 

// 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

返回类型和参数类型中可用类型有三个,分别是number,string和array。number(是js中的number,对应C中的整型,浮点型,一般指针),string(是JavaScript中的string,对应C中代表字符串的char*),array(是js中的数组或类型数组,对应C中的数组;如果是类型数组,必须为Uint8Array或者Int8Array)。

cwrap()与ccall()类似,下面代码就是直接用int_sqrt计算28的开方:

 

int_sqrt = Module.cwrap('int_sqrt', 'number', ['number'])

int_sqrt(28)

第一个参数是函数名,第二个参数是函数返回类型,第三个是参数类型。

除上述方法外,JavaScript还可以“直接”调用编译的C/C++代码。C中的函数编译为js中函数后,其实可以直接调用,比如C中有个a(),在编译后js用_a()来调,与ccall()和cwarp()相比,直接调用会稍微复杂点,但速度更快。

 

var result = Module._int_sqrt(28)

更多关于C/C++与Javascript交互技术请查看官方文档:

https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html

  1. 如何从C / C ++调用JavaScript

Emscripten提供两种方法让C/C++调用JavaScript,一种是使用 emscripten_run_script()运行js脚本,另一种是编写“inline JavaScript”。

emscripten_run_script()方式最直接,但略慢,它是通过eval()来实现的。例如在C代码中插下面一行代码,编译后就能在浏览器弹出alert()。

 

#include <emscripten/emscripten.h>

int callJS() {

  emscripten_run_script("alert('hi')");

  return 1;

}

inline JavaScript”方式是使用EM_ASM()编写,相比这种方法稍微快些。使用这种方式实现上面的“alert”,代码如下:

 

#include <emscripten/emscripten.h>

int callJS() {

  EM_ASM(

    alert('hello world!');

  );

  return 1;

}

此外,如果在C中想传值给JavaScript,那就用EM_ASM_(比EM_ASM多了“_”),代码如下:

 

#include <emscripten/emscripten.h>

int callJS() {

  EM_ASM_({

    Module.print('I received: ' + $0);

  }, 100);

  return 1;

}

如果有返回值,可以用EM_ASM_INT,代码如下:

 

#include <stdio.h>

#include <emscripten/emscripten.h>

int callJS() {

  int x = EM_ASM_INT({

    Module.print('I received: ' + $0);

    return $0 + 1;

}, 100);

printf("%d\n", x); 

return 1;

}

注意:

  1. 如果返回值是int或double,你要指定不同宏,是EM_ASM_INT还是EM_ASM_DOUBLE;
  2. 输入参数用$0,$1等形式表示;
  3. 使用 EM_ASM 注意用‘’,不要用“”,否则会有语法错误;
  4. 返回值用于js传给c数据;

上述C代码编译命令如下:

 

$ emcc -o my_js.js my_js.c -s EXPORTED_FUNCTIONS="['_ callJS ']"

更多相关技术可查看官方文档:

https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#calling-javascript-from-c-c

  1. 如何在Javascript中实现C/C++ API

可以在JavaScript中实现一个C / C ++ API, 许多Emscripten库中都使用了这种方法,如SDL1和OpenGL。可以编写js API 让C/C++来调用,为实现它,需要定义接口,并用extern来标记它是个外部API。然后,将其定义添加到library.js(默认情况下添加到library.js,当然也可以添加到自己编写的文件中),即可在JavaScript中实现这些符号。编译C代码时,编译器会在JavaScript库中查找相关的外部符号。

下面主要介绍将Javascript实现添加到自定义文件中的方法,例如,新建my_js.c文件,C部分代码如下:

 

#include <stdio.h>

extern void my_js(void);

extern int js_add(int a, int b);

int JS_implement_C () {

  int res = js_add(4, 5);

  printf("res = %d\n", res);

  my_js();

  return 1;

}

然后在自定义文件中Javascript接口实现代码如下:

 

mergeInto(LibraryManager.library, {

  my_js: function(a, b) {

    alert('hi');  // Javascript库API

  },

  js_add: function(a, b) {

    var c = add(a, b);   // 用户在Javascript工程中自定义方法

    return c;

  },

});

C/C++代码在进行emcc编译时添加--js-library选项,编译指令如下:

 

$ emcc -o my_js.js my_js.c -s EXPORTED_FUNCTIONS="['_JS_implement_C']" --js-library mylibrary.js

这样就可以在Javascript中实现C/C++的方法,通过该方式可以在C/C++中回调Javascript接口。

更多相关技术可查看官方文档:

https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#implement-a-c-api-in-javascript

  • 经验总结

asm.js与WebAssembly 的异同:

  • asm.js是文本,可读性高,比较直观;另外,一般浏览器都支持 asm.js,存在兼容性问题小;
  • WebAssembly是二进制字节码,因此运行速度比asm.js更快、体积也更小;由于WebAssembly是目前较新的技术,对浏览器的兼容性比asm.js差,但在Chrome、Firefox中都可以使用;

猜你喜欢

转载自blog.csdn.net/pkx1993/article/details/82015730