相互運用性:FortranとC#

ご存知のように、世界中には数百万行のレガシーコードがあります。 もちろん、レガシーの最初の場所はKobolに属しますが、Fortranもたくさんあります。 また、主にコンピューティングモジュール。



少し前まで、彼らは「グラフィックスやインターフェースなど、何か美しいものを作る」というタスクを持つ小さなプログラム(1000行未満、4分の1以上-コメントと空白行)を私にもたらしました。 プログラムは小さいですが、やり直したくありませんでした。叔父は慎重に走り、さらに2か月間調整を行います。



いくつかのコードとテキスト車の形での作業の結果は慎重にカットの下に設定されています。





問題の声明



Fortranには何かを数えるプログラムがあります。 タスク:それを最小化するために、できれば-作業のロジックに入ることなく-入力パラメーターを別のモジュールに配置し、結果の出力をする。



これを行うには、次のことを行う方法を学ぶ必要があります。



フロントエンドはC#で行います-まず、WPFのおかげで、クロスプラットフォームは必要ありません。



環境



まず、環境を準備します。



コンパイラーとして、GCCパッケージのgfortranを使用しました( こちらから入手できます )。 GNU makeも便利です(これは近くにあります )。 ソースコードエディタとして何でも使用できます。 Photranプラグインで日食を置きます。



Eclipseへのプラグインのインストールは、標準のリポジトリから、ベースJunoリポジトリのメニュー項目「Help」/「Install New Software ...」を使用して行います(フィルターにPhotranを入力します)。



すべてのソフトウェアをインストールしたら、gfortranへのパスを登録し、標準パスにバイナリを作成する必要があります。



すべてのプログラムは古いFortran方言で書かれています。つまり、各行の先頭に6スペースのインデントが必須です。 行は、72の習熟度に制限されています。 ファイルの拡張子は。 私がそんなに古くてハードコアなわけではありませんが、私たちが持っているのはそれで働いています。



C#を使用すると、すべてが明確になります-スタジオ。 私はVS2010で働いていました。



最初のプログラム



Fortran



はじめに、簡単なFortranプログラムを作成しましょう。

  module test contains subroutine hello() print *, "Hello, world" end subroutine end module test program test_main use test call hello() end program
      
      



ここでは詳細を分解しません。ここではFortranを教えていませんが、遭遇する瞬間について簡単に説明します。



まず、モジュール。 あなたはそれらを行うことができます、あなたはそれらを行うことはできません。 モジュールを使用したテストプロジェクトでは、エクスポートされたメソッドの名前に影響しました。 戦闘ミッションでは、すべてが1つのピースに書き込まれ、モジュールはありません。 要するに、それはあなたが継承として何をしたかにかかっています。



第二に、Fortran構文は、その中のスペースがオプションであるようなものです。 endif



を書くことができ、 end if



を書くことができます。 do1i=1,10



を実行できますが、人間的にdo 1 i = 1, 10



do1i=1,10



実行できます。 したがって、これは間違いの貯蔵庫にすぎません。 30分の間、私はなぜラインが

  callback()
      
      



何を書くべきか_back()



わかるまで、「 _back()



文字が見つかりませんでした」というエラーを出し_back()





  call callback()
      
      



だから注意してください。



第三に、方言f90およびf95では、行の先頭にインデントを付ける必要がありません。 ここでも、すべてはあなたに来たものに依存します。



しかし、プログラムに戻りましょう。 Eclipse(makefileが正しく構成されている場合)またはコマンドラインからコンパイルします。 始めるために、コマンドラインから作業しましょう:

 > gfortran -o bin\test.exe src\test.for
      
      





起動されたexeファイルは、a)Fortranのランタイムdllを必要とし、b)文字列「Hello、world」を表示します。



ランタイムを必要としないexeを取得するには、 -static



スイッチを使用してコンパイルを実行する必要があります。

 > gfortran -static -o bin\test.exe src\test.for
      
      





dllを取得するには、別の-shared



キーを追加する必要があります。

 > gfortran -static -shared -o bin\test.dll src\test.for
      
      





とりあえず、Fortranを終了してC#に進みましょう。



C#



完全に標準的なコンソールアプリケーションを作成しましょう。 すぐに別のクラスTestWrapper



を追加して、いくつかのコードを記述します。

  public class TestWrapper { [DllImport("test.dll", EntryPoint = "__test_MOD_hello", CallingConvention = CallingConvention.Cdecl)] public static extern void hello(); }
      
      





プロシージャへのエントリポイントは、標準のdumpbin



VSユーティリティを使用して決定されます。

 > dumpbin /exports test.dll
      
      





このコマンドは、興味のある行を見つけることができる長いダンプを提供します:

  3 2 000018CC __test_MOD_hello
      
      





grep



使用して検索するか、 dumpbin



出力をファイルにダンプして、それをdumpbin



できます。 主なもの-呼び出しに配置できるエントリポイントのシンボル名を見ました。



さらに簡単です。 メインモジュールProgram.csで呼び出しを行います。

  static void Main(string[] args) { TestWrapper.hello(); }
      
      





コンソールアプリケーションを起動すると、Fortranで表示された「Hello、world」という行が表示されます。 もちろん、Fortranでコンパイルされたtest.dllをbin/Debug



(またはbin/Release



bin/Release



に入れることを忘れないでbin/Release







原子パラメーター



しかし、データをそこに転送して何かを取り戻すことは、すべて面白くなく興味深いことです。 このため、2回目の反復を実行します。 たとえば、最初のパラメーターに数値1を追加し、その結果を2番目のパラメーターに渡すプロシージャとします。



Fortran



手順は簡単に不名誉です。

  subroutine add_one(inVal, retVal) integer, intent(in) :: inVal integer, intent(out) :: retVal retVal = inVal + 1 end subroutine
      
      





Fortranでは、呼び出しは次のようになります。

  integer :: inVal, retVal inVal = 10 call add_one(inVal, retVal) print *, inVal, ' + 1 equals ', retVal
      
      





次に、このコードをコンパイルしてテストする必要があります。 一般に、コンソールからコンパイルを続行できますが、メイクファイルがあります。 ビジネスに添付しましょう。



exe(テスト用)およびdll(「本番バージョン」用)を行うため、最初にオブジェクトコードにコンパイルしてから、そこからdll / exeをビルドするのが理にかなっています。 これを行うには、Eclipseでメイクファイルを開き、スピリットで何かを記述します。

 FORTRAN_COMPILER = gfortran all: src\test.for $(FORTRAN_COMPILER) -O2 \ -c -o obj\test.obj \ src\test.for $(FORTRAN_COMPILER) -static \ -o bin\test.exe \ obj\test.obj $(FORTRAN_COMPILER) -static -shared \ -o bin\test.dll \ obj\test.obj clean: del /Q bin\*.* obj\*.* *.mod
      
      





これで、Eclipseのボタンを使用して、人間がプロジェクトをコンパイルしてクリーンアップできます。 ただし、これには、作成するパスを環境変数に設定する必要があります。



C#



次に続くのは、C#でのシェルの改良です。 最初に、dllからプロジェクトに別のメソッドをインポートします。

  [DllImport("test.dll", EntryPoint = "__test_MOD_add_one", CallingConvention = CallingConvention.Cdecl)] public static extern void add_one(ref int i, out int r);
      
      





dumpbin



を使用して、以前のようにエントリポイントを定義します。 変数があるため、呼び出しcdecl



(この場合はcdecl



)を指定する必要があります。 変数は参照によって渡されるため、 ref



必要です。 ref



を省略すると、呼び出すときにAVが返されます。「未処理の例外: System.AccessViolationException



:保護されたメモリの読み取りまたは書き込みを試みました。 多くの場合、これは別のメモリが破損していることを示しています。」



メインプログラムで、次のように記述します。

  int inVal = 10; int outVal; TestWrapper.add_one(ref inVal, out outVal); Console.WriteLine("{0} add_one equals {1}", inVal, outVal);
      
      





一般的に、すべて、問題は解決されます。 1つの「しかし」ではない場合-再び、Fortranフォルダーからtest.dll



をコピーする必要があります。 手順は機械的なものであるため、自動化する必要があります。 これを行うには、プロジェクトの[プロパティ]を右クリックし、[ビルドイベント]タブを選択して、[ビルド前のイベントのコマンドライン]ウィンドウにスピリットで何かを記述します。

 make -C $(SolutionDir)..\Test.for clean make -C $(SolutionDir)..\Test.for all copy $(SolutionDir)..\Test.for\bin\test.dll $(TargetDir)\test.dll
      
      



もちろん、私たちは独自の方法に置き換える必要があります。



全体として、コンパイルと起動の後、すべてがうまくいった場合、2番目のバージョンの作業プログラムを取得します。





記述されたコードの呼び出されたdllモジュールに初期パラメーターを渡すだけで十分だとします。 しかし、多くの場合、何らかの方法で文字列をスローする必要があります。 私が理解していなかった待ち伏せが1つあります-エンコード。 したがって、私の例はすべてラテンアルファベット用です。



Fortran



ここではすべてが単純です(ハードコアワーカー向け)。

  subroutine progress(text, l) character*(l), intent(in) :: text integer, intent(in) :: l print *, 'progress: ', text end subroutine
      
      





dllやその他の相互運用性なしで、Fortran内メソッドを記述した場合、長さを送信できませんでした。 また、モジュール間でデータを転送する必要があるため、2つの変数、文字列へのポインターとその長さを操作する必要があります。



メソッドの呼び出しも簡単です。

  character(50) :: strVal strVal = "hello, world" call progress(strVal, len(trim(strVal)))
      
      





len(trim())



は、末尾のスペースをトリミングするために指定されます(1行に50文字が割り当てられ、12文字のみが使用されるため)。



C#



次に、このメソッドをC#から呼び出す必要があります。 この目的のために、 TestWrapper



を完成させTestWrapper





  [DllImport("test.dll", EntryPoint = "__test_MOD_progress", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] public static extern void progress([MarshalAs(UnmanagedType.LPStr)]string txt, ref int strl);
      
      





別のインポートオプションがここに追加されますCharSet



によって使用されCharSet



。 コンパイラには、文字列MarshalAs



を渡すための命令も表示されます。



呼び出しは、すべてのパラメーターを参照( ref



)で渡す必要があるために生じる冗長性を除き、同時に些細に見えます:

  var str = "hello from c#"; var strLen = str.Length; TestWrapper.progress(str, ref strLen);
      
      





コールバック



最も興味深いのは、コールバック、またはdll内でメソッドを渡して何が起こっているかを追跡することです。



Fortran



まず、関数をパラメーターとして受け取る実際のメソッドを作成します。 Fortranでは、次のようになります。

  subroutine run(fnc, times) integer, intent(in) :: times integer :: i character(20) :: str, temp, cs interface subroutine fnc(text, l) character(l), intent(in) :: text integer, intent(in) :: l end subroutine end interface temp = 'iter: ' do i = 1, times write(str, '(i10)') i call fnc(trim(temp)//trim(str), len(trim(temp)//trim(str))) end do end subroutine end module test
      
      





ここで、転送されたメソッドのプロトタイプを記述するinterface



新しいセクションに注意を払う必要がありinterface



。 かなり冗長ですが、一般に、新しいものはありません。



このメソッドの呼び出しは絶対に平凡です:

  call run(progress, 10)
      
      





その結果、前の反復で作成されたprogress



メソッドが10回呼び出されます。



C#



C#に移動します。 ここで、追加の作業を行う必要があります。正しい属性を使用してTestWrapper



クラスでデリゲートを宣言します。

  [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void Progress(string txt, ref int strl);
      
      





その後、呼び出されるrun



メソッドのプロトタイプを定義できます。

  [DllImport("test.dll", EntryPoint = "__test_MOD_run", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] public static extern void run(Progress w, ref int times);
      
      





エントリポイントは従来、 dumpbin



から決定されます。 残りも私たちによく知られています。



このメソッドの呼び出しも簡単です。 そこには、ネイティブのFortranメソッド( TestWrapper.progress



など、最後の反復で説明)またはC#ラムダのいずれかをTestWrapper.progress



ことができます。

  int rpt = 5; TestWrapper.run(TestWrapper.progress, ref rpt); TestWrapper.run((string _txt, ref int _strl) => { var inner = _txt.Substring(0, _strl); Console.WriteLine("Hello from c#: {0}", inner); }, ref rpt);
      
      





そのため、メソッド内でコールバックを渡して容量のある操作の実行の進行状況を表示するような方法でコードをやり直すのに十分なツールが既にあります。 まだわからないことは、配列を渡すことだけです。



配列



文字列よりも少し複雑です。 文字列の場合、いくつかの属性を記述するだけで十分であれば、配列の場合はペンで少し作業する必要があります。



Fortran



まず、配列を印刷するための手順を、文字列転送の形式で将来のために少し予約して作成します。

  subroutine print_arr(str, strL, arr, arrL) integer, intent(in) :: strL, arrL character(strL), intent(in) :: str real*8, intent(in) :: arr(arrL) integer :: i print *, str do i = 1, arrL print *, i, " elem: ", arr(i) end do end subroutine
      
      





double



(またはreal



倍精度)の配列宣言が追加され、そのサイズも送信されます。

Fortranからの呼び出しも一般的です。

  character(50) :: strVal real*8 :: arr(4) strVal = "hello, world" arr = (/1.0, 3.14159265, 2.718281828, 8.539734222/) call print_arr(strVal, len(trim(strVal)), arr, size(arr))
      
      





出力は、印刷された文字列と配列です。



C#



TestWrapper



特別なものTestWrapper



ません。

  [DllImport("test.dll", EntryPoint = "__test_MOD_print_arr", CallingConvention = CallingConvention.Cdecl)] public static extern void print_arr(string titles, ref int titlesl, IntPtr values, ref int qnt);
      
      





ただし、プログラム内では少し作業をして、 System.Runtime.InteropServices



アセンブリを使用する必要があります。

  var s = "abcd"; var sLen = s.Length; var arr = new double[] { 1.01, 2.12, 3.23, 4.34 }; var arrLen = arr.Length; var size = Marshal.SizeOf(arr[0]) * arrLen; var pntr = Marshal.AllocHGlobal(size); Marshal.Copy(arr, 0, pntr, arr.Length); TestWrapper.print_arr(s, ref sLen, pntr, ref arrLen);
      
      





これは、Fortranプログラム内で配列へのポインターを渡す必要があるためです。つまり、管理領域から非管理領域にデータをコピーする必要があり、それに応じてメモリが割り当てられます。 この点で、次のようなシェルを作成するのは理にかなっています。

  public static void PrintArr(string _titles, double[] _values) { var titlesLen = _titles.Length; var arrLen = _values.Length; var size = Marshal.SizeOf(_values[0]) * arrLen; var pntr = Marshal.AllocHGlobal(size); Marshal.Copy(_values, 0, pntr, _values.Length); TestWrapper.print_arr(_titles, ref titlesLen, pntr, ref arrLen); }
      
      





すべてをまとめる



すべての反復の完全なソースコード(および配列をコールバック関数に転送するという形でのもう少しのボーナス)は、 ビットパケット (hg)のリポジトリにあります。 誰かが追加した場合-あなたはコメントを歓迎します。



伝統的に、非常に多くのテキストが判明したため、最後まで読んでくれたすべての人に感謝します。



All Articles