Mosh の FFI を浮動小数引数に対応する
実際の動作を見る
まずは浮動小数引数がどのように扱われるかを手元の環境(i686 GNU/Linux)でチェックする。
hage.c を書いて
double add(double a, double b, double c) { return a + b + c; } void call_add() { add(1.0, 2.0, 3.0); }
gcc -c hage.c -S -o hage.s -O0
アセンブリを見る。
.LC1: .long 0 .long 1074266112 .align 8 .LC2: .long 0 .long 1073741824 .text .globl call_add .type call_add, @function call_add: pushl %ebp movl %esp, %ebp subl $24, %esp fldl .LC1 fstpl 16(%esp) fldl .LC2 fstpl 8(%esp) fld1 fstpl (%esp) call add fstp %st(0) leave ret
fld とは何だろうか。Intel の命令セットリファレンスを読んでみる。「ソース・オペランドをFPU レジスタ・スタックにプッシュする」らしい。ああそういえば Mona で FPU レジスタを保存するコードを書いたときに見たような。
fstpl は「この命令は、ST(0) レジスタの値をデスティネーション・オペランドにコピーする。」。
つまり
という流れか。
fld1 という 1.0 用の定数ロード命令があって面白い。
さて呼び出された add の方はどうだろうか。
add: pushl %ebp movl %esp, %ebp subl $24, %esp movl 8(%ebp), %eax movl %eax, -8(%ebp) movl 12(%ebp), %eax movl %eax, -4(%ebp) movl 16(%ebp), %eax movl %eax, -16(%ebp) movl 20(%ebp), %eax movl %eax, -12(%ebp) movl 24(%ebp), %eax movl %eax, -24(%ebp) movl 28(%ebp), %eax movl %eax, -20(%ebp) fldl -8(%ebp) faddl -16(%ebp) faddl -24(%ebp) leave
1つ目の引数を ST(0) にロードしてスタックにおかれた引数と fadd で足し合わせ結果を ST(0) に格納。
呼び出し規約を調べる
おおよその動きは把握できたので、きちんと呼び出し規約を調べる。
shinichiro_h さんの 呼び出し規約 - 2008-05-23 - 兼雑記 が分かりやすい。
これくらいシンプルだと一瞬で理解できる。浮動小数を返すときはスタックトップか。なるほど。
IA32 での対応
とりあえず float は無視。要望のあった double から。
まずはスタックに値を積む部分のテストから書く。
TEST_F(FFITest, CStackWithFlonum) { CStack cstack; EXPECT_TRUE(cstack.push(Object::makeFlonum(3.0))); EXPECT_TRUE(cstack.push(Object::makeFlonum(2.0))); double* p = (double*)cstack.frame(); EXPECT_EQ(4, cstack.count()); EXPECT_DOUBLE_EQ(3.0, p[0]); EXPECT_DOUBLE_EQ(2.0, p[1]); }
テストを実行すると当然失敗。
[ FAILED ] FFITest.CStackWithFlonum
ここまでくれば後は簡単。以下のようにスタックに積み上げるコードを書く。
コンパイルしてテストを実行。
// Flonum -> double } else if (obj.isFlonum()) { if (MAX_ARGC - count_ < 2) { return false; } union { double fvalue; struct { uint32_t low; uint32_t high; } u32; } v; v.fvalue = obj.toFlonum()->value(); frame_[count_++] = v.u32.low; frame_[count_++] = v.u32.high; }
一発で通ってしまった。
[ RUN ] FFITest.CStackWithFlonum [ OK ] FFITest.CStackWithFlonum
次のテストを用意しよう。テスト用の shared library に以下のコードを追加。
int subf(double a, double b) { return a - b; }
double の戻り値にはまだ対応していないので int に cast 。
次に FFI API の最下層でのテストを Scheme で書く。
(4 (catch (let* ([handle (%ffi-open "./libffitest.so.1.0")] [sub (%ffi-lookup handle 'subf)]) (%ffi-call->int sub 6.0 2.0))))
6.0 - 2.0 の結果が 4 になれば OK 。これも一発で通った。
さて戻り値 double に対応しよう。eax を戻り値としていた callStub を st[0] を戻り値としてやればよい。
gcc inline アセンブリ拡張では st[0] は「"t" 第一(スタックの一番上の)浮動小数点レジスタ」。
double ret; asm volatile( "movl %%esp, %%edx ;" "subl %1 , %%esp ;" "movl %%esp, %%edi ;" // copy arguments to stack "rep ;" "movsb ;" "movl %%edx, %%edi ;" "call *%%eax ;" "movl %%edi, %%esp ;" : "=t" (ret) // t: st[0] : "c" (bytes), "S" (frame), "a" (func) // c:ecx, S:esi a:eax : "edi", "edx", "memory"); return ret; }
この Stub を利用し %ffi-call->double を実装すればよい。
同様にテストを書く
(let* ([handle (%ffi-open "./libffitest.so.1.0")] [sub (%ffi-lookup handle 'subf2)]) (display (%ffi-call->double sub 6.0 2.0)))
(define libffitest (open-shared-library "./libffitest.so.1.0")) (define subf2 (c-function libffitest double subf2 double double)) (test* (subf2 1.0 0.0) 1.0)
できました。どうぞご利用下さい> id:mjt さん
x86-64
手元に環境がないので週明けに会社で対応予定。
感想
1.5時間ほどで実装できた。思ったよりも簡単だったな。