WebAssembly 学习(二)
这一部分介绍如何使用Emscripten来编译C/C++模块。手册原文参考在这里。
Emscripten 环境设置
首先,先配置所需的开发环境。
一,先下载Emscripten SDK,参考这个链接。
- 先克隆emsdk库到本地
git clone https://github.com/emscripten-core/emsdk.git
- 进入emsdk 文件夹
cd emsdk
- 抓取最新的emsdk版本(如果是第一次抓取emsdk这个步骤可以省略)
git pull
- 下载并安装最新的SDK工具
如果是linux系统使用指令(在我的电脑powershell也是使用下面的指令):
./emsdk install latest
如果是windows则使用(CMD中使用),所有./emsdk 都把./省略:
emsdk install latest
PS:我自己在解决这个问题的时候,发现因为之前安装过一个中间层cygwin导致没有办法正常运行这个指令。
参考别人的资料,需要把在环境变量PATH中把cygwin的路径变量移除。但是事实上,根据PATH的工作方式,匹配则使用该路径,只需要把有关cygwin的路径往后移,移动到最后则优先使用Windows自身的命令环境。如果下次遇到需要使用cygwin的环境则可以直接调整路径优先级。
- 为当前用户启动最新的SDK(写入.emscripten file)
./emsdk activate latest
该步骤运行过程出现*表达式或语句中包含意外的标记“C:\Program”。的错误
这种问题就是在系统环境变量中,有的环境变量值没有分开一条一条写直接用分号*;**隔开了,这个做法不行,解决办法是把每一条路径变量都分开写。重新设置环境变量后需要把命令终端重启。
- 初始化并启动PATH以及其他环境变量(在当前的终端下)
source ./emsdk_env.sh
Windows下使用指令(PowerShell下也是下面的指令,使用上面activate指令后,下面的指令也可以省略):
emsdk_env.bat
注意:如果改变了SDK的位置后,需要重新使用activate指令或者source指令更新环境。
二 编译一个例程
设置好环境后,我们来看看如何用它来编译一个C语言的例子到Emscripten。在用Emscripten编译时有很多选择,但我们主要讨论的是两种情况。
- 编译成wasm并创建HTML来运行我们的代码,加上在web环境中运行wasm所需的所有JavaScript "胶水"代码。
- 编译到wasm,只创建JavaScript。
下面逐个介绍。
创建HTML和JavaScript的场景
这是我们要看的最简单的情况,即让emscripten生成你需要的一切,以WebAssembly的形式在浏览器中运行你的代码。
- 首先,我们需要一个例子来编译。复制下面这个简单的C语言例子,并把它保存在本地驱动器的一个新目录下,名为hello.c的文件。
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
- 现在,使用你用来进入Emscripten编译器环境的终端窗口,导航到与hello.c文件相同的目录,并运行以下命令。
emcc hello.c -o hello.html
我们在命令中传入的选项如下:
-o hello.html
这个参数指定我们希望Emscripten生成一个HTML页面来运行我们的代码(和一个文件名),以及wasm模块和用于编译和实例化wasm的JavaScript "胶水 "代码,以便在web环境中使用。
运行上面的指令后,在你的源代码目录中你应该有:
- 二进制wasm模块代码(hello.wasm)
- 一个包含胶合代码的 JavaScript 文件,用于在本地 C 功能和 JavaScript/wasm 之间进行转换 (hello.js)
- 一个HTML文件,用于加载、编译和实例化你的wasm代码,并在浏览器中显示其输出(hello.html)
- 运行例程
现在剩下的就是让你在一个支持WebAssembly的浏览器中加载生成的hello.html。它在Firefox 52、Chrome 57、Edge 57、Opera 44中都是默认启用的。
注意:如果你试图直接从你的本地硬盘打开生成的HTML文件(hello.html)(例如file://your_path/hello.html),你会得到一个错误信息,大意是async和sync获取wasm都失败。你需要通过一个HTTP服务器(http://)来运行你的HTML文件–更多信息请参见如何建立一个本地测试服务器?
直接测试本地文件的问题
如果你把一些例子作为本地文件打开,它们就不会运行。这可能是由于各种原因造成的,最可能的是:
- 它们具有异步请求的特点。如果你只是从本地文件运行例子,一些浏览器(包括Chrome)不会运行异步请求(见从服务器获取数据)。这是因为安全限制(关于网络安全的更多信息,请阅读网站安全)。
- 它们具有服务器端语言的特点。服务器端语言(如PHP或Python)需要一个特殊的服务器来解释代码并提供结果。
- 它们包括其他文件。浏览器通常将使用
file://
模式加载资源的请求视为跨源请求。因此,如果你加载一个包括其他本地文件的本地文件,这可能会触发一个 CORS 错误。
PS: CORS 全称是跨域资源共享(Cross-Origin Resource Sharing),是一种 AJAX 跨域请求资源的方式,支持现代浏览器,IE支持10以上。 CORS与JSONP的使用目的相同,但是比JSONP更强大。 JSONP只支持GET请求,CORS支持所有类型的HTTP请求。
搭建简单的本地HTTP服务器
为了解决异步请求的问题,我们需要通过本地网络服务器运行这些例子来测试。
可以使用很多中方式来搭建一个简单的服务器,但是使用emscripten的指令emrun可以快速默认开启一个python本地服务器。
在对应的工作环境里面,运行指令:emrun FILENAME.html
则可以使用默认浏览器访问本地浏览器。
其他方法参考这个链接。
使用自定义的HTML模板(custom HTML template)
有时你会想使用一个自定义的HTML模板。让我们来看看我们如何做到这一点。
- 源码继续使用上面的简单代码
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
- 在emsdk 的库文件里面找到一个hmtl模板
html_template
把它复制到上面的工作路径中 - 在激活EMscripten的编译环境中运行以下指令:
emcc -o FILENAME.html hello.c -O3 --shell-file html_template/shell_minimal.html
这次我们传递的参数略有不同。
- 我们指定了 -o hello2.html,意味着编译器仍然会输出 JavaScript 胶合代码和 .html。
- 我们指定了**-O3**,这是用来优化代码的。Emcc和其他C编译器一样有优化级别,包括。-O0(无优化),-O1,-O2,-O,-Oz,-Og,和-O3。对于发布版本来说,-O3是一个很好的设置。需要注意的是这里面的是大写字母O+数字
- 我们还指定了 --shell-file html_template/shell_minimal.html --这提供了你想用来创建HTML模板的路径,你将通过该模板运行你的例子。
- 现在我们来运行这个例子,还是使用
emrun FULENAME.HTML
指令。上面的命令将生成.html,它的内容与模板基本相同,并添加了一些胶水代码来加载生成的wasm,运行它,等等。在你的浏览器中打开它,你会看到和上一个例子差不多的输出。
注意:你可以指定只输出JavaScript "胶水 "文件*,而不是完整的HTML,方法是在-o标志中指定一个.js文件而不是HTML文件,例如:emcc -o hello2.js hello2.c -O3。然后你可以完全从头开始建立你的自定义HTML,尽管这是一个高级的方法;通常使用提供的HTML模板会更容易。
Emscripten需要大量的JavaScript "胶水 "代码来处理内存分配、内存泄漏和一系列其他问题
调用在C语言中定义的函数
如果你在C代码中定义了一个函数,你想在需要时从JavaScript中调用,你可以使用Emscripten ccall()
函数和EMSCRIPTEN_KEEPALIVE
声明(它将你的函数添加到导出的函数列表中(见为什么我的C/C++源代码中的函数在我编译到JavaScript时消失了,和/或我得到没有函数可以处理?)
让我们来看看这是如何工作的。
- 首先创建一份代码,源码如下:
#include <stdio.h>
#include <emscripten/emscripten.h>
int main() {
printf("Hello World\n");
return 0;
}
#ifdef __cplusplus
#define EXTERN extern "C"
#else
#define EXTERN
#endif
EXTERN EMSCRIPTEN_KEEPALIVE void myFunction(int argc, char ** argv) {
printf("MyFunction Called\n");
}
默认情况下,Emscripten生成的代码总是只调用main()
函数,而其他函数则作为死代码被排除在外。在函数名前加上 EMSCRIPTEN_KEEPALIVE
可以阻止这种情况的发生。你还需要导入emscripten.h
库来使用EMSCRIPTEN_KEEPALIVE
。
注意:我们包括
#ifdef
块,这样如果你想把它包含在C++代码中,这个例子仍然可以工作。由于C语言相对于C++语言的重载规则(name mangling),如果不这样设置,这里会出现问题,如果你使用的是C++语言,它将被视为一个外部C函数。关于#ifdef __cplusplus
的更多内容参考链接。
- 设置自定义HTML模板,把需要使用的自定义HMTL模板放到相应的文件夹路径下,这里继续使用上面的
shell_minimal.html
模板。 - 运行编译指令。在你的最新目录中(在Emscripten编译器环境的终端窗口中),用以下命令编译你的C代码。(注意,我们需要用
NO_EXIT_RUNTIME
编译,这是必要的,否则当main()退出时,运行时将被关闭–对于正确的C语言模拟是必要的,例如,atexits被调用–并且调用编译后的代码是无效的。)
emcc -o hello3.html hello.c --shell-file html_template/shell_minimal.html -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']"
此处编译完成后使用emrun运行HTML页面将得到与之前一致的结果。
- 现在通过在Javascript中运行调用我们的函数,打开编译得到的hello3.html文件。在脚本
<script type='text/javascript'>
前添加按钮触发脚本
<button id="mybutton">Run myFunction</button>
- 把下述脚本代码添加到第一个
<script>
标签的结尾处即可
document.getElementById("mybutton").addEventListener("click", () => {
alert("check console");
const result = Module.ccall(
"myFunction", // name of C function
null, // return type
null, // argument types
null // arguments
);
});
点击按钮后触发结果如下:
上述内容便是ccall()
的运行方式。
总结
这个内容主要是讲了如何用Emscripten编译基本的C/C++代码,并通过javascript调用自定义的函数。下一步将开始根据自己的代码进一步学习每个函数是如何进行编译调用。首先从本地文件的加载开始。