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


うまくいった。
あとは上位の FFI API に対応させて

(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時間ほどで実装できた。思ったよりも簡単だったな。