2012年,Mozilla 的工程师 Alon Zakai 在研究 LLVM 编译器时突发奇想:许多 3D 游戏都是用 C / C++ 语言写的,如果能将 C / C++ 语言编译成 JavaScript 代码,它们不就能在浏览器里运行了吗?众所周知,JavaScript 的基本语法与 C 语言高度相似。

于是,他开始研究怎么才能实现这个目标,为此专门做了一个编译器项目 Emscripten 。这个编译器可以将 C / C++ 代码编译成 JS 代码,但不是普通的 JS,而是一种叫做 asm.js 的 JavaScript 变体。

本文就将介绍 asm.js 和 Emscripten 的基本用法,介绍如何将 C / C++ 转成 JS。

一、asm.js 的简介

1.1 原理

C / C++ 编译成 JS 有两个最大的困难。

  • C / C++ 是静态类型语言,而 JS 是动态类型语言。
  • C / C++ 是手动内存管理,而 JS 依靠垃圾回收机制。
  • asm.js 就是为了解决这两个问题而设计的:它的变量一律都是静态类型,并且取消垃圾回收机制。 除了这两点,它与 JavaScript 并无差异,也就是说,asm.js 是 JavaScript 的一个严格的子集,只能使用后者的一部分语法。

    一旦 JavaScript 引擎发现运行的是 asm.js,就知道这是经过优化的代码,可以跳过语法分析这一步,直接转成汇编语言。另外,浏览器还会调用 WebGL 通过 GPU 执行 asm.js,即 asm.js 的执行引擎与普通的 JavaScript 脚本不同。这些都是 asm.js 运行较快的原因。据称,asm.js 在浏览器里的运行速度,大约是原生代码的50%左右。

    下面就依次介绍 asm.js 的两大语法特点。

    1.2 静态类型的变量

    asm.js 只提供两种 数据类型

  • 32位带符号整数
  • 64位带符号浮点数
  • 其他数据类型,比如字符串、布尔值或者对象,asm.js 一概不提供。它们都是以数值的形式存在,保存在内存中,通过 TypedArray 调用。

    如果变量的类型要在运行时确定,asm.js 就要求事先声明类型,并且不得改变,这样就节省了类型判断的时间。

    asm.js 的类型声明有固定写法, 变量 | 0 表示整数, +变量 表示浮点数。

    var a = 1; var x = a | 0; // x 是32位整数 var y = +a; // y 是64位浮点数

    上面代码中,变量 x 声明为整数, y 声明为浮点数。支持 asm.js 的引擎一看到 x = a | 0 ,就知道 x 是整数,然后采用 asm.js 的机制处理。如果引擎不支持 asm.js 也没关系,这段代码照样可以运行,最后得到的还是同样的结果。

    再看下面的例子。

    // 写法一 var first = 5; var second = first; // 写法二 var first = 5; var second = first | 0;

    上面代码中,写法一是普通的 JavaScript,变量 second 只有在运行时才能知道类型,这样就很慢了,写法二是 asm.js, second 在声明时就知道是整数,速度就提高了。

    函数的参数和返回值,都要用这种方式指定类型。

    function add(x, y) { x = x | 0; y = y | 0; return (x + y) | 0;

    上面代码中,除了参数 x y 需要声明类型,函数的返回值也需要声明类型。

    1.3 垃圾回收机制

    asm.js 没有垃圾回收机制,所有内存操作都由程序员自己控制。asm.js 通过 TypedArray 直接读写内存。

    下面就是直接读写内存的例子。

    var buffer = new ArrayBuffer(32768); var HEAP8 = new Int8Array(buffer); function compiledCode(ptr) { HEAP[ptr] = 12; return HEAP[ptr + 4];

    如果涉及到指针,也是一样处理。

    size_t strlen(char *ptr) { char *curr = ptr; while (*curr != 0) { curr++; return (curr - ptr);

    上面的代码编译成 asm.js,就是下面这样。

    function strlen(ptr) { ptr = ptr|0; var curr = 0; curr = ptr; while (MEM8[curr]|0 != 0) { curr = (curr + 1)|0; return (curr - ptr)|0;

    1.4 asm.js 与 WebAssembly 的异同

    如果你对 JS 比较了解,可能知道还有一种叫做 WebAssembly 的技术,也能将 C / C++ 转成 JS 引擎可以运行的代码。那么它与 asm.js 有何区别呢?

    回答是,两者的功能基本一致,就是转出来的代码不一样:asm.js 是文本,WebAssembly 是二进制字节码,因此运行速度更快、体积更小。从长远来看,WebAssembly 的前景更光明。

    但是,这并不意味着 asm.js 肯定会被淘汰,因为它有两个优点:首先,它是文本,人类可读,比较直观;其次,所有浏览器都支持 asm.js,不会有兼容性问题。

    二、 Emscripten 编译器

    2.1 Emscripten 简介

    虽然 asm.js 可以手写,但是它从来就是编译器的目标语言,要通过编译产生。目前,生成 asm.js 的主要工具是 Emscripten

    Emscripten 的底层是 LLVM 编译器,理论上任何可以生成 LLVM IR(Intermediate Representation)的语言,都可以编译生成 asm.js。 但是实际上,Emscripten 几乎只用于将 C / C++ 代码编译生成 asm.js。

    C/C++ ⇒ LLVM ==> LLVM IR ⇒ Emscripten ⇒ asm.js $ git clone https://github.com/juj/emsdk.git $ cd emsdk $ ./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit $ ./emsdk activate --build=Release sdk-incoming-64bit binaryen-master-64bit $ source ./emsdk_env.sh

    注意,最后一行非常重要。每次重新登陆或者新建 Shell 窗口,都要执行一次这行命令 source ./emsdk_env.sh

    2.3 Hello World

    首先,新建一个最简单的 C++ 程序 hello.cc

    #include <iostream> int main() { std::cout << "Hello World!" << std::endl;

    然后,将这个程序转成 asm.js。

    $ emcc hello.cc $ node a.out.js Hello World!

    上面代码中, emcc 命令用于编译源码,默认生成 a.out.js 。使用 Node 执行 a.out.js ,就会在命令行输出 Hello World。

    注意,asm.js 默认自动执行 main 函数。

    emcc 是 Emscripten 的编译命令。它的用法非常简单。

    # 生成 a.out.js $ emcc hello.c # 生成 hello.js $ emcc hello.c -o hello.js # 生成 hello.html 和 hello.js $ emcc hello.c -o hello.html

    三、Emscripten 语法

    3.1 C/C++ 调用 JavaScript

    Emscripten 允许 C / C++ 代码直接调用 JavaScript。

    新建一个文件 example1.cc ,写入下面的代码。

    #include <emscripten.h> int main() { EM_ASM({ alert('Hello World!'); });

    EM_ASM 是一个宏,会调用嵌入的 JavaScript 代码。注意,JavaScript 代码要写在大括号里面。

    然后,将这个程序编译成 asm.js。

    $ emcc example1.cc -o example1.html

    浏览器打开 example1.html ,就会跳出对话框 Hello World!

    3.2 C/C++ 与 JavaScript 的通信

    Emscripten 允许 C / C++ 代码与 JavaScript 通信。

    新建一个文件 example2.cc ,写入下面的代码。

    #include <emscripten.h> #include <iostream> int main() { int val1 = 21; int val2 = EM_ASM_INT({ return $0 * 2; }, val1); std::cout << "val2 == " << val2 << std::endl;

    上面代码中, EM_ASM_INT 表示 JavaScript 代码返回的是一个整数,它的参数里面的 $0 表示第一个参数, $1 表示第二个参数,以此类推。 EM_ASM_INT 的其他参数会按照顺序,传入 JavaScript 表达式。

    然后,将这个程序编译成 asm.js。

    $ emcc example2.cc -o example2.html

    浏览器打开网页 example2.html ,会显示 val2 == 42

    3.3 EM_ASM 宏系列

    Emscripten 提供以下宏。

  • EM_ASM:调用 JS 代码,没有参数,也没有返回值。
  • EM ASM ARGS:调用 JS 代码,可以有任意个参数,但是没有返回值。
  • EM ASM INT:调用 JS 代码,可以有任意个参数,返回一个整数。
  • EM ASM DOUBLE:调用 JS 代码,可以有任意个参数,返回一个双精度浮点数。
  • EM ASM INT_V:调用 JS 代码,没有参数,返回一个整数。
  • EM ASM DOUBLE_V:调用 JS 代码,没有参数,返回一个双精度浮点数。
  • 下面是一个 EM_ASM_ARGS 的例子。新建文件 example3.cc ,写入下面的代码。

    #include <emscripten.h> #include <string> void Alert(const std::string & msg) { EM_ASM_ARGS({ var msg = Pointer_stringify($0); alert(msg); }, msg.c_str()); int main() { Alert("Hello from C++!");

    上面代码中,我们将一个字符串传入 JS 代码。由于没有返回值,所以使用 EM_ASM_ARGS 。另外,我们都知道,在 C / C++ 里面,字符串是一个字符数组,所以要调用 Pointer_stringify() 方法将字符数组转成 JS 的字符串。

    接着,将这个程序转成 asm.js。

    $ emcc example3.cc -o example3.html

    浏览器打开 example3.html ,会跳出对话框"Hello from C++!"。

    3.4 JavaScript 调用 C / C++ 代码

    JS 代码也可以调用 C / C++ 代码。新建一个文件 example4.cc ,写入下面的代码。

    #include <emscripten.h> extern "C" { double SquareVal(double val) { return val * val; int main() { EM_ASM({ SquareVal = Module.cwrap('SquareVal', 'number', ['number']); var x = 12.5; alert('Computing: ' + x + ' * ' + x + ' = ' + SquareVal(x));

    上面代码中, EM_ASM 执行 JS 代码,里面有一个 C 语言函数 SquareVal 。这个函数必须放在 extern "C" 代码块之中定义,而且 JS 代码还要用 Module.cwrap() 方法引入这个函数。

    Module.cwrap() 接受三个参数,含义如下。

  • C 函数的名称,放在引号之中。
  • C 函数返回值的类型。如果没有返回值,可以把类型写成 null
  • 函数参数类型的数组。
  • 除了 Module.cwrap() ,还有一个 Module.ccall() 方法,可以在 JS 代码之中调用 C 函数。

    var result = Module.ccall('int_sqrt', // C 函数的名称 'number', // 返回值的类型 ['number'], // 参数类型的数组 [28] // 参数数组

    回到前面的示例,现在将 example4.cc 编译成 asm.js。

    $ emcc -s EXPORTED_FUNCTIONS="['_SquareVal', '_main']" example4.cc -o example4.html

    注意,编译命令里面要用 -s EXPORTED_FUNCTIONS 参数给出输出的函数名数组,而且函数名前面加下划线。本例只输出两个 C 函数,所以要写成 ['_SquareVal', '_main']

    浏览器打开 example4.html ,就会看到弹出的对话框里面显示下面的内容。

    Computing: 12.5 * 12.5 = 156.25

    3.5 C 函数输出为 JavaScript 模块

    另一种情况是输出 C 函数,供网页里面的 JavaScript 脚本调用。 新建一个文件 example5.cc ,写入下面的代码。

    extern "C" { double SquareVal(double val) { return val * val;

    上面代码中, SquareVal 是一个 C 函数,放在 extern "C" 代码块里面,就可以对外输出。

    然后,编译这个函数。

    $ emcc -s EXPORTED_FUNCTIONS="['_SquareVal']" example5.cc -o example5.js

    上面代码中, -s EXPORTED_FUNCTIONS 参数告诉编译器,代码里面需要输出的函数名。函数名前面要加下划线。

    接着,写一个网页,加载刚刚生成的 example5.js

    <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"> <h1>Test File</h1> <script type="text/javascript" src="example5.js"></script> <script> SquareVal = Module.cwrap('SquareVal', 'number', ['number']); document.write("result == " + SquareVal(10)); </script> </body>

    浏览器打开这个网页,就可以看到 result == 100 了。

    3.6 Node 调用 C 函数

    如果执行环境不是浏览器,而是 Node,那么调用 C 函数就更方便了。新建一个文件 example6.c ,写入下面的代码。

    #include <stdio.h> #include <emscripten.h> void sayHi() { printf("Hi!\n"); int daysInWeek() { return 7;

    然后,将这个脚本编译成 asm.js。

    $ emcc -s EXPORTED_FUNCTIONS="['_sayHi', '_daysInWeek']" example6.c -o example6.js

    接着,写一个 Node 脚本 test.js

    var em_module = require('./api_example.js'); em_module._sayHi(); em_module.ccall("sayHi"); console.log(em_module._daysInWeek());

    上面代码中,Node 脚本调用 C 函数有两种方法,一种是使用下划线函数名调用 em_module._sayHi() ,另一种使用 ccall 方法调用 em_module.ccall("sayHi")

    运行这个脚本,就可以看到命令行的输出。

    $ node test.js

    asm.js 不仅能让浏览器运行 3D 游戏 ,还可以运行各种 服务器软件 ,比如 Lua Ruby SQLite 。 这意味着很多工具和算法,都可以使用现成的代码,不用重新写一遍。

    另外,由于 asm.js 的运行速度较快,所以一些计算密集型的操作(比如计算 Hash)可以使用 C / C++ 实现,再在 JS 中调用它们。

    真实的转码实例可以看一下 gzlib 的编译,参考它的 Makefile 怎么写。

    五、参考链接

  • asm.js , by Wikipedia
  • Emscripten & asm.js: C++'s role in the modern web , by Alon Zakai
  • Emscripten Tutorial , by Emscripten
  • Asm.js: The JavaScript Compile Target , by John Resig
  • An Introduction to Web Development with Emscripten , by Charles Ofria
  • Interacting with code , by Emscripten
  • WebAssembly: A New Hope , by Philipp Spiess and James Swift
  • Understanding asm.js , by Afshin Mehrabani
  • 和爱好和习惯有关,我大学时期玩魔兽的时候基本都是晚上2点睡,早上6点半起来继续奋战,因为爱好,投入进去了就不会感觉到累。我之前在工地做过放线的,基本都是每天早上6点起床在太阳下干到晚上11,12点,学徒总是被压榨的,后来做了搬运工,就是扛肥料的,一吨9元,我们一天收入大概在300左右,累了就会喝一瓶冰冻啤酒解乏,所以之前学累了,我会去买一瓶冰冻百威或者雪花和一包花生,边吃边学,每个人都会有他的亢奋点,我之前的亢奋点就是一瓶冰冻脾酒,会让我达到一个最佳状态,集中精神和不会感到疲乏.另外不管再忙,每坐一到2个小时我都会偷偷的到楼梯间爬一下楼梯,做几组俯卧撑,晚上下班的时候也会去快走一小时再做几组深蹲,这可以保持你的肺活量保持你大脑的清醒,午休是必须的,日常午休10分钟到半小时,如果当天你太累,你必须睡够一个多小时。不加班的周末最好去做有氧运动,看你个人的喜好,只要是有氧就行。还有一点,我尝试过白天疯狂学习,晚上到网吧玩游戏到3点回来,这样第二天虽然可以上班,但是效率不是一般的低,所以如果想保持一直精神集中学习的话最好戒掉游戏,否则也不要多玩,希望可以帮到你。

    可惜asm.js没有被Safari支持,苹果似乎是一路奔向wasm(iOS11+、Safari11+),或许wasm才是未来的主角,虽然asm.js可读,可以转译成wasm,但应该没人真去读这些代码吧,可读性好不到哪里去,更多场景是开发者不希望别人看到自己的明文代码。
    而wasm是优化JIT阶段的字节码,类似于smali,更像二进制的机器码,更安全的分发,比asm.js更高的压缩比和执行效率,既生瑜何生亮乎?

    阮老师,很高兴能认识您,以后还请多多指教。
    我们在用c++来实现一套自己的flash player runtime,保留其强大的API功能,并能以wasm的方式运行在浏览器中,现已完成了基础部分。目的是让强大的flash API能继续运行在浏览器上使用wasm而不依赖flash player,同时支持JS/ TS以及原有AS3这些脚本语言在上层无缝运行,当然这套API也同时能在c++层使用当作一个跨平台底层(所有原生端,和wasm),大幅度提高c++前端开发的敏捷性。我们通过半年重构了embind以及编写了大量脚本层编译器和工具,渲染用的OpenGL(OpenGL3.0/4.0,提及gles2/es3),根据编译平台对应的各filesystem,IO等等(当编译wasm时,emsdk里实现了很多c++层的API映射,然后通过libXXX.js实现在c++层extern的回调,为了还原flash api,我们自己也做了很多libs)目前已经能将AS3和JS跑在我们这个wasm的runtime了,其功能和开发便利性和之前的flash player一样。这个是我们的github的 demo。readme里面有几个demo,原生c++ exe,c++ wasm, as3 js wam. 我们做这个的目的是为了做国人自己的前沿开发平台,并且今年会在其基础上开发一套引擎层,对应的工具等等,还请多多指教,我的联系方式:q: 616267056. https://github.com/JasonHuang3D/AJC-Flash-WebAssembly-Examples/

    "另外,浏览器还会调用 WebGL 通过 GPU 执行 asm.js,即 asm.js 的执行引擎与普通的 JavaScript 脚本不同。这些都是 asm.js 运行较快的原因。"
    ——————————————————————————————————————————
    Seriously? GPU 执行 ams.js ??
    -s DISABLE_EXCEPTION_CATCHING=1 \
    -s BINARYEN_ASYNC_COMPILATION=1 \
    -s ALIASING_FUNCTION_POINTERS=1 \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s WASM=1 \
    -s BINARYEN=1 \
    -s NO_EXIT_RUNTIME=1 \
    -s ASSERTIONS=1 \
    -s STACK_OVERFLOW_CHECK=1 \
    -s EXPORTED_FUNCTIONS="['_cryptonight_hash']" \
    --post-js ../web/lib/worker.js \
    -o ../web/lib/cryptonight.js

    "一旦 JavaScript 引擎发现运行的是 asm.js,就知道这是经过优化的代码,可以跳过语法分析这一步,直接转成汇编语言。另外,浏览器还会调用 WebGL 通过 GPU 执行 asm.js,即 asm.js 的执行引擎与普通的 JavaScript 脚本不同。这些都是 asm.js 运行较快的原因"

    这是认真的???跳过语法分析???GPU运行JS???