Emscripten

Emscriptrn

EmscriptenWebAssembly工具链里重要的组成部分。从最为简单的理解来说,Emscripten能够帮助我们将C/C++代码编译为ASM.js以及WebAssembly代码,同时帮助我们生成部分所需的JavaScript胶水代码。

但实质上EmscriptenLLVM工具链相当接近,其包含了各种我们开发所需的C/C++头文件、宏参数以及相关命令行工具。通过这些C/C++头文件及宏参数,其可以指示Emscripten为源代码提供合适的编译流程并完成数据转换,如下图所示:

Emscriptrn 架构

emcc是整个工具链的编译器入口,其能够将C/C++代码转换为所需要的LLVM-IR代码,Clang/LLVM(Fastcomp)能够将通过emcc生成的LLVM-IR代码转换为ASM.jsWebAssembly代码,而emsdk.emscripten文件主要是用来帮助我们管理工具链内部的不同版本的子集工具及依赖关系以及相关的用户编译设置。

环境配置

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

# 也可以直接拉取代码
$ git clone https://github.com/emscripten-core/emsdk.git

$ ./emsdk update
$ ./emsdk install latest
# 如果出现异常使用 ./emsdk install sdk-1.37.12-64bit
# https://github.com/kripken/emscripten/issues/5272

安装完毕后激活响应环境即可以进行编译:

$ ./emsdk activate latest
$ source ./emsdk_env.sh# you can add this line to your .bashrc

到这里基本环境已经配置完毕,我们可以对简单的counter.c进行编译,源文件如下:

int counter = 100;

int count() {
  counter += 1;
  return counter;
}
$ emcc counter.c -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm

# 如果出现以下错误,则是由如下参数
# WebAssembly Link Error: import object field 'DYNAMICTOP_PTR' is not a Number
emcc counter.c -O1 -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm

这样我们就得到了WebAssembly代码:

Some WebAssembly code

使用Docker

如果在本地执行上述搭建步骤时一直失败,可以改用Docker预先配置好的镜像进行处理:

# 拉取 Docker 镜像
docker pull 42ua/emsdk

# 执行编译操作
docker run --rm -v $(pwd):/home/src 42ua/emsdk emcc hello_world.c

对应的Dockfile如下所示,我们可以自行修改以适应未来的编译环境:

FROM ubuntu

RUN \
	apt-get update && apt-get install -y build-essential \
	cmake python2.7 python nodejs-legacy default-jre git-core curl && \
	apt-get clean && \
\
	cd ~/ && \
	curl -sL https://s3.amazonaws.com/mozilla-games/emscripten/releases/emsdk-portable.tar.gz | tar xz && \
	cd emsdk-portable/ && \
	./emsdk update && \
	./emsdk install -j1 latest && \
	./emsdk activate latest && \
\
	rm -rf ~/emsdk-portable/clang/tag-*/src && \
	find . -name "*.o" -exec rm {} \; && \
	find . -name "*.a" -exec rm {} \; && \
	find . -name "*.tmp" -exec rm {} \; && \
	find . -type d -name ".git" -prune -exec rm -rf {} \; && \
\
	apt-get -y --purge remove curl git-core cmake && \
	apt-get -y autoremove && apt-get clean


# http://docs.docker.com/engine/reference/run/#workdir
WORKDIR /home/src

编译命令如下所示,如果本地安装好了emcc则可以直接使用,否则使用Docker环境进行编译:

$ docker run --rm -v $(pwd):/home/src 42ua/emsdk emcc counter.c -s WASM=1 -s SIDE_MODULE=1 -o counter.wasm

编译参数

Emscripten包含了非常丰富的相关设置参数帮助我们在编译和链接时优化我们的代码。其中部分常用的参数包括:

  • -O1、-O2、-O3、-Oz、-Os、-g等:编译优化,具体可参考Emscripten官网相关章节;
  • -s ENVIRONMENT:设定编译代码的可执行环境,默认值为"web,work,node"
  • -s SINGLE_FILE:是否将ASM.jsWebAssembly代码以Base64的方式嵌入到JavaScript胶水代码中,可取值0/1
  • -s WASM:是否编译为WebAssembly代码,0编译为ASM.js1编译为WebAssembly
  • -s FETCH:是否启用Fetch模块,可取值0/1
  • -s DISABLE_EXCEPTION_CATCHING:禁止生成异常捕获代码,可取值0/1
  • -s ERROR_ON_UNDEFINED_SYMBOLS:编译时出现Undefined Symbols后是否退出,可取值0/1
  • -s EXIT_RUNTIME:执行完毕 main 函数后是否退出,可取值0/1
  • -s FILESYSTEM:是否启用File System模块,可取值0/1
  • -s INVOKE_RUN:是否执行C/C++main函数,可取值0/1
  • -s ASSERTIONS:是否给运行时增加断言,可取值0/1
  • -s TOTAL_MEMORY:总的可用内存使用数,可取以16777216为基数的整数值;
  • -s ALLOW_MEMORY_GROWTH:当可用内存不足时,是否自动增长,可取值0/1
  • -s EXPORTED_FUNCTIONS:暴露的函数列表名称;
  • -s LEGACY_VM_SUPPORT:是否增加部分兼容函数以兼容低版本浏览器(iOS9、老版本Chrome,可取值0/1
  • -s MEM_INIT_METHOD:是否将.mem文件以Base64的方式嵌入到JavaScript胶水代码中,可取值0/1
  • -s ELIMINATE_DUPLICATE_FUNCTIONS:将重复函数进行自动剔除,可取值0/1
  • closure:是否使用Google Closure进行最终代码的压缩,可取值0/1
  • –llvm-lto:是否进行LLVM的链接时优化,可取值0-3
  • –memory-init-file:同-s MEM_INIT_METHOD

Hello World

我们先以打印Hello World!作为我们学习WebAssembly的第一个程序吧!让我们先快速编写一个C/C++的打印Hello World!代码,如下所示:

#include <stdio.h>

int main() {
  printf("Hello World!\n");
  return 0;
}

使用相关的GCC等相关编译器能够很正确得到对应的输出:

> emcc main.c -o hello.html

执行完毕后你将得到三个文件代码,分别是:

  • hello.html
  • hello.js:相关的胶水代码,包括加载WASM文件并执行调用等相关逻辑
  • hello.wasm:编译得到的核心WebAssembly执行文件

如果我们想要让NodeJS使用我们代码,那么直接执行:

> emcc main.c

第三方库

在我们的日常的业务开发中相关程序是不可能如此简单的。除了我们自己的操作逻辑外,我们还会依赖于非常多商用或开源的第三方库及框架。比如在数据通信及交换中我们往往会使用到JSON这种轻量的数据格式。在C/C++中有非常多相关的开源库能解决JSON解析的问题,例如cJSON等,那么接下来我们就增加一点点复杂度,结合cJSON库编一个简单的JSON解析的程序。

首先我们下载相关的源码放置在我们项目的vendor文件夹中。接着我们在当前项目的根目录下创建一个CMakeLists.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.txtCMake的“配置文件”,CMake会根据CMakeLists.txt的内容帮助我们生成跨平台的编译命令。然后让我们在代码中引入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("%s\n", cJSON_GetStringValue(data));

    cJSON_Delete(json);
    return 0;
}

由于我们使用了CMake,因此Emscripten的编译命令需要有一点点修改,我们将不使用emcc而是使用emcmakeemmake来创建我们的相关WebAssembly代码,命令如下:

> mkdir build
> cd build
> emcmake cmake ..
> emmake make

我们创建了一个build文件夹用来存放cmake相关的生成文件及信息,接着进入build文件夹并使用emcmakeemmake命令生成对应的WebAssembly代码sample.html、sample.js、sample.wasm,最后我们执行访问sample.html后可以看到其正确的输出了JSONdata内容。

WASM的调试

对于开发的WebAssembly代码而言,我们对于调试可以使用两种方式,一种方式是通过日志的方式进行输出,另一种方式使用单步调试。使用日志的方式输出调试信息非常容易,Emscripten能很好的支持C/C++里面的相关IO库。而对于单步调试而言,目前最新版本的FirefoxChrome浏览器都已经有了一定的支持,例如我们有如下代码:

#include <stdio.h>

int main() {
    printf("Hello World!");
    return 0;
}

然后我们使用emcc进行编译得到相关的文件:

> emcc -g4 main.c -o main.wasm # -g4可生成对应的sourcemap信息

接着打开Chrome及其开发者工具,我们就可以看到对应的main.c文件并进行单步调试了。但值得注意的是,目前emcmake对于soucemap的生成支持并不是很好,并且浏览器的单步调试支持也仅仅支持了代码层面的映射关系,对于比较复杂的应用来说目前的单步调试能力还比较不可用,因此建议开发时还是以日志调试为主要手段。

Links