この記事は F# Advent Calendar 2014 の 14日目の記事です。今回は英語版もあるそうで、以下から辿っていくと英語版の F# Advent にたどり着けます(当然、中身が英語ですね)。
F# Advent Calendar 2014 – connpass
http://connpass.com/event/9758/
F# Advent Calendar in English 2014 | Sergey Tihon’s Blog
http://sergeytihon.wordpress.com/2014/11/24/f-advent-calendar-in-english-2014/
さて、去年は F#とFortranの話…のその前に というのを書いて、あえなく挫折したワタクシですが、今回は F# から Fortran を呼び出すことに成功致しましたッ!!! って、誰得な技術情報なのですが、いちおう、頭が F つながりというネタ記事なので、お許しを(笑)。
Fortarn で DLL を作成する
DLL は Intel Visual Fortran 2013 を使っています。
Intel の Fortran では、DLL を作成できて、以下のように「!DEC$ ATTRIBUTES DLLEXPORT::<公開名>」のように、DLL で公開する関数を定義できます。gcc にも fortran があるのですが、そっちのほうはスタティックライブラリしか作ったことがなくて、よくわかりません。ただし、linux 上で動かすのならば *.so ファイルを作ってそのまま C 言語から使えるようにできるので、おそらく F# からも似たような形で作れるでしょう。
まあ、Windows 上ならば DLL なので、このスタイルで。
! 数値を渡す簡単なパターン integer function add1( a, b ) !DEC$ ATTRIBUTES DLLEXPORT::ADD1 integer, intent(in) :: a, b integer :: total total = a + b add1 = total end function add1 ! 1次元配列を渡す integer function sum1( v, count ) !DEC$ ATTRIBUTES DLLEXPORT::SUM1 integer, intent(in) :: v(10) integer, intent(in) :: count integer :: i, total total = 0 do i=1,count print *, 'in sum1: ', i, v(i) total = total + v(i) end do sum1 = total end function sum1 ! 二次元配列を渡す integer function sum2( v, cnti, cntj ) !DEC$ ATTRIBUTES DLLEXPORT::SUM2 integer :: v(3,4) integer :: cnti, cntj integer :: i,j integer :: total total = 0 do j=1,cntj do i=1,cnti print *,i,j,v(i,j) total = total + v(i,j) end do end do sum2 = total end function sum2
Fortran の中身がチープすぎますが、数値(int型)と配列を試してます。あと実用的にするには、double, float あたりを試しておく必要がありますね。
まずは、試しに C++ から呼び出す
動作確認をするために C++ から呼び出してみましょう。Fortran から公開されれる DLL 関数は、dllimport を使って呼び出せるのですが、値を渡すところがポインタになっているところが注意したいところです。これ Fortran の関数渡しがスタック経由ではなくてグローバル変数経由だからなんですよね。C 言語の場合スタックに値を積んで渡します(C#などの.NET言語から渡すときも同じ)が、Fortran に渡すときは、これに注意しないといけないという、変な感じなのです。まあ、Fortran だけでやっているときは、あんまり考慮しなくてよくて便利(でかい配列をそのまま渡すとか)なので、良し悪しということで。
#include "stdafx.h" #include <iostream> #define DllImport __declspec( dllimport ) extern "C" { DllImport int ADD1(const int *, const int *); DllImport int SUM1( int(*)[] , const int *); DllImport int SUM2(int(*)[4][3], const int *, const int *); } int _tmain(int argc, _TCHAR* argv []) { int x = 10; int y = 20; int ans = ADD1(&x, &y); std::cout << "ans " << ans << std::endl; int v[] = { 1, 2, 3, 4, 5 }; int cnt = 5; int ans2 = SUM1((int(*)[])&v, &cnt); std::cout << "ans2 " << ans2 << std::endl; int v2[][3] = { {1,2,3}, {11,22,33}, {10,20,30}, {100,200,300} }; int cnti = 3; int cntj = 4; int ans3 = SUM2( &v2, &cnti, &cntj); std::cout << "ans3 " << ans3 << std::endl; return 0; }
実行すると、こんな感じになります。
C# から呼び出してみる
これを C# で書き直してみましょう。DllImport を使うのは分かったのですが、int のポインタを渡さないといけないので、ref を使います。関数の呼び出し形式は、明示的に CallingConvention = CallingConvention.Cdecl を定義する必要があります。
class Program { [DllImport("FModule.dll", CallingConvention = CallingConvention.Cdecl)] extern static int SUM1([In] int[] v, [In] ref int count); [DllImport("FModule.dll", CallingConvention = CallingConvention.Cdecl)] extern static int SUM2([In] int[,] v, [In] ref int cnti, [In] ref int cntj); [DllImport("FModule.dll", CallingConvention = CallingConvention.Cdecl)] extern static int ADD1([In] ref int a, [In] ref int b); static void Main(string[] args) { int a = 10; int b = 20; int ans = ADD1(ref a, ref b); Console.WriteLine("ans {0}", ans); int[] v = { 1, 2, 3, 4, 5 }; int cnt = v.Length; int ans2 = SUM1(v, ref cnt); Console.WriteLine("ans2 {0}", ans2); /* int[][] v2 = { new int[]{1,2,3,4}, new int[]{11,22,33,44}, new int[]{10,20,30,40}, }; */ int[,] v2 = { {1,2,3}, {11,22,33}, {10,20,30}, {100,200,300}, }; int cnti = 3; int cntj = 4; int ans3 = SUM2(v2, ref cnti, ref cntj); Console.WriteLine("ans3 {0}", ans3); }
2次元配列を渡すときは、int[][] ではなくて int[,] を使います C++ や Fortran と同じように連続したメモリに int 値が配置されるようにするためです。ADD1 関数を呼び出すときに ref を付けるのが面倒なのですが、これで Fortran の DLL を正しく呼び出せます。
F# から呼び出してみる
更に F# に書き換えてみましょう。dll から export されているポインタは int& のように宣言します。
open System open System.Runtime.InteropServices type ARY2D = int[,] [<AutoOpen>] module FDLL = [<DllImport("FModule.dll", CallingConvention = CallingConvention.Cdecl)>] extern int ADD1(int& a, int& b) [<DllImport("FModule.dll", CallingConvention = CallingConvention.Cdecl)>] extern int SUM1(int[] v, int& count ) // int[,] がそのままでは渡せないので alias を作って指定する [<DllImport("FModule.dll", CallingConvention = CallingConvention.Cdecl)>] // extern int SUM2(int[,] v, int& cnti, int& cnt2 ) extern int SUM2(ARY2D v, int& cnti, int& cnt2 ) let mutable a = 10 let mutable b = 20 let ans = ADD1( &a , &b ) Console.WriteLine( "ans {0}", ans ) let mutable v = [|1;2;3;4;5|] let mutable cnt = v.Length let ans2 = SUM1( v, &cnt ) Console.WriteLine( "ans2 {0}", ans2 ) // 二次元配列は Array2D で作る let vv = [| [|1;2;3|]; [|11;22;33|]; [|10;20;30|]; [|100;200;300|]; |] let v2 = Array2D.init 4 3 ( fun i j -> vv.[i].[j]) let mutable cnti = 3 let mutable cntj = 4 let ans3 = SUM2( v2, &cnti, &cntj ) Console.WriteLine( "ans3 {0}", ans3 )
C# と比べて簡素に書ける…ほどではなくて、あまり変わりませんね。それよりも、気になるのは const で渡したいのに let mutable を使わないといけないところです。これは Fortran の呼び出しの制約で、値のポインタを渡すために、内容が変更されなくても a の変数は mutable にしないと渡せないのです。ここはひと工夫したいところです。せっかくの immutable な値なのに、ってところです。
あと、C# での2次元配列 int[,] を F# でどうやって書くのか不明だったので…すが、方法が解りました。Array2D を使うと int[,] 相当なものが作れます。これで、もう一工夫必要なのは extern するときに int[,] で型指定ができないので、alias を作ります。ここでは、ARY2D という型 alias を作って extern int SUM2( ARY2D v, … ) としています。こうすると、C++ と同じように2次元配列として渡せるようになります。
実行した結果は 2次元配列の向きが C++ と逆になっていますが、まあ、こっちのほうが分かりやすいかなと。Xij のほうが X[j,i] よりも書きやすいかなと思います(いや、単純に順番を間違えただけなんですけどね)。
まとめ
というわけで、1年越しの案件?として、F# から Fortran を扱うネタ記事ができました。これで、既存の Fortran ライブラリを F# から作ったアプリが作れますね…っていうか、誰得な技術情報なのですが、いやあ、やってみたかったということで。皆様、よいクリスマスをッ!!!
ピンバック: F# Weekly #50, 2014 | Sergey Tihon's Blog