nccc: FFI VMの例題
前回、以下の命令からなるインタプリタを設計し、複雑な呼び出しプロトコルを持つ関数や他のスレッドで処理しないといけないようなものはインタプリタ(FFI VM)を通して呼ぶ方向にした。
[RETURN]
[REGADDR rBASE rOUT]
[ADD rIN1 rIN2 rOUT]
[LDC const rDEST] ;; 定数ロード
[LD rBASE offs rDEST] ;; メモリリード
[ST rBASE offs rSRC] ;; メモリライト
[MOV rFROM rTO]
[CALL rFUNC rINBUF rOUTBUF]
[JMP imm]
[JNZ rIN imm]
もうちょっとプログラムを書いてみて必要な機能性を考えたい。
例題: プロファイリング
現在時刻を返却するNCCC関数 current_time
があるとする。
[current_time => time] ;; 0 in 1 out
FFI VMを使用して任意のNCCC呼び出しに掛かった時間を計測する。ここでは、2つの時刻を追加で返却するようにする。
;; r0 = 元のNCCC呼び出し
;; r1 = 元のNCCC入力
;; r2 = 元のNCCC出力
[LDC times r3] ;; r3 = 計時を返却するバッファ(2 words)
[LDC ¤t_time r4] ;; r4 = current_time 関数のNCCC wrapperへのポインタ
[LDC 0 r5] ;; r5 = スクラッチ
[LDC 8 r6] ;; r6 = 8 (sizeof(word))
[LDC 0 r7] ;; r7 = ダミー
[REGADDR r5 r8] ;; r8 = 計時情報の返却用ポインタ(開始時)
[ADD r8 r6 r9] ;; r9 = 計時情報の返却用ポインタ(終了時)
[CALL r4 r7 r8] ;; current_time(開始時)
[CALL r0 r1 r2] ;; 元のNCCC呼び出し
[CALL r4 r7 r9] ;; current_time(終了時)
[RETURN]
FFI VM側で呼び出しを行うことで、Schemeインタプリタ側のGC pause等の影響を受けずに関数の処理に掛かった時間を計測できる。
例題: NCCC呼び出しのカリー化
NCCC関数 test_argsum_mix_4_f64
は引数を4つ受けとり、double値1つを返却する。
[test_argsum_mix_4_f64 s32 s64 f32 f64 => f64] ;; f64 = s32 + s64 + f32 + f64
これを引数2つ取りdouble値1つを返却する関数にする。
[offset10_2_f64 f32 f64 => f64] ;; f64 = 5 + 5 + f32 + f64
これを行うためには LD
(メモリからの読み取り) 命令が必要となる。
;; r0 = 入力 [f32 f64]
;; r1 = 出力 [f64]
[LDC &test_argsum_mix_4_f64 r3] ;; r3 = 元のNCCC関数
;; 引数 r6 = r7-10 [5 5 f32 f64] を構築する
[LDC 5 r7]
[LDC 5 r8]
[LD r0 0 r9] ;; r9 = f32
[LD r0 1 r10] ;; r10 = f64
[REGADDR r7 r6]
[CALL r3 r6 r1]
[RETURN]
例題: 異種イベントキューの接続
YuniframeではlibuvのイベントキューとSDL2のイベントキューの両方を同時に処理する必要がある。macOSの実装上の都合で、Schemeスクリプト側はSDL2のイベントキューの待合せ処理を行う必要がある(GUIイベントはメインスレッドからしか受信できない)ため、libuvのイベントキューはFFI VMを実行する別のスレッドで待合せる。
このlibuvの待合せ処理は2状態を持つ。libuv自体はスレッドセーフでは無いため、libuvの利用権がメインスレッドとイベント待ちスレッドのそれぞれでやりとりされていると言える。
- イベント待ち受け状態 。イベント待ち受け状態では、外部からの通信イベントを受信し、イベントキューにイベントを追加していく。
- 一時停止状態 。メインスレッドでlibuvの処理を行うため、イベントの待ち受けを停止している状態。
使用するncccプリミティブとリソース
この目的のために、libuvをwrapしたライブラリminiioを作成している。(また、libuv自体はブラウザで動かないので、必要な機能だけを抜き出す意図もある)
miniio_ioctx_process
はイベントを待ち受け、イベントキューに積む。イベント待ち受け状態では、これを繰り返し呼ぶことになる。イベントが返却される可能性が無い場合はゼロを返却する。(ただし今回のユースケースではChimeが存在するのでゼロが返却されることはない。)
yfrm_chime_trigger
はメインスレッドを起床させる。エラーが発生した場合は非ゼロを返却する。
yfrm_mtx_lock
yfrm_mtx_unlock
yfrm_cv_wait
はミューテックスと条件変数を操作する。 yfrm_cv_mtx
はエラーが発生した場合に非ゼロを返却する。
[miniio_ioctx_process ctx => result]
[yfrm_chime_trigger ctx chime => result]
[yfrm_mtx_lock mtx =>]
[yfrm_mtx_unlock mtx =>]
[yfrm_cv_wait cv mtx => err]
また、待ち受けスレッドのコンテキストデータとして、8ワードのデータを確保する。これは
- word 0: I/Oコンテキスト (miniio_ioctx_processに渡す)
- word 1: メインスレッドのコンテキスト (yfrm_chime_triggerに渡す)
- word 2: メインスレッドのchime (yfrm_chime_triggerに渡す)
- word 3: ミューテックス
- word 4: 条件変数
- word 5: コンテキスト保持フラグ: 非ゼロの場合は、待ち受けスレッドがI/Oコンテキストを持っている
- word 6: 解放要求フラグ: 正値の場合は、I/Oコンテキストの解放を要求されている。-1の場合は、待ち受けスレッドの終了を要求されている。
- word 7: 待ち受けスレッド結果データ: 非ゼロの場合は、待ち受けスレッドは既に終了している。
ステータスはミューテックスで保護される。
VMコード
;; 初期化
[LDC ctx r8] ;; r8 = 待ち受けスレッドのコンテキスト
[LDC 0 r0] ;; r0 = 定数0
[LDC 1 r1] ;; r1 = 定数1
[REGADDR r20 r10] ;; r10 = 呼び出しバッファのアドレス(in = [r20, r21])
[REGADDR r22 r11] ;; r11 = 呼び出しバッファのアドレス(out = [r22])
loop: ;; メインループ
[LDC &yfrm_mtx_lock r9]
[LD r8 5 r20] ;; mtx
[CALL r9 r10 r11] ;; mtxを確保
[LD r8 6 r7] ;; 解放要求フラグ
[JNZ r7 wait_check] ;; 解放要求フラグが非ゼロなら、I/Oコンテキストをメインスレッドに渡す
[ST r8 5 r1] ;; コンテキスト保持フラグをセット
[LDC &yfrm_mtx_unlock r9]
[LD r8 5 r20] ;; mtx
[CALL r9 r10 r11] ;; mtxを解放
[LDC &miniio_ioctx_process r9]
[LD r8 0 r20] ;; ctx
[CALL r9 r10 r11] ;; I/O待ちを開始
[LD r8 1 r20] ;; ctx
[LD r8 2 r21] ;; chime
[CALL r9 r10 r11] ;; yfrm_chime_trigger でメインスレッドを起床
[JMP loop]
wait_mainthread: ;; I/Oコンテキストの解放を開始 (mtxを持っていること)
[LD r8 6 r7] ;; 解放要求フラグ
[JNZ r7 wait_check] ;; 解放要求フラグが非ゼロならwait_checkへ
[ST r8 5 r1] ;; コンテキスト保持フラグをセットしてI/O待ちに戻る準備
[LDC &yfrm_mtx_unlock r9]
[LD r8 5 r20] ;; mtx
[CALL r9 r10 r11] ;; mtxを解放
[JMP wait_ioctx] ;; I/Oコンテキスト待ちに戻る
wait_check:
[ST r8 5 r0] ;; コンテキスト保持フラグをクリア
[ADD r7 r1 r7]
[JNZ r7 do_wait_mainthread] ;; 解放要求フラグが -1 以外ならdo_wait_mainthreadへ
[JMP quit] ;; 解放要求フラグが -1 なら quitへ
do_wait_mainthread:
[LDC &yfrm_cv_wait r9]
[LD r8 4 r20] ;; cv
[LD r8 5 r21] ;; mtx
[CALL r9 r10 r11] ;; cv_waitする
[JMP wait_mainthread]
;; スレッド終了処理 (mtxを持っていること)
quit:
[ST r8 7 r1] ;; word7 = 1 スレッド終了を宣言
[LDC &yfrm_mtx_unlock r9]
[LD r8 3 r20] ;; mtx
[CALL r9 r10 r11] ;; mtx 解放
[LDC &yfrm_chime_trigger r9]
[LD r8 1 r20] ;; ctx
[LD r8 2 r21] ;; chime
[CALL r9 r10 r11] ;; yfrm_chime_trigger でメインスレッドを起床
[RETURN]