.Net と ネイティブ な C/C++ Dll インタフェース
Mol は多くの計算部分で
インテル® MKL(Math Kernel Library)を利用しています。Mol は .Net 専用のクラスライブラリー、MKLは従来の(C/C++やFORTRANで記述された)ネイティブな関数ライブラリーです。実際には C/C++ で記述された、ネイティブな DLL、Mol.C++.Dll が Mol と MKL の間を調整しています。
Mol(.Net クラスライブラリー) <ー> Mol.C++.Dll(ネイティブ DLL) <ー> MKL(ネイティブ DLL)
以下、.Net からネイティブな Dll 関数を利用するプログラミング手法をまとめてみました。
さらに Mol のネイティブ Dll インターフェース用クラス NativeDll と NonlinearEquations クラス(非線形最少二乗化問題の解法)についても記述しました。
C++ によるネイティブ Dll の記述
まず、ネイティブ Dll (UserNative.Dll)の簡単な例です(C++)。
例題の関数 CDllTest は .Net サイドから以下のデータ(情報)を受け取り
スカラーデータ: int N、int nt
配列 : double *arFrom
文字列 : char *stFrom
以下のデータを .Net サイドに戻します。
文字列 : char *stTo
配列 : double *arTo (関数の戻り値)
- .Net サイドからネイティブ DLL 関数の呼び出し例です(逆の例は扱いません)。
- 多くの場合、文字列と配列データを相互に交換できれば十分でしょう。
以下の Dll は UserNative.Dll とします。
UserNative.Dll 自体は Mol の例題 にソースファイルやソリューションファイル等が含まれていますので、以下のプログラムを追加して実行することは簡単にできます。
#include "stdafx.h"
// Mol.h には各種宣言や利用方法などが記述されていますので参考にしてください。
#include "Mol.h"
//MOL_DLL_EXPORT(t) は Mol.h で以下のように定義されています。
//#define MOL_DLL_EXPORT(t) extern "C" __declspec(dllexport) t __cdecl
MOL_DLL_EXPORT(double *)
CDllTest(double *arFrom,int N, char *stFrom, char * stTo,int nt)
{
double *arTo = new double[N];
printf(" C++:.Net からの配列 =");
for(int i=0;i<N;++i) {
printf("%g ",arFrom[i]);
arTo[i] = arFrom[i]+1.0;
}
printf("\n");
printf(" C++:.Net からの文字列=%s\n",stFrom);
strcpy(stTo,"C++ から .Net への文字列");
return arTo;
}
- MOL_DLL_EXPORT は .Net (Dll外)から呼び出される関数を定義しています。
- MOL_DLL_EXPORT 等のマクロを使用しなければ Mol.h をインクルードする必要はありません。
- __cdecl等のキーワードの意味は検索等で確認してください。
呼び出し例1) C++ Dll(UserNative.Dll) のロード、アンロードを .Net に任す方法
前記の、C/C++ で記述された ネイティブ Dll 内関数、CDllTest() を .Net の C# プログラムから呼び出すには、まず、C#の先頭で、以下のように宣言します。
[DllImport("UserNative.dll", CharSet = CharSet.Ansi,CallingConvention=CallingConvention.Cdecl)]
public static extern IntPtr CDllTest(
[MarshalAs(UnmanagedType.LPArray)] double[] arr,int na,
string str,
StringBuilder stb, int nb
);
- CharSet = CharSet.Ansi は UserNative.dll が Ansi (Shift_JIS) 文字列を扱うことを示しています。
- C#の文字列は Unicode が基本ですので、CDllTest 呼び出し時に、文字コードの変換が自動的に実行されます。
- IntPtr はハンドルやポインターの宣言に使用します(特に Int に対するポインターに限定されません)。
- CDllTest()から文字列を受け取る場合は StringBuilder を使用します。
UserNative.Dll の CDllTest() 関数が正しく宣言されれば、あとは普通に呼び出すだけです(.Net が Dll を自動的にロード/アンロードします)。
public void Test1()
{
StringBuilder stb = new StringBuilder(N);
double[] ar = new double[N];
for (int i = 0; i < N; ++i) ar[i] = i + 1.0;
IntPtr pt = CDllTest(ar, N, ".Net から C++ への文字列", stb, stb.Capacity);
Console.Write(".Net: C++ からの文字列=");
Console.WriteLine(stb.ToString());
// マネージ配列へコピー
Marshal.Copy(pt, ar, 0, N);
Console.Write(".Net: C++ からの配列 =");
for (int i = 0; i < N; ++i)
{
Console.Write(ar[i]+" ");
}
Console.Write("\n");
}
- 例では、CDllTest() から受け取った配列(IntPtr pt)を開放(delete[])していません。開放用の関数定義(C++側で開放関数を記述)等は省略しています。
呼び出し例2) Windows API を使用する例
以下は Windows API を利用して、Dll のロードやアンロードをC#プログラマーが制御する方法です。
呼び出し例1と比べて
- 実行時にロードする DLL を選択できる
- 関数が定義されているかどうか調べることができる
- 呼び出したいC/C++関数は Delegate として宣言する
という点が異なり、より柔軟性が高い方法といえます。
まずは宣言部分です。
// Windows API の宣言(CallingConvention.Cdeclは付けない、デフォルトのStdCallになっている)
[DllImport("kernel32")]internal static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
[DllImport("kernel32")]internal static extern bool FreeLibrary(IntPtr hModule);
[DllImport("kernel32")]internal static extern IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName);
// Windows API とは異なり CallingConvention.Cdecl を指定する。
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate IntPtr DelegateCDllTest(
[MarshalAs(UnmanagedType.LPArray)] double[] ar, int na,
string str,
StringBuilder stb, int nb
);
若干長くなりますが、呼び出しプログラムは以下のようになります。
public static void Test2()
{
// DLL のロード
IntPtr handle = LoadLibrary("UserNative.dll");
// 関数の検索
IntPtr funcPtr = GetProcAddress(handle, "CDllTest");
// デリゲートの作成
DelegateCDllTest PtrTestDelegate = (DelegateCDllTest)Marshal.GetDelegateForFunctionPointer(funcPtr, typeof(DelegateCDllTest));
StringBuilder stb = new StringBuilder(N);
double[] ar = new double[N];
for (int i = 0; i < N; ++i) ar[i] = i + 1.0;
// CDllTest() の呼び出し。
IntPtr pt = PtrTestDelegate(ar, N, ".Net から C++ への文字列", stb, stb.Capacity);
Console.Write(".Net: C++ からの文字列=");
Console.WriteLine(stb.ToString());
// マネージ配列へコピー
Marshal.Copy(pt, ar, 0, N);
Console.Write(".Net: C++ からの配列 =");
for (int i = 0; i < N; ++i)
{
Console.Write(ar[i] + " ");
}
Console.Write("\n");
// DLL のアンロード
FreeLibrary(handle);
}
実行結果
「呼び出し例1」と「呼び出し例2」ともに以下の結果が出力されます。
C++:.Net からの配列 =1 2 3 4 5 6 7 8 9 10
C++:.Net からの文字列=.Net から C++ への文字列
.Net: C++ からの文字列=C++ から .Net への文字列
.Net: C++ からの配列 =2 3 4 5 6 7 8 9 10 11
※「コンソールアプリケーション」としてビルドしてください。
Mol NativeDll クラス によるネイティブ Dll の利用
Mol には NativeDll クラスが用意されています。
NativeDll クラスは「呼び出し例2」に於ける Windows API を気にする必要のないようにした簡単なラッパークラスです。
コンストラクターにパス名を指定するだけで Dll がロードされます。
Dll のアンロードはデストラクター内で自動実行されるので特にプログラマーが気にする必要はありません(もちろん Dispose() メソッドで明示的にアンロードすることもできます)。
前記の例題 Test2() は、Windows API の宣言を削除して、以下のように簡単に記述できます。
public static void Test2()
{
// DLL のロード
NativeDll dll = new NativeDll("UserNative.dll");
// 関数の検索
IntPtr funcPtr = dll.GetFunctionPtr("CDllTest");
// デリゲートの作成
DelegateCDllTest PtrTestDelegate = (DelegateCDllTest)Marshal.GetDelegateForFunctionPointer(funcPtr, typeof(DelegateCDllTest));
............................
}
Mol のネイティブ Dll Mol.C++.Dll とユーザ作成のネイティブ Dllの連携
NativeDll のコンストラクターに関数名(引数は固定されています)を指定して、
NativeDll クラスの作成時に指定した関数を呼び出すことができます。
また、デストラクター(Dispose)呼び出し時点でも呼び出されるので、
ロード/アンロード時に特別な処理が必要な場合に利用できます。
// DLL のロードと OnLoad() 関数の呼び出し。
NativeDll dll = new NativeDll("UserNative.dll","OnLoad");
............................
OnLoad(関数名は任意ですが、引数は固定)は UserNative.dll と Mol.C++.Dll のインタフェースも考慮して
用意されています(が、現時点では将来のために用意してあるといったレベルです)。
以下、例題 UserNative.dll からの抜粋です。
// Mol が提供するサービス関数のアドレスを保持する配列(OnLoad で設定されます)。
static PTR * gpLibFunc = NULL;
//
// NativeDllクラスがユーザ作成DLLをロード/アンロードするときに呼び出す関数(実装はオプション)の定義例。
// 本DLLを UserNative.Dll とすると、以下のように指定します(.Netサイドで)。
//
// NativeDll dll = new NativeDll("UserNative.Dll","OnLoad"); この時点で、OnLoad()が呼ばれます。
//
// ※ NativeDll dll = new NativeDll("UserNative.Dll"); も可ですが、本関数は呼ばれません。
//
// 引数の意味(アンロード時は p==NULL、np==0 です)
// p[] ... Molオブジェクトを操作する関数アドレスなどの情報を格納した配列。
// 現状ではベクトルや行列の配列アドレスを得る関数アドレスが最初の要素に格納されています。
// np ... 配列 p[] の要素数。
//
// 戻り値 0 ... 正常終了。 ゼロ以外は .Net サイドで例外が発生します。
//
MOL_DEF_NATIVE_LOAD OnLoad(PTR p[],int np)
{
if(p==NULL) return 0; // アンロード時、何もしない。
gpLibFunc = p;
return 0;
}
非線形最少二乗問題の解法: NonlinearEquations クラスと NativeDll クラス
NonlinearEquations クラスはもちろん名前の通り非線形連立方程式
Yi(X) = Fi(X1,X2,...,Xn)
i=1,2,...m
を処理するクラスです(Y,F,X等は Double のベクトル、Fiは非線形方程式)。
現状では線形連立方程式のユークリッドノルム(各成分の二乗和の平方根: ((ΣiYi(X)2)1/2)を最小化する非線形最少二乗問題の解法(MinimizeNorm()メソッド)がサポートされています。
非線形方程式 Fi(X1,X2,...,Xn) はプログラマー
が具体的に記述して値を Yi に格納しなければばりません。
Fi の計算部分は以下のどちらかの方法で実装します。
- メソッドを delegate として NonlinearEquations に与える。
- C/C++ 関数を記述したネイティブ DLL を NativeDll クラスでロードしてから、NonlinearEquations に与える。
いずれにしても Mol に組み込まれた非線形最少二乗問題解法エンジンが必要な時に呼び出します。
全てを C# で記述できるなら delegate として計算部分を定義します。
既に C/C++ 等の計算部分が存在するならネイティブな DLL を作成して、.Net から直接呼び出すか NativeDll クラスを通じて呼び出すようにします。
呼び出し時のメソッドや関数の引数は固定です。詳細は例題等を参照してください。