LLVM : WebAssembly支持初探

LLVM : WebAssembly支持初探

在这个问题是否可以将不同语言编译到LLVM IR层面链接?如果可以,与传统的编译为目标代码链接有什么不同?中,我提到了SWIFT与C++ / Rust 在 LLVM IR的融合。而现在其实基于LLVM的语言前端很多,如C / C++ / SWIFT / Julia / Rust...等,而提到这么多语言,一个不容忽略的语言就是JavaScript,所以我也想知道JavaScript是否与LLVM会产生关系。而在这两天查阅资料的时候,首先发现了emscripten。它可以做什么呢?首先看看它的介绍:
  • Compile C and C++ code into JavaScript
  • Compile any other code that can be translated into LLVM bitcode into JavaScript.
  • Compile the C/C++ runtimes of other languages into JavaScript, and then run code in those other languages in an indirect way (this has been done for Python and Lua)!

第二点是不是很像之前那个问题?也就是无论什么语言,然后只要能转为LLVM IR,那就可以被继续转换成JavaScript,而怎么转换的呢?让我们看看emscripten的架构图:

这里使用了Clang把C/C++代码转为LLVM IR,然后则可以利用emscripten在LLVM体系中实现的一个后端目标平台Fastcomp把LLVM IR转为JavaScript。


那么,emscripten选用LLVM来实现它的Fastcomp,我认为是合适的。LLVM的优秀不仅说体现在LLVM IR,自身的代码上,也体现在你可以快速在已有的体系下编写一个新的目标平台。我个人认为甚至是已经有规定好的步骤了,然后你只需要一步一步的按照那个步骤走,然后填写好你要的东西就很容易构建起来了。正是如此,现在很多公司和开源项目若要编写一个新的目标平台的编译器,LLVM几乎都是首选了(除了技术的原因,当然还有BSD License等非技术的因素考虑)。

那么,emscripten是把LLVM IR 转为了 JavaScript,其默认输出是js(当然,现在由于有了WebAssembly, emscripten开始加入了WebAssembly的支持)。不过这篇文章我想更多谈的是 WebAssembly,因为在我搜寻LLVM与JavaScript的时候,我发现了它,而且是标准。随后我玩了一下 WebAssembly的demo,嗯...不错。

然后我发现它的一个编译工具链 binaryen 也正是基于LLVM,那么选用LLVM的更多理由可以参看这篇BastienGohman-WebAssembly-HereBeDragons文档(很好的文档,后面我也会多次提到这份文档),谈到了为什么WebAssembly选择了基于LLVM。

接下来,我则会快速记录一下如何使用Clang / LLVM / Binaryen 来让JavaScript调用C++生成的WebAssembly,目前来说,Clang / LLVM 都是初步支持WebAssembly,所以你在编译LLVM的时候,你需要使用-DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly 来开启WebAssembly的目标平台的编译支持。我这里并没有使用emscripten的编译器emcc来完成这次实验,因为我目前只想测试纯净的Clang / LLVM (svn最新版) 加上 Binaryen 的工具链是怎么样的情况,所以我剥离了很多东西。

那么我要完成的过程是 Clang -> LLVM -> Binaryen -> WebAssembly.

那么我们就开始完成做这样的工作吧。所谓工欲善其事,必先利其器。我们的器就是Clang / LLVM / Binaryen. 对于如何编译Clang / LLVM,我这里就不在多言,可自行参照:Clang - Getting Started,在这里若你拿不定这里的Optional步骤是否需要,那么就把每一步都做下来。 那么在最后编译的时候,我只有一点需要提醒的,就是上文说道的 -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly 你需要加上。所以你的大致命令会是 (如果你按照Clang官网步骤,在LLVM跟路径建立了 build 目录)

cmake ../ -DCMAKE_INSTALL_PREFIX={WHERE_YOU_WANT_TO_INSTALL} -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly

而至于Binaryen,参照其 GitHub 的README即可。


那么,接下来我们开始干正事。首先我有一个很简单的函数,叫做magic.cpp.

// magic.cpp
template <unsigned N> struct Fibonacci
{
  enum
  {
    value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value
  };
};

template <> struct Fibonacci<1>
{
  enum
  {
    value = 1
  };
};

template <> struct Fibonacci<0>
{
  enum
  {
    value = 0
  };
};

extern "C" int magic()
{
  return Fibonacci<15>::value;
}

我使用了C++的模板技术,产生Fibonacci值,正常的情况下是610,对吧?那么我这里使用magic函数作为我提供给外部的接口,也就是后面JavaScript可以调用的函数。而为了防止C++的Name Mangling,我这里使用extern "C",采用C的链接规则。

然后我使用clang编译出LLVM IR文件,这里我把Target设为wasm32. 对于WebAssembly有两种Target,一种是支持32位的wasm32,一种是支持64位的wasm64. 那么为什么要设置target呢?或许对于我们这里的简单C程序是不需要的,但是若你参看了我先前的WebAssembly pdf文档就知道WebAssembly本身是有ABI的,而LLVM IR虽然希望做到真正的完整中间独立,但是在生产LLVM IR的时候已经不可避免的暴露了一些目标平台ABI信息,如参数的传递与函数返回等。所以,若要支持WebAssembly,Clang前端也是需要参与的。若你去查看emscripten基于Clang的编译器前端,其实他所改动的很少,主要就是处理emscripten定义的目标平台的ABI信息。而emscripten编译C++所基于的ABI主要是Itanium C++ ABI,然后略微调整了一些细节,和我今年在Clang上加入AIX的ABI类似。


然后编译命令很简单,如下所示:

clang++ --target=wasm32 magic.cpp -emit-llvm -o magic.bc -c

接下来,我则需要利用LLVM的llc工具生成相关的WebAssembly汇编文件。

llc -march=wasm32 -filetype=asm magic.bc -o magic.s

这里我同样设置march为wasm32,否则默认会设置为当前的编译环境平台。而-filetype=asm表示要生成汇编文件。

我们可以大致看一下这个汇编文件:

	.text
	.file	"magic.bc"
	.hidden	magic
	.globl	magic
	.type	magic,@function
magic:                                  # @magic
	.result 	i32
# BB#0:                                 # %entry
	i32.const	$push0=, 610
                                        # fallthrough-return: $pop0
	.endfunc
.Lfunc_end0:
	.size	magic, .Lfunc_end0-magic


	.ident	"clang version 4.0.0 (trunk 290707)"

这样的push指令,一看就是栈机,对吧?的确如此,WebAssembly的确是设计为基于栈的。还有什么是基于栈的?Java。

接下来,我们则开始利用Binaryen的工具链来帮助我们走完剩下的路。

在Binaryen的README中,有如下阐释:

This repository contains code that builds the following tools in bin/:

  • wasm-shell: A shell that can load and interpret WebAssembly code. It can also run the spec test suite.
  • wasm-as: Assembles WebAssembly in text format (currently S-Expression format) into binary format (going through Binaryen IR).
  • wasm-dis: Un-assembles WebAssembly in binary format into text format (going through Binaryen IR).
  • wasm-opt: Loads WebAssembly and runs Binaryen IR passes on it.
  • asm2wasm: An asm.js-to-WebAssembly compiler, using Emscripten's asm optimizer infrastructure. This is used by Emscripten in Binaryen mode when it uses Emscripten's fastcomp asm.js backend.
  • s2wasm: A compiler from the .s format emitted by the new WebAssembly backend being developed in LLVM. This is used by Emscripten in Binaryen mode when it integrates with the new LLVM backend.
  • wasm.js: wasm.js contains Binaryen components compiled to JavaScript, including the interpreter, asm2wasm, the S-Expression parser, etc., which allow you to use Binaryen with Emscripten and execute code compiled to WASM even if the browser doesn't have native support yet. This can be useful as a (slow) polyfill.
  • binaryen.js: A stand alone library that exposes Binaryen methods for parsing s-expressions and instantiating WASM modules in JavaScrip

那么,我们现在有了LLVM WebAssembly 的汇编文件.s,那么接下来我们就应该利用s2wasm工具。而s2wasm并没有直接产生最后的.wasm文件,而是产生了.wast文件。我们如何来生成.wast文件呢?命令很简单:
s2wasm magic.s >magic.wast

我们打开这个magic.wast文件大致看一下是什么:

(module
  (table 0 anyfunc)
  (memory $0 1)
  (export "memory" (memory $0))
  (export "magic" (func $magic))
  (func $magic (result i32)
    (i32.const 610)
  )
)

若有编译原理的背景,应该很容易看出来这个其实就是增强版的AST(Abstract Syntax Tree)表示,这也是为什么叫.wast的原因,而这一个则是当前Web Assembly的文本形式的表示,被称为Symbolic Expression,简称S-Expression,然而目前还没有决定S-Expression会是最终的WebAssembly文本形式的表示。

然后我们关心一下这个.wast最后的

(func $magic (result i32)
    (i32.const 610)
  )

这一个则是有一个函数名叫$magic,结果是i32类型,函数体内饰一个i32的常量610。

接下来我们把.wast转为.wasm(.wast与.wasm有什么区别呢?可以想想LLVM IR中的.ll 与 .bc的关系,我们把.ll转为.bc叫做llvm-as,而这里呢?看后文),.wasm为WebAssembly的Binary表示,使用wasm-as工具即可。命令是

wasm-as magic.wast >magic.wasm

这样我们就完成了Web Assembly的生成。现在,让我们把这个WebAssembly放到浏览器去执行。首先我们来写一个简单的Html,名字叫做index.html。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Hello WebAssembly</title>
  <script>
     if ('WebAssembly' in window) {
        fetch('magic.wasm')
            .then(response => response.arrayBuffer())
            .then(buffer => WebAssembly.compile(buffer))
            .then(module => {
              const instance = new WebAssembly.Instance(module);
              document.getElementById("btn").addEventListener("click", function() {
                      alert("magic function result is : " + instance.exports.magic());
                  }, false);
            });
      } else {
        output.value = "Your browser doesn't support Web Assembly. You may need " +
        "to enable it in your browser's flags.";
      }
  </script>
  </head>
<body>
  <input type="button" id="btn" value="Click Me to Execute WebAssembly" />
</body>
</html>

很简单的一个页面,其作用就是点击按钮后,会执行WebAssembly里magic()函数的结果。

而在运行这个WebAssembly前,我们需要对浏览器开启WebAssembly的支持,目前默认是关。如果是Chrome,在浏览器输入框输入 chrome://flags/#enable-webassembly ,然后开启支持。如果是Firefox,则是在浏览器输入框输入 about:config,然后找到 javascript.options.wasm,设置为true.

最后,我们在我们index.html所在目录下,开启一个简单的服务器,使用命令:

python -m SimpleHTTPServer 8000

然后,我们在浏览器输入localhost:8000即可。

我最开始使用的是Chrome,版本是 55.0.2883.95 (64-bit),点击按钮后,提示我这个问题:

而这个原因我思索了一会儿后,想到的答案就是我使用的是GitHub最新版的Binaryen,所以WebAssembly格式已经变化了,毕竟这一个WebAssembly其实还没有正式稳定下来。于是,我去下载了Firefox Nightly版本,目前我使用的版本号是:


经过我的测试,这一个是没有任何问题的。

然后我们点击页面的按钮:


这个结果如我们所愿!

那么,通过这样的方式,其实不止 C / C++,即使是 Rust / Julia / ... 都可以。而Rust目前实验性的支持WebAssembly,其所核心点正在于他可以产生LLVM IR,然后emscripten则可以吞LLVM IR,而emscripten最近也开始实验性的支持WebAssembly了,其对接的正是LLVM WebAssembly Backend。


当然,不作死就不会死。接下来,我们尝试使用更复杂的例子,也就是使用C++标准库。

#include <random>

static double getMagicNumber()
{
  std::random_device rd;
  std::mt19937 mt(rd());
  std::uniform_real_distribution<double> dist(1.0, 10.0);
  return dist(mt);
}

extern "C" double magic()
{
  return getMagicNumber();
}

这个例子则是使用标准库的随机数,然后每调用一次magic函数就产生一个随机数。但是,若你直接使用Clang编译的话,会发现由于平台不支持,于是直接报错。然后我查看了emscripten,我发现它的搜索路径是自己从emscripten里面的system/include/libcxx拿过来的,然后通过__EMSCRIPTEN__宏来控制。于是,我就把它的libc / libc++ 等库文件直接拿过来,写了一个Python脚本调用相关命令把标准库的文件一次都编译为了wasm32平台的LLVM BitCode文件,最后通过llvm-link组合这些分离的.bc文件,然后得到了libc.bc / libc++.bc等文件(这一个我目前是1.35版本的emscripten,或许我会后面尝试一下GitHub最新版),这个标准库BitCode文件将会与我后面的magic.bc混合链接。

最终,我得到了一个mix.bc。然后,我使用llc来转为汇编文件,然而llc直接炸了:


更新

所以,按照目前的版本,我想还能做的就是希望WebAssembly为我们提供一个独立的C++标准库(而不是我这样的提取emscripten,定义__EMSCRIPTEN__,然后绕过去,因为这样或许会产生一些问题。除非Web Assembly告诉我们说,你们就只能用emscripten SDK编译,由emscripten帮你把所有的事情都弄好),若如此,而按照之前提到的那个文档,想要带来的是支持C++14的标准库,我想应该是类似emscripten的方式,改动LLVM libc++,毕竟这是BSD License的优势。这样的话,就可以方便我这样的人,用手动的形式去探究各个阶段与各种可能性。而另外一个就是等待LLVM WebAssembly更加的稳定,如之前描述,现在WebAssembly还只是LLVM的实验后端,默认并不会编译这个平台,但是我相信这个过程应该不会太久,若各大公司都决心做这件事,进度一定会很快,所以WebAssembly还是值得期待的。

写完时,已经是凌晨了,今天也是今年最后一天了,祝大家新年快乐!


TODO:

验证 GitHub最新 的 emscripten 的 libc / libc++ 库文件,取出来后是否依旧会出现llc炸了的问题。

Update:

我验证了GitHub最新的 libc / libc++库文件,若按照文章所述的方法,llc依然会炸掉。那么emcc是如何处理的呢?它使用了LLVM的优化器opt工具,进行优化。的确,按照这个方法不会出现llc炸掉了,但是很遗憾,最后产生的WebAssembly是无法在Firefox 53.0a1 Nightly版本中执行的。所以,我认为现在是无法直接使用LLVM Web Assembly后端在实际开发项目中的,能做的只能是等待。

文章被以下专栏收录