初识WebAssembly
WebAssembly,简称 WASM ,是一种新的低级二进制格式,它可以在所有主流的浏览器中使用,包括 Chrome,Firefox,Edge和Safari。WebAssembly最初由Mozilla、Google、微软和其他技术公司推出,它的目标是通过提供一种高性能、可移植和安全的方式来扩展Web平台,以便开发人员可以在Web上构建更加复杂和功能更加强大的应用程序。
WebAssembly与JavaScript不同,它是一种二进制格式,而不是文本格式,它需要使用虚拟机来解释字节码,同时提供与Web技术的交互。WebAssembly可以将C/C++、Rust等语言编写的代码编译成字节码格式,然后在浏览器中运行。WebAssembly可以通过使用JavaScript的API调用,也可以在WebAssembly模块之间进行通信。WebAssembly可以更快地加载、解析和执行,从而提供更好的性能和更低的内存占用。
Emscripten开发入门
WebAssembly需要将C/C++、Rust等语言编写的代码编译成字节码格式,然后在浏览器中运行。Emscripten是一个将C/C++代码转换为WebAssembly的工具集,是WebAssembly技术的重要推动者之一。Emscripten的核心是一个将LLVM字节码转换为JavaScript或WebAssembly字节码的编译器,称为LLVM到JavaScript的编译器。该编译器可以将C/C++代码编译为LLVM字节码,然后通过LLVM到JavaScript编译器将字节码转换为JavaScript或WebAssembly代码。通过利用Emscripten工具,开发者可以使用C/C++等语言来开发Web应用,利用WebAssembly的高性能、低资源消耗、跨平台等优势来提升Web应用的性能和用户体验。
使用Emscripten开发编译流程大致如下:
Emscripten安装
安装Emscripten需要使用emsdk脚本,emsdk脚本是基于python语言开发的,安装前需要确认有以下依赖项:git
,python
,Node.js
。
安装步骤如下:
在终端中输入以下命令
bash
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
git pull
./emsdk install latest
./emsdk activate latest
配置环境变量
bash
source ./emsdk_env.sh
安装完成后的目录
安装结束后进行版本校验,查看版本信息
emcc -v
Hello, Emscripten
接下来我们开始Emscripten开始之旅,新建一个hello.cpp
文件,代码如下:
c
// hello.cpp
#include <iostream>
using namespace std;
int main() {
cout << "Hello, World!" << endl;
return 0;
}
切换到当前工程目录,在控制台使用emcc
命令生成wasm文件,通过-o
选项指定输出文件:
emcc hello.cpp -o hello.js
编译完成后在目录中会生成hello.js
文件和hello.wasm
文件。 被编译成WebAssembly后的C/C++代码是不能直接运行的,需要通过浏览器加载运行。 在同一个目录,我们继续新建一个hello.html
文件,并引入hello.js
文件:
xml
// hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="hello.js"></script>
</body>
</html>
在浏览器中打开hello.html
文件,在开发者面板中的Console中能够看到hello.cpp
的输出内容。
WebAssembly是一种跨平台的字节码,不仅可以在浏览器中运行,也可以在node环境中运行:
WebAssembly和JavaScript交互
如果要实现一个实用功能的WebAssembly模块,必然需要提供能够和外部交互的函数接口。包括JavaScript调用C/C++函数功能,C/C++函数调用JavaScript方法,以及C/C++函数和JavaScript交换数据等需求。
Emscripten编译生成的js文件是一种胶水代码,用来加载WebAssembly模块和导出相关函数。生成的js代码中有一个全局对象Module,该全局对象是Emscripten的核心,负责WebAssembly模块的载入、创建初始化等,以及对C/C++函数进行封装等功能。在之前hello.js
中有对hello.cpp
中的main
函数的封装代码如下:
Emscripten会对封装导出的函数前面加上下划线,main()
函数会被封装成_main()
。我们可以在浏览器控制台中直接使用_main()
函数。
JavaScript调用C/C++函数
接下来我们新建一个export.cpp
文件,代码如下:
arduino
// export.cpp
#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
EM_PORT_API(int) add(int a, int b)
{
return a + b;
}
我们定义了一个EM_PORT_API
的宏定义,用来统一C和C++环境,方便用来导出函数。其中__EMSCRIPTEN__
是用来识别Emscripten环境,__cplusplus
是用来判断C++代码,EMSCRIPTEN_KEEPALIVE
是Emscripten的宏定义,用来优化编译的。
切换到工程目录,在控制台使用emcc命令生成wasm文件:
arduino
emcc export.cpp -o export.js
目录中生成文件:export.js
和export.wasm
。继续在浏览器中测试我们的代码,新建一个export.html
文件,代码如下:
xml
// export.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script async src="export.js"></script>
<script>
Module = {}
Module.onRuntimeInitialized = function() {
console.log(Module._add(123, 456))
}
</script>
</body>
</html>
WebAssembly实例是通过方法createWasm()
异步创建的,有可能js文件加载后,Emscripten运行环境没有准备好,我们需要通过建立一种Emscripten运行时准备就绪的机制。一种简单的方法就是使用onRuntimeInitialized
回调方法,在这个回调方法中运行WebAssembly Module导出的方法。
通过运行浏览器,我们可以在控制台查看JavaScript调用C++代码的结果:
C/C++函数调用JavaScript方法
反过来,我们也可以在C/C++函数调用JavaScript方法。首先需要创建一个call_js.cpp
文件,代码如下:
arduino
// call_js.cpp
#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
// declare the function in the cpp file
// function definition in js file
EM_PORT_API(int) js_add(int a, int b);
EM_PORT_API(void) js_console_log(int a);
// define the function in the cpp file
EM_PORT_API(void) call_js_func() {
int ret = js_add(11, 22);
js_console_log(ret);
}
我们在call_js.cpp
文件中声明了js_add
和js_console_log
两个函数,这两个函数/方法的具体实现是在js文件中。接着新建一个mylibrary.js
文件,代码如下:
javascript
// mylibrary.js
mergeInto(LibraryManager.library, {
js_add: function (a, b) {
console.log("js_add");
return a + b;
},
js_console_log: function(a) {
console.log("js_console_log: ", a);
}
})
按照在C++文件中声明的方法签名,在js文件中实现了js_add
和js_console_log
的方法功能,并合并注入到LibraryManager.library
中。LibraryManager.library
对象可以简单理解为是JavaScript注入到C/C++中的运行时库。
在控制台使用emcc命令生成wasm文件,需要使用--js-library
链接外部的js文件:
css
emcc call_js.cpp --js-library mylibrary.js -o call_js.js
继续新建一个call_js.html
文件用来测试代码,在浏览器查看我们的运行结果。
xml
// call_js.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script async src="./call_js.js"></script>
<script>
Module = {}
Module.onRuntimeInitialized = function() {
Module._call_js_func();
}
</script>
</body>
</html>
浏览器的运行结果如下:
总结
我们简单的展示了C/C++和JavaScript函数之间通过WebAssembly的相互调用,在这里并没有涉及到复杂的数据交换和相关的内存模型操作。WebAssembly是一个强大的Web应用技术,可以为Web应用程序提供更好的性能和更多强大的功能。Emscripten是一个用来开发WebAssembly应用的强大工具集。通过不断的学习WebAssembly开发技术,可以帮助开发人员在Web平台上构建更快、更安全、更可移植的应用程序。