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 のアプリケーションをブラウザで動かす 一覧