他言語との連携 〜 NLFFIをつかう最終更新: 令和3年8月20日 SML/NJからのC言語関数の呼出や変数操作を提供する外部関数インターフェース(Foreign Function Interface; FFI)としてMatthias Blume氏によるNLFFIがある。これは、C言語のヘッダファイルからSML/NJへスタブを生成するコマンドライン・ツールとC言語変数を操作するライブラリから構成される。残念なことにマニュアルがバイナリ配布パッケージには含まれておらず、また計算幾科学の専門家を対象とした書きぶりのために、普通のプログラマが利用するには必要な情報を得るのが難しい。そこで、手持ちのCやFortranのコードを接続するためにどんな作業をすればよいかという観点から解説を試みたい。 1. 作業の流れつぎの簡単なC言語ルーチンを呼び出すことを考える。 void foo() SML/NJから呼び出す外部手続きは、ダイナミックリンクライブラリ(*.dllまたは*.soファイル)に格納されていなければならない。このプログラムをダイナミックリンクライブラリとしてコンパイルするには、
を実行する。必要に応じて、コマンドプロンプトへの入力を[cmd]で、SML/NJの対話型環境への入力を[sml]で表す。なお、-mrtdは、Windows (mingw)でデフォルトの呼出規約を__stdcallとするためのものである。この例では引数がないので関係ないが、NLFFIは__stdcallにのみ対応し、Cの呼出規約を使うとただちにクラッシュする(かといって[関数名]@[n]形式のシンボルにも対応していないので、ソース上で__stdcall修飾するのではだめである)。つぎに作成したDLLを読み込む短いSMLコードを記述する。これは定型的なもので、以下のとおりに書く。 Cのソースコード(sample.c)とこのファイル(libh.sml)から、接続手続きへのスタブコードを自動生成する。
SML/NJには、Compilation Manager (CM)という複数のプログラム単位を管理する機構がある。use文のチェインでは、結局すべてのコードをコンパイルすることになるのに対し、CMを使うと変更があったソースコードにコンパイルが限定され、コンパイル効率が高くなる。ここでは、CMの詳細に立ち入らず、nlffi-generated.cmにf-foo.smlのコンパイルの方法が書かれていると述べるに留めておく。f-foo.smlでは、structure F_fooが定義されており、その中のfがCの手続きfoo()へのスタブである。実際、SML/NJ上で、たしかにfoo()に対応する型付けがなされるていることと、正しく呼び出せることが確認できる。
さて、C言語手続きを作成して、これを呼び出すまでを見てきた。ただし、今回の例では、引数/返り値ともvoid (unit)型で、SMLとCの間で情報のやりとりはしていない。これを行うには、次節で紹介する、SMLのデータ型とCのデータ型とを相互に変換する方法を知る必要がある。 2. C言語のしきたりでデータを操作する2.1 簡単な例とSML/NJでのC言語オブジェクトの操作C言語のデータオブジェクトを生成・操作するモジュールstructure Cが用意されている。ListやStringなどのBasisライブラリと異なり、デフォルトでは読み込まれないため、使う前に
字面から、操作の内容は明らかであろうが、この3行の操作を詳説することでSML/NJでのC言語データの取り扱いを紹介しよう。SML/NJでは、(1)C言語の型を表現するデータ、(2)(1)で指定された型をもつC言語オブジェクトを生成する関数、(3)C言語オブジェクトから値を読み出す関数、(4)C言語オブジェクトに値を書き込む関数、の4つを通してC言語変数へのアクセスを実現している。(1)のC言語の型を表現するデータは、structure C.Tで、値の読み出し関数はC.Getに、値の書き込み関数はC.Setに定義されている。これらの3つのstructureではC言語の型ごとに共通の名前が使用されている。対応関係を、SML/NJでの値がもつ型とともに表1に示す。
(2)のオブジェクトは、C.new [型]で生成する。この例の1行目では、これを使ってsigned int型の変数を確保し、それをiと置いている。型制約(C.sint, C.rw) C.objの部分は、この変数が読み書き可能であることを表す。C.rwをC.roに変更すると、この変数はconst signed int型になり、2行目でのC.Set.sintによる変更ができなくなる(しようとすると型エラーが起こる)。ここでは無意味に思えるが、接続するC言語のグローバル変数や関数引数の定数性を正しく表現するためにC.roによる指定が用意されている。なお、型制約を省略するとC.newが生成するものの型が(C.sint,?.X1) C.objになってしまい、やはりSetできない。これは不便なので、
というバックドアを使って
2.2 配列の操作例えば、要素数1024のdouble型配列を生成するには、以下のように書く。
ここで、C.T.arr([型],[寸法])が要素数3のdouble型配列という型を表すデータである。寸法の正しい書き方には、興味深い話題を提供してくれるのたが、単に配列を使うには煩雑すぎるので、ここではUnsafe.castを使っている。要素を表すオブジェクトは、関数C.Arr.sub([配列],[添字])で取り出せる。したがって、ここで生成した要素への読み書きは、
毎回このような低レベル操作を行うのは大変なので、つぎのようなC⇔ML間の変換関数を使うと良いだろう。 fun arrFromC c = let Cではポインタを介して配列を操作することが多い。これに対応する操作関数がC.Pointerに定義されている。これらとC言語の式との対応を表2に示す。 (void *)を経由する必要があることに注意 2.3 構造体の操作Cの関数で構造体に設定した値をMLで取り出す例を通じて 構造体の操作の仕方を解説する. 次のようなCコードをMLから呼び出したいとしよう.
このコードでは,構造体Stはm_int とm_double をメンバにもつ.
また,構造体の操作例として受け取った構造体のポインタに
数値を設定する関数 set_number() で中身を設定している.
まず,DLLとFFIをつくる.このコードをstruct.c,DLLをstruct.dllとしよう.
DLLに合わせて,先のlibh.smlの
{name = "./struct.dll",...} と修正したものを同じフォルダ
に準備した上で,
とする.
出来上がった NLFFI-Generate ディレクトリには以下が含まれる:
このコードを動かすと m_int = 5 , m_double_ng = 5.5
になる.
……はずであるが,アラインメントの整合が取れておらず,ver 110.85 時点
ではm_double = 1.01595548785E~316 などの壊れた値になる.つまり
|