快速上手 WebAssembly 应用开发:Emscripten 使用入门

在上一篇文章《WebAssembly 如何演进成为“浏览器第二编程语言”?》中, 我们较为详细地讲述了 WebAssembly 的演变历程,通过 WebAssembly 的演变历程,我们可以对 WebAssembly 的三个优点(二进制格式、Low-Level 的编译目标、接近 Native 的执行效率)有比较深刻的理解。
在本章中我们将选取 Emscripten 及 C/C++ 语言来简要讲述 WebAssembly 相关工具链的使用,通过较为简单的例子帮助大家更快速地上手 WebAssembly 相关的应用开发。请放心,在本章中我们将避免复杂难懂的 C/C++ 语言技巧,力求相关示例简单、直接、易懂。如果你有 Rust、Golang 等支持 WebAssembly 的相关语言背景,那么可以将本章相关内容作为参考,与对应官方工具链结合学习。
Emscripten 是 WebAssembly 工具链里重要的组成部分。从最为简单的理解来说,Emscripten 能够帮助我们将 C/C++ 代码编译为 ASM.js 以及 WebAssembly 代码,同时帮助我们生成部分所需的 JavaScript 胶水代码。
但实质上 Emscripten 与 LLVM 工具链相当接近,其包含了各种我们开发所需的 C/C++ 头文件、宏参数以及相关命令行工具。通过这些 C/C++ 头文件及宏参数,其可以指示 Emscripten 为源代码提供合适的编译流程并完成数据转换,如下图所示:
Emscripten 编译流程(来自官网)
emcc 是整个工具链的编译器入口,其能够将 C/C++ 代码转换为所需要的 LLVM-IR 代码,Clang/LLVM(Fastcomp)能够将通过 emcc 生成的 LLVM-IR 代码转换为 ASM.js 及 WebAssembly 代码,而 emsdk 及.emscripten 文件主要是用来帮助我们管理工具链内部的不同版本的子集工具及依赖关系以及相关的用户编译设置。
在我们的日常业务开发过程中,实际上并不需要太过关心 Emscripten 内部的实现细节,Emscripten 已经非常成熟且易于使用。但相关读者若想知道 Emscripten 内部的更多细节,可以访问 Emscripten 官网 以及 Github 阅读相关 WIKI 进一步了解。
在进行相关操作之前,请先确保已经安装 git 工具并能够使用基本的 git 命令,接下来我们以 Linux 系统下的操作作为示例演示如何下载、安装及配置 Emscripten。若你的操作系统为 Windows 或是 OSX 等其他系统,请参考官方文档中的相关章节进行操作。
-
安装
> git clone https://github.com/emscripten-core/emsdk.git
-
下载
> cd emsdk
> git pull
> ./emsdk install latest
> ./emsdk install 1.38.45
-
激活及配置
> ./emsdk activate latest # or ./emsdk activate 1.38.45
> source ./emsdk_env.sh
现在让我们执行 emcc -v
命令查看相关的信息,若正确输出如下类似信息则说明 Emscripten 安装及配置成功。
emcc -v 的相关信息输出
Hello World!
作为我们学习 WebAssembly 的第一个程序吧!让我们先快速编写一个 C/C++ 的打印 Hello World!
代码,如下所示:#include <stdio.h>
int main() {
printf("Hello World!n");
return 0;
}
> emcc main.c -o hello.html
执行完毕后你将得到三个文件代码,分别是:
-
hello.html
-
hello.js:相关的胶水代码,包括加载 WASM 文件并执行调用等相关逻辑
-
hello.wasm:编译得到的核心 WebAssembly 执行文件
Hello World!
在页面上正确输出了!当然,实际上 hello.html 文件并不是一定需要的,如果我们想要让 NodeJS 使用我们代码,那么直接执行:> emcc main.c
a.out.js
及 a.out.wasm
两个文件,然后我们使用 NodeJS 执行:> node a.out.js
也能正确的得到对应的输出(你可以自行创建 html 文件并引入 a.out.js
进行浏览器环境的执行 )。
当然,在我们的日常的业务开发中相关程序是不可能如此简单的。除了我们自己的操作逻辑外,我们还会依赖于非常多商用或开源的第三方库及框架。比如在数据通信及交换中我们往往会使用到 JSON 这种轻量的数据格式。在 C/C++ 中有非常多相关的开源库能解决 JSON 解析的问题,例如cJSON
等,那么接下来我们就增加一点点复杂度,结合 cJSON
库编一个简单的 JSON 解析的程序。
cJSON
的主页,然后下载相关的源码放置在我们项目的 vendor 文件夹中。接着我们在当前项目的根目录下创建一个CMakeList.txt
文件,并填入如下内容:cmake_minimum_required(VERSION 3.15) # 根据你的需求进行修改
project(sample C)
set(CMAKE_C_STANDARD 11) # 根据你的 C 编译器支持情况进行修改
set(CMAKE_EXECUTABLE_SUFFIX ".html") # 编译生成.html
include_directories(vendor) # 使得我们能引用第三方库的头文件
add_subdirectory(vendor/cJSON)
add_executable(sample main.c)
# 设置 Emscripten 的编译链接参数,我们等等会讲到一些常用参数
set_target_properties(sample PROPERTIES LINK_FLAGS "-s EXIT_RUNTIME=1")
target_link_libraries(sample cjson) # 将第三方库与主程序进行链接
CMakeList.txt
呢?简单来说,CMakeList.txt
是 CMake
的“配置文件”,CMake
会根据 CMakeList.txt
的内容帮助我们生成跨平台的编译命令。在我们现在及之后的文章中,不会涉及非常复杂的 CMake
的使用,你完全可以把 CMakeList.txt
里的相关内容当成固定配置提供给多个项目的复用,如若需要更深入的了解 CMake
的使用,可以参考 CMake
的 官网教程及文档。好了,现在让我们在代码中引入 cJSON
然后并使用它进行 JSON 的解析操作,代码如下:#include <stdio.h>
#include "cJSON/cJSON.h"
int main() {
const char jsonstr[] = "{"data":"Hello World!"}";
cJSON *json = cJSON_Parse(jsonstr);
const cJSON *data = cJSON_GetObjectItem(json, "data");
printf("%sn", cJSON_GetStringValue(data));
cJSON_Delete(json);
return 0;
}
CMake
,因此 Emscripten 的编译命令需要有一点点修改,我们将不使用 emcc 而是使用 emcmake 及 emmake 来创建我们的相关 WebAssembly 代码,命令如下:> mkdir build
> cd build
> emcmake cmake ..
> emmake make
我们创建了一个 build 文件夹用来存放 cmake 相关的生成文件及信息,接着进入 build 文件夹并使用 emcmake 及 emmake 命令生成对应的 WebAssembly 代码 sample.html、sample.js、sample.wasm,最后我们执行访问 sample.html 后可以看到其正确的输出了 JSON 的 data 内容。
如若你从未使用过 CMake,请不要为 CMake 的相关内容因不理解而产生沮丧或者畏难情绪。在我的日常的 WebAssembly 开发中,基本都是沿用一套
CMakeList.txt
并进行增删改,与此同时编译流程基本与上诉内容一致,你完全可以将这些内容复制在你的备忘录里,下次需要用到时直接修改即可。
#include <stdio.h>
int main() {
printf("Hello World!");
return 0;
}
> emcc -g4 main.c -o main.wasm # -g4 可生成对应的 sourcemap 信息
接着打开 Chrome 及其开发者工具,我们就可以看到对应的 main.c 文件并进行单步调试了。
使用 Chrome 进行单步调试
但值得注意的是,目前 emcmake 对于 soucemap 的生成支持并不是很好,并且浏览器的单步调试支持也仅仅支持了代码层面的映射关系,对于比较复杂的应用来说目前的单步调试能力还比较不可用,因此建议开发时还是以日志调试为主要手段。
#include <stdio.h>
#include "cJSON/cJSON.h"
int json_parse(const char *jsonstr) {
cJSON *json = cJSON_Parse(jsonstr);
const cJSON *data = cJSON_GetObjectItem(json, "data");
printf("%sn", cJSON_GetStringValue(data));
cJSON_Delete(json);
return 0;
}
json_parse
的函数之中,以便外部 JavaScript 能够顺利的调用得到此方法,接着我们修改一下 CMakeList.txt
的编译链接参数:#....
set_target_properties(sample PROPERTIES LINK_FLAGS "
-s EXIT_RUNTIME=1
-s EXPORTED_FUNCTIONS="['_json_parse']"
")
EXPORTED_FUNCTIONS 配置用于设置需要暴露的执行函数,其接受一个数组。这里我们需要将 json_parse
进行暴露,因此只需要填写 _json_parse
即可。需要注意的是,这里暴露的函数方法名前面以下划线(_)开头。然后我们执行 emcmake 编译即可得到对应的生成文件。
let jsonstr = JSON.stringify({data:"Hello World!"});
jsonstr = intArrayFromString(jsonstr).concat(0);
const ptr = Module._malloc(jsonstr.length);
Module.HEAPU8.set(jsonstr, ptr);
Module._json_parse(ptr);
在这里,intArrayFromString
、Module._malloc
以及 Module.HEAPU8
等都是 Emscripten 提供给我们的方法。intArrayFromString
会将字符串转化成 UTF8 的字符串数组,由于我们知道 C/C++ 中的字符串是需要