月別アーカイブ: 2014年5月

Emscripten C++ のアプリをブラウザで動かす (8) iOS でも動く

実際に C/C++ で書かれたゲームを Emscripten で移植してみました。
プラグインなしにブラウザだけ動きます。

↓Windows Firefox
Windows Firefox の画面

ChiRaKS (Emscripten) ここから直接実行できます

● お勧めブラウザと端末

PC      : Chrome / Firefox / IE11 / Safari どれでも OK
Android : Chrome / Firefox どちらか (Krait , Cortex-A9 以降推奨)
iOS     : Safari (iOS7以降: iPhone 5s, iPad Air, iPad mini retina,
                  iOS8以降: iPhone5/5c, iPad4)

未確認ですがおそらく iPad Air, iPad mini retina でも動作します。
(2014/05/30 追記: 動作確認取れました)
(2014/09/19 追記: iOS8 で対応機種が増えました)

● CPU レンダリング

このゲームは CPU だけですべて描画しています。
つまり JavaScript で 3D 演算やライティング、ポリゴンのラスタライズも行っており、
それなりに演算負荷が高いアプリケーションとなっています。

PC 上では Chrome/Firefox/IE11/Safari いずれもフレームレート上限となり
高速に動作しておりレスポンスも十分。

Android 端末でも、最近のハイエンドな機種ではブラウザと思えないほど
快適にプレイできています。

● iOS でも動く

WebGL を使用していないので、これまで動かなかった OS やブラウザでも
走らせられるようになりました。

特に iPhone 5s の Safari はかなり高速に動作しています。
WebGL が使えないものの、Emscripten による iOS アプリの作成も
可能なことがわかりました。

↓iPhone 5s Safari
iOS 7.1 iPhone 5s

なお手持ちの iPad4/iPad3/iPhone5 では動かなかったので注意が必要です。
今のところ iOS で正しく動作するのは 64bit CPU (Apple A7) 搭載端末だけかもしれません。
よって iPad Air, iPad mini retina では動作するのではないかと思われます。
iPad Air / iPad mini retina でも動作することを確認しました。

● 動作テスト

下記は動作テストした環境一覧です。より詳しい表はこちら。

【おすすめ】         Chrome   Firefox   IE11    Safari    独自
----------------------------------------------------------------
Deskttop Windows8     30fps     30fps  30fps       --       --
Deskttop Ubuntu14     30fps     30fps     --       --       --
iPhone 5s iOS7.1       4fps        --     --    30fps       -- 
Kindle HDX 7          30fps     30fps     --       --    30fps (silk)
Tegra Note 7          30fps     30fps     --       --       --
Nexus 10              29fps     29fps     --       --       --
Nexus 7 (2013)        29fps     29fps     --       --       --
HTC J Butterfly     B 29fps   B 29fps     --       --       --
Nexus 7 (2012)      B 29fps   B 29fps     --       --       --
dtab 01             B 29fps   画面乱れ    --       --       --


【非推奨】           Chrome   Firefox   IE11    Safari    独自
----------------------------------------------------------------
HTC EVO 3D          D 10fps   C 20fps     --       --       --
Xperia acro              --   E  8fps     --       --    動作しない
iPad 4              E 1.8fps       --     --   動作しない   --
iPhone 5            E 1.7fps       --     --   動作しない   --
iPad 3              E 1.0fps       --     --   動作しない   --
PS Vita                  --        --     --       --   E 0.1fps

・fps値が大きい方が高速、ただし 30fps が上限
・B = 若干 FPS 値の変動あり。ほぼ 30fps で動作。
・C~D = 変動が大きく安定しない
・E = 低速で動作に支障あり

ページを読み込んだあと、速度が安定するまで多少時間がかかる場合があります。
おそらく JIT Compiler か GC と思われます。

ほぼ 100% CPU 性能に依存するため、
GPU が非力な Tegra 3 (Nexus 7 2012) でも結構良いスコアになっています。

● PlayStation Vita でも一応動く

PS Vita の内蔵ブラウザでも一応動くことがわかりました。
ただし非常に低速で 0.1fps。 ほぼ止まっているに等しい状態です。

↓PS Vita 内蔵ブラウザ
PS Vita

iOS 版 Chrome でも動きますが低速です。
PS Vita ブラウザ や iOS 版 Chrome が極端に遅いのは、
Native コンパイラが組み込まれていないためだと考えられます。

● Native アプリ

ブラウザではあまり速度が出ていない古い Android 端末でも、
こちらの Android Native アプリ版 では十分高速に動作していました。

アプリの方は画面サイズに応じてレンダリング解像度を
160×160 ~ 540×540 まで可変する構造になっているので、
画面が小さい機種ほど正確な比較にならないかもしれません。

速度差は生じているものの、常に 512×512 固定でレンダリングしている
Emscripten 版は、予想以上に頑張っているといえます。

● プログラムについて

ゲーム本体はすべて C/C++ で書かれており、ライブラリを抜いて 100 file ほど。
コンパイル後の js は約 900KB で、Data 込でも合計 1MB 弱です。

スコアの保存は Emscripten の IDBFS を使用しています。
IDBFS の読み書き (FS.syncfs) とレンダリング結果の画面転送だけ
JavaScript で記述しています。

ゲーム中は描画エリア (Canvas) の外部もタッチエリアとして使うことができます。
ゲームの状態を見てタッチイベントの動作を変えています。

ChiRaKS (Emscripten) 解説ページ

● まとめ

WebGL 無しなら iOS でも高速に動作する (iPhone 5s のみ) iOS8 以降は iOS も WebGL に対応しており iPhone 5s 以外でも動きます
・C/C++ で書かれた資産を活用可能
・PC, Android, iOS 全てに対応したアプリ開発へ応用可能

関連エントリ
Emscripten C++/OpenGL ES 2.0 (7) Emscripten の OpenGL API と WebGL
Emscripten C++/OpenGL ES 2.0 (6) Chrome の速度と IE11/Safari
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (5)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (4)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (3)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (2)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (1)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす 一覧

Emscripten C++/OpenGL ES 2.0 (7) Emscripten の OpenGL API と WebGL

Emscripten の OpenGL API には 3 つの動作モードがあります。

(1) defaut (WebGL-friendly subset of OpenGL)
(2) -s FULL_ES2=1 (OpenGL ES 2.0 emulation)
(3) -s LEGACY_GL_EMULATION=1 (OpenGL emulation of older desktop and mobile versions)

(1) は WebGL 命令をほぼそのまま OpenGL ES 2.0 API にコンバートしたものです。

(2) は (1) に加えて OpenGL ES 2.0 の足りない機能を補っています。
WebGL は VertexBuffer を経由しない描画をサポートしていないためです。

(3) は WebGL 上で OpenGL 1.x の固定パイプラインをエミュレートしています。
各種レンダーステートにより、動的にシェーダーの生成も行っているようです。

通常は (1) のままで十分ですが、もし正しく描画されない要素がある場合は
(2) を試すことが可能です。
具体的には glVertexAttribPointer() や glDrawElements() の最後の引数に、
直接データのポインタを渡している場合が相当します。
もともと GPU Hardware 的にも望ましくない機能であること、
また Emscripten でのエミュレーションもオーバーヘッドが生じるため
Client Side Arrays は出来る限り使わないことが推奨されています。

● GPU/Browser 毎の Extension

前回のデータを Android 端末含めてさらに集めてみました。
詳細は下記ページへ

WebGL Extensions (Emscripten)

テクスチャ

                                   TS RTS CTex Depth ANI Float Half
W8  Intel HD G 4000   Firefox 29   8K  8K  DXT   Y    Y   Y-L   Y
W8  Intel HD G 4000   Chrome 35    8K  8K  DXT   Y    Y   Y-L   Y-L
W8  Intel HD G 4000   IE 11       16K 16K  DXT   N    Y   Y-L   N
W8  RADEON HD 6750M   Firefox 29  16K 16K  DXT   Y    Y   Y-L   Y
W8  RADEON HD 6750M   Chrome 35   16K 16K  DXT   Y    Y   Y-L   Y-L
W8  RADEON HD 6750M   IE 11       16K 16K  DXT   N    N   Y-L   N
W7  GeForce GTX650    Firefox 29  16K 16K  DXT   Y    Y   Y-L   Y
W7  GeForce GTX650    Chrome 35   16K 16K  DXT   Y    Y   Y-L   Y-L
W7  GeForce GTX650    IE 11       16K 16K  DXT   N    N   Y-L   N
OSX RADEON HD 6750M   Firefox 29  16K 16K  DXT   Y    Y   Y-L   Y
OSX RADEON HD 6750M   Chrome 35    4K 16K  DXT   Y    Y   Y-L   Y-L
OSX RADEON HD 6750M   Safari 7    16K 16K  DXT   Y    Y   Y     N
OSX Intel HD G 4000   Firefox 29   4K  4K  DXT   Y    Y   Y-L   Y
OSX Intel HD G 4000   Chrome 35    4K 16K  DXT   Y    Y   Y-L   Y-L
OSX Intel HD G 4000   Safari 7    16K 16K  DXT   Y    Y   Y     N
U14 RADEON HD 6750M   Firefox 29  16K 16K  DXT   Y    Y   Y-L   Y
U14 RADEON HD 6750M   Chrome 35   16K 16K  DXT   Y    Y   Y-L   Y-L
U14 RADEON R3         Firefox 29  16K 16K  DXT   Y    Y   Y-L   Y
U14 RADEON R3         Chrome 35   16K 16K  DXT   Y    Y   Y-L   Y-L
U14 Intel HD G        Firefox 29   8K  8K  DXT   Y    Y   Y-L   Y

                                   TS RTS CTex Depth ANI Float Half
A42 S80 Adreno 330    Firefox 29   4K  4K  ATC   Y    Y   Y     Y
A42 S80 Adreno 330    Chrome 35    4K  4K  ---   N    Y   Y     Y-L
A44 S4P Adreno 320    Firefox 29   4K  4K  ATC   Y    Y   Y     Y
A44 S4P Adreno 320    Chrome 35    4K  4K  ---   N    Y   Y     Y-L
A44 T4  ULP GeForce72 Firefox 29   4K  4K  DXT   N    Y   N     Y
A44 T4  ULP GeForce72 Chrome 35    4K  4K  DXT   N    Y   N     Y-L
A44 E5  Mali-T604     Firefox 29   8K  8K  ---   Y    N   Y-L   Y
A41 K3  VivanteGC4000 Firefox 29   8K  8K  ---   Y    Y   Y     Y
A44 T3  ULP GeForce12 Firefox 29   2K  2K  DXT   N    Y   N     Y
A44 T3  ULP GeForce12 Chrome 35    2K  2K  DXT   N    Y   N     Y
A42 OM4 PVR SGX540    Firefox 29   2K  2K  PVR   Y    N   Y     Y
A41 R66 Mali-400MP4   Firefox 29   4K  4K  ---   Y    N   N     N
A40 S3  Adreno 220    Firefox 29   4K  4K  ATC   Y    Y   Y     Y
A23 S2  Adreno 205    Firefox 29   4K  4K  ATC   Y    Y   Y     Y

TS   = GL_MAX_TEXTURE_SIZE
RTS  = GL_MAX_RENDERBUFFER_SIZE
CTex = 対応している圧縮テクスチャフォーマット
Depth= WEBGL_depth_texture
ANI  = EXT_texture_filter_anisotropic
Float= OES_texture_float (Y-L は OES_texture_float_linear)
Half = OES_texture_half_float (Y-L は OES_texture_half_float_linear)

W8=Windows8.1, W7=Windows7, OSX=10.9, U14=Ubuntu14.04, A44= Android 4.4

圧縮テクスチャは、Desktop では 100% DXT1~DXT5 (S3TC) が利用可能となっています。
Android は通常の OpenGL ES 2.0 と同じく GPU によって DXT/ATC/PVR とばらばら。
Android が共通でサポートしているはずの ETC1 が列挙されていませんが、
本当に使えないのかどうかは未確認。

DXT に対応しているはずの Vivante GC4000 が未対応となっているのは、
ブラウザの WebGL Layer が GL_COMPRESSED_TEXTURE_FORMSTS ではなく、
GL_EXTENSIONS だけチェックしているからではないかと思われます。

テクスチャ以外

                                   Uniform  In/Out EInt VAO Ins DB sRGB
W8  Intel HD G 4000   Firefox 29   254/ 221 16/10   Y    N   N   N   N
W8  Intel HD G 4000   Chrome 35    254/ 221 16/10   Y    Y   Y   N   N
W8  Intel HD G 4000   IE 11        512/ 512 16/14   N    N   N   N   N
W8  RADEON HD 6750M   Firefox 29   254/ 221 16/10   Y    N   N   N   N
W8  RADEON HD 6750M   Chrome 35    254/ 221 16/10   Y    Y   Y   N   N
W8  RADEON HD 6750M   IE 11        512/ 512 16/14   N    N   N   N   N
W7  GeForce GTX650    Firefox 29   254/ 221 16/10   Y    N   N   N   N
W7  GeForce GTX650    Chrome 35    254/ 221 16/10   Y    Y   Y   N   N
W7  GeForce GTX650    IE 11        512/ 512 16/14   N    N   N   N   N
OSX RADEON HD 6750M   Firefox 29   768/ 768 16/16   Y    Y   N   Y   Y
OSX RADEON HD 6750M   Chrome 35    768/ 768 16/32   Y    Y   Y   Y   N
OSX RADEON HD 6750M   Safari 7     768/ 768 16/32   Y    Y   N   N   N
OSX Intel HD G 4000   Firefox 29  1024/1024 16/16   Y    Y   Y   Y   Y
OSX Intel HD G 4000   Chrome 35   1024/1024 16/15   Y    Y   Y   Y   N
OSX Intel HD G 4000   Safari 7    1024/1024 16/15   Y    Y   N   N   N
U14 RADEON HD 6750M   Firefox 29  4096/4096 29/32   Y    Y   Y   Y   Y
U14 RADEON HD 6750M   Chrome 35   4096/4096 29/32   Y    Y   Y   Y   N
U14 RADEON R3         Firefox 29   256/ 256 29/32   Y    Y   Y   Y   Y
U14 RADEON R3         Chrome 35    256/ 256 29/32   Y    Y   Y   Y   N
U14 Intel HD Graphics Firefox 29  4096/4096 16/32   Y    Y   Y   Y   Y

                                   Uniform  In/Out EInt VAO Ins DB sRGB
A42 S80 Adreno 330    Firefox 29   256/ 224 16/16   Y    Y   N   N   N
A42 S80 Adreno 330    Chrome 35    256/ 224 16/16   Y    Y   N   N   N
A44 S4P Adreno 320    Firefox 29   256/ 224 16/16   Y    Y   Y   Y   N
A44 S4P Adreno 320    Chrome 35    256/ 224 16/16   Y    Y   N   N   N
A44 T4  ULP GeForce72 Firefox 29   280/1024 16/15   N    Y   N   N   N
A44 T4  ULP GeForce72 Chrome 35    280/1024 16/15   N    Y   N   N   N
A44 E5  Mali-T604     Firefox 29  1024/1024 16/15   Y    Y   Y   Y   N
A41 K3  VivanteGC4000 Firefox 29   568/ 568 16/12   Y    N   N   N   N
A44 T3  ULP GeForce12 Firefox 29   256/1024 16/15   N    N   N   N   N
A44 T3  ULP GeForce12 Chrome 35    256/1024 16/15   N    Y   N   N   N
A42 OM4 PVR SGX540    Firefox 29   128/  64  8/ 8   Y    Y   N   N   N
A41 R66 Mali-400MP4   Firefox 29   256/ 256 16/12   N    N   N   N   N
A40 S3  Adreno 220    Firefox 29   251/ 221 16/ 8   Y    Y   N   N   N
A23 S2  Adreno 205    Firefox 29   251/ 222 16  8   Y    N   N   N   N

Uniform = Vsh/Fsh
In/Out  = Attribute/Varying
EInt    = OES_element_index_uint (32bit index)
VAO     = OES_vertex_array_object
Ins     = ANGLE_instanced_array
DB      = WEBGL_draw_buffers
sRGB    = EXT_sRGB

基本的には GPU に依存しますが、さらにブラウザごとにフィルタがかかっている印象です。
Desktop Native では対応してるはずの機能もブラウザによっては無効となっています。
Mobile GPU はもともとこんな感じだったので、それほど驚きはないでしょう。

対応度のばらつきが大きく、最適化以外ではあまり積極的に利用しない方が良いことがわかります。
OpenGL ES 2.0 の要求レベルが低いためで、WebGL 2.0 が OpenGL ES 3.x
ベースになるならもう少し統一されるのではないでしょうか。

動作は遅いものの Firefox + WebGL は Android 2.3 時代の端末でも動作しました。

関連エントリ
Emscripten C++/OpenGL ES 2.0 (6) Chrome の速度と IE11/Safari
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (5)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (4)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (3)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (2)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (1)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす 一覧

Emscripten C++/OpenGL ES 2.0 (6) Chrome の速度と IE11/Safari

以前のテストで Chrome の場合だけ極端に遅かった原因が判明しました。
Chrome の WebGL getError() 呼び出しが非常に重かったためです。
getError() 無しのビルドでは 60fps を超える速度となっています。
JavaScript が遅いわけではありませんでした。

Windows 8.1 Chrome 35 (60fps)
emscripten_windows_chrome.png

InternetExplorer でも動くようになったので比較してみます。

Core i7-3615QM / Intel HD Graphics 4000
----------------------------------------------------------
Windows 8.1  Firefox 29         60fps 以上
Windows 8.1  Chrome 35          60fps 以上 (glGetError無し)
Windows 8.1  IE 11           34-60fps

いずれも fps 値が大きい方が高速です。
fps に幅があるものは、前回のテストに従い描画負荷を変更しているため。
「34-60fps」なら一番重い 1. で 34fps 前後、一番軽い 3. で 60fps 以上
出ていることを意味しています。

以下別の PC のテスト

Core i7-2720QM / RADEON HD 6750M
----------------------------------------------------------
Mac OSX 10.9  Firefox 29        60fps 以上
Mac OSX 10.9  Chrome 35         60fps 以上 (glGetError無し)
Mac OSX 10.9  Safari 7          60fps 以上

Windows 8.1   Firefox 29        60fps 以上
Windows 8.1   Chrome 35         60fps 以上 (glGetError無し)
Windows 8.1   IE 11             60fps 以上

AMD Athlon 5350 / GeForce GTX 650
----------------------------------------------------------
Windows 7     Firefox 29        60fps 以上
Windows 7     Chrome 35         60fps 以上 (glGetError無し)
Windows 7     IE 11          50-60fps

Tegra 4 Cortex-A15 / ULP GeForce 72 (Tegra Note 7)
----------------------------------------------------------
Android 4.4   Firefox 29        60fps 以上
Android 4.4   Chrome 35         60fps 以上 (glGetError無し)

↓ Chrome 向け 修正の例: Emscripten から出力された *.js の _glGetError() 内にある
return GLctx.getError() を return 0 に変更。

function _glGetError() {
      // First return any GL error generated by the emscripten library_gl.js interop layer.
      if (GL.lastError) {
        var error = GL.lastError;
        GL.lastError = 0/*GL_NO_ERROR*/;
        return error;
      } else { // If there were none, return the GL error from the browser GL context.
        //return GLctx.getError();  // ← 削除
	return	0;
      }
    }

Chrome 以外では glGetError() (WebGL の getError()) があってもなくても
速度には影響がないようです。
IE/Safari 共にかなり高速に動作しています。

これまで IE で動かなかった原因は、オプティマイザが
小さい描画の Index を BYTE Size に変換してしまっていたためでした。

また IE は WebGL Extension の対応度があまり高くないようです。
DXT, float texture はすべてのブラウザが対応していますが、
depth_texture, element_index_uint 未対応は IE だけとなっています。

上の Chrome のキャプチャ画面では depth_texture が用いられています。
↓ IE は depth_texture がないので、Tegra2/3 と同じく ShadowMap に
Color Buffer を使用しています。

Windows 8.1 Internet Explorer 11
emscripten_windows_ie

// IE11
GL_VERSION: WebGL 0.93
GL_RENDERER: Internet Explorer
GL_VENDOR: Microsoft
GL_SHADING_LANGUAGE_VERSION: OpenGL ES GLSL 1.00 (WebGL)

Extension:
GL_WEBGL_compressed_texture_s3tc
GL_OES_texture_float
GL_OES_texture_float_linear
GL_EXT_texture_filter_anisotropic
GL_OES_standard_derivatives

// Firefox 29
GL_VERSION: WebGL 1.0
GL_RENDERER: Mozilla
GL_VENDOR: Mozilla
GL_SHADING_LANGUAGE_VERSION: OpenGL ES GLSL 1.00 (WebGL)

Extension:
GL_EXT_texture_filter_anisotropic
GL_OES_element_index_uint
GL_OES_standard_derivatives
GL_OES_texture_float
GL_OES_texture_float_linear
GL_OES_texture_half_float
GL_WEBGL_compressed_texture_s3tc
GL_WEBGL_depth_texture
GL_WEBGL_lose_context
GL_MOZ_WEBGL_lose_context
GL_MOZ_WEBGL_compressed_texture_s3tc
GL_MOZ_WEBGL_depth_texture

// Chrome
GL_VERSION: WebGL 1.0 (OpenGL ES 2.0 Chromium)
GL_RENDERER: WebKit WebGL
GL_VENDOR: WebKit
GL_SHADING_LANGUAGE_VERSION: OpenGL ES GLSL 1.00 (WebGL)

Extension:
GL_ANGLE_instanced_arrays
GL_EXT_texture_filter_anisotropic
GL_WEBKIT_EXT_texture_filter_anisotropic
GL_OES_element_index_uint
GL_OES_standard_derivatives
GL_OES_texture_float
GL_OES_texture_float_linear
GL_OES_texture_half_float
GL_OES_texture_half_float_linear
GL_OES_vertex_array_object
GL_WEBGL_compressed_texture_s3tc
GL_WEBKIT_WEBGL_compressed_texture_s3tc
GL_WEBGL_depth_texture
GL_WEBKIT_WEBGL_depth_texture
GL_WEBGL_lose_context
GL_WEBKIT_WEBGL_lose_context
GL_WEBGL_debug_renderer_info

// Safari 7
GL_VERSION: WebGL 1.0 (2.1 ATI-1.22.25)
GL_RENDERER: WebKit WebGL
GL_VENDOR: WebKit
GL_SHADING_LANGUAGE_VERSION: OpenGL ES GLSL 1.00 (WebGL)

Extension:
GL_OES_texture_float
GL_OES_standard_derivatives
GL_WEBKIT_EXT_texture_filter_anisotropic
GL_OES_vertex_array_object
GL_OES_element_index_uint
GL_WEBGL_lose_context
GL_WEBKIT_WEBGL_compressed_texture_s3tc
GL_WEBKIT_WEBGL_depth_texture

HOST GPU によって多少違いがあります。
OS 毎、GPU 毎のより詳しいデータは下記に載せました。

WebGL Extensions

次回: Emscripten C++/OpenGL ES 2.0 (7) Emscripten の OpenGL API と WebGL

関連エントリ
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (5)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (4)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (3)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (2)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (1)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす 一覧

Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (5)

● JSライブラリ

JavaScript で書かれたライブラリと直接リンクすることができます。

// lib.js
mergeInto( LibraryManager.library, {
    print: function(x){
        console.log( x );
    }
});
// main.cpp
extern "C" {
    void print( int x );
}

int main()
{
    print( 12345 );	// これ
    return  0;
}

ビルド手順

emcc main.cpp --js-library lib.js -o main.html

C言語のリンケージに含まれるので、extern 宣言するだけで呼び出せます。

Emscription の library*.js を見ると、標準で用意された関数も
JavaScript で書かれていることがわかります。

利点としては Native な JavaScript の方が自由度が高いことが挙げられます。
ただ C/C++ で書いた場合は asm.js 対応コードに変換してくれるため、
どちらが効率がよく動作できるのかは処理内容に依存すると思われます。

● 引数

// lib.js
mergeInto( LibraryManager.library, {
    print_char : function(x){
        console.log( x );
    },
    print_short : function(x){
        console.log( x );
    },
    print_int : function(x){
        console.log( x );
    },
    print_float : function(x){
        console.log( x );
    },
    print_double : function(x){
        console.log( x );
    },
    print_llong : function(x,y){
        console.log( y * 4294967296 + x );
    },
    print_string : function(x){
        console.log( Pointer_stringify(x) );
    }
});
// main.cpp
extern "C" {
    void print_char( char x );
    void print_short( short x );
    void print_int( int x );
    void print_float( float x );
    void print_double( double x );
    void print_llong( long long x );
    void print_string( const char* x );
};

int main()
{
    print_char( 123 );
    print_short( 12345 );
    print_int( 123456789 );
    print_float( 1.2345f );
    print_double( 1.2345678901234 );
    print_llong( 12345678901234ll );
    print_string( "ABCDEFG" );
    return  0;
}
// output
123
12345
123456789
1.2345000505447388
1.2345678901234
12345678901234
ABCDEFG

ポインタを受け取る場合はメモリアクセスが必要です。
型に応じて HEAP8,HEAP16,HEAP32,HEAP64 を用いるか
setValue()/getValue() 命令を使うことができます。
文字列の Object 変換は Pointer_stringify() で可能。

double に収まらない long long (64bit int) 型は、
2個の整数 (low, high) に分解されていることもわかります。

● 戻り値

// lib.js
mergeInto( LibraryManager.library, {
    ret_llong : function(x,y){
	asm.setTempRet0( 1 );	// high
    	return	0>>>0;	// low
    },
});
// main.cpp
#include 

extern "C" {
    long long ret_llong( long long x );
}

int main()
{
    printf( "%lld\n", ret_llong( 0 ) );
    return  0;
}
// output
4294967296

64bit 型は global な戻り値レジスタ tempRet0~ を併用しています。
他の関数呼び出しで破壊される可能性があるので atomic ではない点に注意。

文字列定数など static なポインタを返すのは簡単ではないようです。
メインメモリ空間に配置されていなければならないためで、
static な data 領域はコンパイル時に作られます。
ライブラリ初期化時に HEAP から確保して代用する形になるでしょう。

C言語に返すことが可能な heap memory の確保は、
Module._malloc() / Module._free() を使用します。
C言語の runtime に含まれるので、コンパイル時にこれらの関数への参照がなければ
正しいコードが生成されない点に注意が必要です。

// lib.js
mergeInto( LibraryManager.library, {
    alloc_int : function(size){
    	return	Module._malloc( size * 4 );
    },
    dump_int : function(addr, size){
        for( i= 0 ; i< size ; i++ ){
	    console.log( Module.HEAP32[(addr>>2) + i] ); // memory アクセス
        }
    },
});
// main.cpp
#include 

extern "C" {
    int*  alloc_int( int size );
    void  dump_int( const int* buffer, int size );
}

int main()
{
    int*  buffer= alloc_int( 100 );
    for( int i= 0 ; i< 100 ; i++ ){
        buffer[i]= i * i;
    }
    dump_int( buffer, 100 );
    free( buffer );   // -- (1)
    return  0;
}

↑(1) の free が無ければ C言語で malloc()/free() が生成されないため、
JavaScript 側の Module._malloc() は正しい挙動になりません。

例えば下記のようにアロケータを完全に置き換えようとしても、
C言語から参照がないので _malloc/_free がダミーコードになります。
一見正しく動作しますがメモリリークします。

// lib.js
mergeInto( LibraryManager.library, {
    my_alloc : function(byte_size){
    	return	Module._malloc( byte_size );
    },
    my_free : function(addr){
    	Module._free( addr );
    },
});
// main.cpp
extern "C" {
    void* my_alloc( int byte_size );
    void  my_free( void* addr );
}

int main()
{
    int*  buffer= (int*)my_alloc( 100 * sizeof(int) );
    for( int i= 0 ; i< 100 ; i++ ){
        buffer[i]= i * i;
    }
    my_free( buffer );
    return  0;
}

● メモリマップ

0~7               NULL trap 領域
STATIC_BASE~      data/bss segument (8~)
STACK_BASE~       stack 領域
DYNAMIC_BASE~     heap 領域
~TOTAL_MEMORY-1

NULL アクセスはコンパイラが検出できる場合だけ例外が発生します。
動的な NULL アクセスはデフォルトではエラーになりませんが、
-s SAFE_HEAP=1 を付けることでメモリアクセス時のアドレスチェックが可能です。
その分動作速度は低下します。

stack は前方から後方に向かって確保されています。

文字列定数や static/global に確保したメモリは STATIC エリアに格納します。
初期値を持たない配列も含まれるため、下記のようなケースでは初期化データに
大量の 0 が並ぶことになります。
ファイルサイズが増加するので、HEAP から確保した方が無駄が少なく済むようです。

// main.cpp
int buffer[1024];

/* memory initializer */ allocate([0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
~

次回: Emscripten C++/OpenGL ES 2.0 (6) Chrome の速度と IE11/Safari

関連エントリ
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (4)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (3)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (2)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (1)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす 一覧

Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (4)

Emscripten のメモリ空間は固定の配列上に確保されており、
デフォルトの USE_TYPED_ARRAYS=2 では TypedArray によって
任意の型でアクセスできるようになっています。
USE_TYPED_ARRAYS の指定により他のメモリモードを用いることもできます。

関数は JavaScript の function がそのまま用いられており、
呼び出しや引数の受け渡しは普通の JavaScript のコードになっていました。
よって明示的な CallStack は存在せず、引数は Stack を消費しません。
Stack は基本的には auto 変数のために用いられています。

可変引数は特別で、stack 上に格納したのち va_list 渡しになっています。
関数の途中では動的に stack pointer は動かないので、可変引数領域も
prologue 時に確保されているようです。

var i=env.STACKTOP|0;
var c=new global.Int32Array(buffer);

function Ra(){
    var a=0,b=0;
    a=i;	// a が frame pointer
    i=i+16|0;	// stack 確保
    b=a;	// b= local 変数 (address)
    ~
    c[b>>2]=12345;	// intアクセス, bへの代入
    ~
    i=a;	// stack pointer の復帰
    return 0
}

関数呼び出しが通常の function なので、関数ポインタは HEAP や STACK と別空間にあります。
callback などの関数では「関数ポインタ用に設けられた関数のテーブル」
に対する index 値が渡されています。
引数など型に応じてテーブルは複数存在するので、関数ポインタを別の関数型に
cast するようなコードは正しく動かないと公式サイトにも書かれています。

var _=env.abort;
var ua=env._emscripten_set_main_loop_arg;

function mb(a){ // NULL pointer 用
    a=a|0;
    _(0)	// env.abort 呼び出し
}

function fb(){	// main()
    ~

    // emscripten_set_main_loop_arg() の呼び出し
    // 最初の引数に、関数ポインタとして 1 ( Qa[1] ) が渡っている
    ua(1,0,0,1);

    i=a;
    return 0
}

// EMSCRIPTEN_END_FUNCS
var Qa=[mb,gb];  // 関数ポインタ用配列 (void (*)(int) 用)

コード中たまに見かける dynCall() が関数ポインタを使った呼び出しを行っています。

あとから気がついたのですが、emcc に –js-opts 0 を指定すると
もっと読みやすい出力が得られるようです。

function _main() {
 var $0 = 0, $ABCDEFG = 0, $vararg_buffer7 = 0, label = 0, sp = 0;
 sp = STACKTOP;
 STACKTOP = STACKTOP + 16|0;
 $vararg_buffer7 = sp;
 $ABCDEFG = sp + 4|0;
 HEAP32[$vararg_buffer7>>2] = $ABCDEFG;
 (_printf((72|0),($vararg_buffer7|0))|0);
 $0 = (_func01(123)|0);
 HEAP32[$vararg_buffer7>>2] = $0;
 (_printf((8|0),($vararg_buffer7|0))|0);
 HEAP32[$vararg_buffer7>>2] = $ABCDEFG;
 (_printf((16|0),($vararg_buffer7|0))|0);
 _emscripten_set_main_loop_arg((1|0),($ABCDEFG|0),0,1);
 (_puts((96|0))|0);
 HEAP32[$vararg_buffer7>>2] = $ABCDEFG;
 (_printf((24|0),($vararg_buffer7|0))|0);
 STACKTOP = sp;
 return 0;
}

可変引数領域が stack 上に確保されていることがわかります。

JavaScript のコードとして動くため、block するような I/O 命令は
本来うまく動かないことになります。
callback が発生しないためで、一旦 event driven のための
メインループに return しなければ完了通知を受け取ることができません。

Emscripten は memory 上に仮想的な File System を作っているため、
ファイルアクセスでは何も問題が起こらず C言語コードのまま動きます。
互換性が高く、アプリケーションを比較的簡単に移植できる要因として
この仮想 FileSystem の果たす役割は非常に大きいのではないかと思います。

emscripten_set_main_loop() は一見その場で block しているように
見えますが、見た目とは全く異なる挙動をしていました。

static void ems_loop( void* arg )
{
   ...
}

int main()
{
    Initialize();
    emscripten_set_main_loop( ems_loop, 0, true );
    Finalize();
    return  0;
}

emscripten_set_main_loop() の最後の引数が ture の場合無限 Loop をシミュレートします。
実行はその場で停止し、以後毎フレーム ems_loop() が呼ばれます。

これだと event loop に return していないように見えるのですが、
JavaScript のコード上では emscripten_set_main_loop() の最後で例外を throw していました。
つまり emscripten_set_main_loop() が呼ばれた時点で main() 関数
そのものが終了してしまいます。
emscripten_set_main_loop() 以後の Finalize() 等のコードが呼ばれることはありません。

では下記のようなコードはどうでしょうか。

class AppModule {
public:
};

static void ems_loop( void* arg )
{
    AppModule* app= reinterpret_cast( arg );
    ~
}

int main()
{
    AppModule  app;
    emscripten_set_main_loop_arg( ems_loop, &app, 0, true );
    return  0;
}

例外はあくまで JavaScript の throw なので、main() が終了しても
AppModule の destructor が呼ばれるわけではありません。

また main() が終了するなら ems_loop() で stack 上の app に対する
不正アクセスが行われてしまうようにも見えますが、こちらも大丈夫でした。
main() の epilogue を通らないので、中で確保した stack 領域は
そのまま保持されたままとなっているようです。

以上より、関数呼び出しは仮想化されていないので、
現在の実行コンテキストを保存したまま一時的に Event Loop に戻るような処理は
やはり簡単には実現できないのではないかと思われます。
Thread で block することを想定した同期 I/O まわりなど。

例えば socket を使った通信は

connect( sock, &addr, sizeof(addr) );
recv( sock, buffer, size, 0 );
~
close( sock );

connect() が必ず EINPROGRESS を返すので、継続するには Event Loop に
戻らなければならないようです。
↓こんな感じで使えないかと思ったのですが簡単にはいきませんでした。

connect( sock, &addr, sizeof(addr) );
emscripten_push_main_loop_blocker( recv_loop, sock );
~
close( sock );

もし CallStack など関数呼び出しも仮想化されているならば、
自前で Context 切換するなど何らかの手段が取れるのではないかと思ったからです。

関連エントリ
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (3)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (2)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす (1)
Emscripten C++/OpenGL ES 2.0 のアプリケーションをブラウザで動かす 一覧