Emscripten 入门教程

Web 技术突飞猛进,但是有一个领域一直无法突破 —- 游戏。

游戏的性能要求非常高,一些大型游戏连 PC 跑起来都很吃力,更不要提在浏览器的沙盒模型里跑了!但是,尽管很困难,许多开发者始终没放弃,希望让浏览器运行 3D 游戏。

Emscripten是一个编译器项目。这个编译器可以将 C / C++ 代码编译成 JS 代码,但不是普通的 JS,而是一种叫做 asm.js 的 JavaScript 变体。

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

Emscripten 简介

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

Emscripten

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

C/C++ ⇒ LLVM ==> LLVM IR ⇒ Emscripten ⇒ asm.js

Emscripten 的底层是 LLVM 编译器

Emscripten 的安装

Emscripten 的安装可以根据官方文档。由于依赖较多,安装起来比较麻烦,我发现更方便的方法是安装 SDK。

你可以按照下面的步骤操作。

$ 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。

使用

首先,新建一个最简单的 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

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!。

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。

EM_ASM 宏系列

Emscripten 提供以下宏。

  • EM_ASM:调用 JS 代码,没有参数,也没有返回值。
  • EMASMARGS:调用 JS 代码,可以有任意个参数,但是没有返回值。
  • EMASMINT:调用 JS 代码,可以有任意个参数,返回一个整数。
  • EMASMDOUBLE:调用 JS 代码,可以有任意个参数,返回一个双精度浮点数。
  • EMASMINT_V:调用 JS 代码,没有参数,返回一个整数。
  • EMASMDOUBLE_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++!"。

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 

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>
<html>
<body>
<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>
</html>

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

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
Hi!
Hi!
7

用途

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

Emscripten 入门教程

: » Emscripten 入门教程

原创文章,作者:306829225,如若转载,请注明出处:https://blog.ytso.com/251278.html

(0)
上一篇 2022年5月2日
下一篇 2022年5月2日

相关推荐

发表回复

登录后才能评论