轉:asm.js 和 Emscripten 入門教程

轉:http://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.htmljavascript

asm.js 和 Emscripten 入門教程

做者: 阮一峯html

日期: 2017年9月 7日java

Web 技術日新月異,可是有一個領域一直沒法突破 ---- 遊戲。node

遊戲的性能要求很是高,一些大型遊戲連 PC 跑起來都很吃力,更不要提在瀏覽器的沙盒模型裏跑了!可是,儘管很困難,許多開發者始終沒放棄,但願讓瀏覽器運行 3D 遊戲。ios

2012年,Mozilla 的工程師 Alon Zakai 在研究 LLVM 編譯器時突發奇想:許多 3D 遊戲都是用 C / C++ 語言寫的,若是能將 C / C++ 語言編譯成 JavaScript 代碼,它們不就能在瀏覽器裏運行了嗎?衆所周知,JavaScript 的基本語法與 C 語言高度類似。git

因而,他開始研究怎麼才能實現這個目標,爲此專門作了一個編譯器項目 Emscripten這個編譯器能夠將 C / C++ 代碼編譯成 JS 代碼,但不是普通的 JS,而是一種叫作 asm.js 的 JavaScript 變體。程序員

本文就將介紹 asm.js 和 Emscripten 的基本用法,介紹如何將 C / C++ 轉成 JS。es6

1、asm.js 的簡介

1.1 原理

C / C++ 編譯成 JS 有兩個最大的困難。github

  • C / C++ 是靜態類型語言,而 JS 是動態類型語言。
  • C / C++ 是手動內存管理,而 JS 依靠垃圾回收機制。

asm.js 就是爲了解決這兩個問題而設計的:它的變量一概都是靜態類型,而且取消垃圾回收機制。除了這兩點,它與 JavaScript 並沒有差別,也就是說,asm.js 是 JavaScript 的一個嚴格的子集,只能使用後者的一部分語法。web

一旦 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; } 

上面代碼中,除了參數xy須要聲明類型,函數的返回值也須要聲明類型。

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,不會有兼容性問題。

2、 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 

2.2 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

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 

3、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 代碼,沒有參數,也沒有返回值。
  • 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++!"。

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"> <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> 

瀏覽器打開這個網頁,就能夠看到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 Hi! Hi! 7 

4、用途

asm.js 不只能讓瀏覽器運行 3D 遊戲,還能夠運行各類服務器軟件,好比 LuaRuby 和 SQLite。 這意味着不少工具和算法,均可以使用現成的代碼,不用從新寫一遍。

另外,因爲 asm.js 的運行速度較快,因此一些計算密集型的操做(好比計算 Hash)可使用 C / C++ 實現,再在 JS 中調用它們。

真實的轉碼實例能夠看一下 gzlib 的編譯,參考它的 Makefile 怎麼寫。

5、參考連接

相關文章
相關標籤/搜索