Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9703

C# DllExportでWindowスタイルなAPIをC#風に扱う

$
0
0

TL;DR;

DllImportにPreserveSig=falseをつけると、HRESULTを返す関数を自動で例外に変換してくれる。
そして、空いた戻り値に出力引数の実体を返してくれます。
便利!

概要

C#とC/C++の相互運用といえば、C++/CLI、C++/CX、COM、IPCなどが考えられます。
その中でも、System.Runtime.InteropServices.DllImport(P/Invoke)という機能があります。
DllImportは基本的には、C++でExportした関数と同じ関数名で(互換性のある)引数、戻り値のメソッドをC#に宣言し、属性でマークすることで使用できます。
しかし、引数や戻り値に関してはintなどの型互換だけでなく、参照<->ポインタ、HRESULT<->例外などの互換も可能です。
また、C言語ではよく見かける出力引数を、一定条件下で戻り値として扱うこともできます。

サンプル

次のようなC++のDLLがあるとします。
C言語(Windows)ではよく、戻り値に成否を使い、引数に二重ポインタを渡し、ポインタ先に返したいオブジェクトのポインタを書き込みます。

// 二重ポインタ(本体の確保は呼び出され側)extern"C"__declspec(dllexport)WINAPIHRESULTGetBestCouple(constTCHAR**ppAnswer){*ppAnswer=TEXT("YukaMaki");returnE_ABORT;}// 一重ポインタ(本体の確保は呼び出し側)extern"C"__declspec(dllexport)WINAPIHRESULTGetBestCouple2(constTCHAR*pAnswer){lstrcpyA(pAnswer,TEXT("YukMaki_01234567890123456789"));returnE_ABORT;}

ちなみに、これは以下のようなメモリ配置になっています。(sizeof(void*)==4の場合)

\0を忘れていました、YUKAMAKIの後に\0が続きます。ですので、TCHAR[8]ではなく、TCHAR[9]です。

これを、色々な条件でDLLImportしてみましょう。

Case 1

普通に二重ポインタ(TCHAR**)を渡し、実態へのポインタを格納しているメモリ領域を、C++側が確保した領域へのポインタに書き換えてもらう。C#側では二重ポインタ(TCHAR**)からTCHAR*を取り出して、マーシャラでデコード。

[DllImport("D3DVisualization.dll",EntryPoint="GetBestCouple")]staticexternintGetBestCouple_H_PP_(IntPtrppAnswer);// 本体A(TCHAR[])へのポインタB(TCHAR*)へ// のポインタC(TCHAR**)のBの部分のメモリを確保してCにアドレスを記録// TCHAR* pAnswer; // 確保// TCHAR** ppAnswer = &pAnswer;// の操作に相当IntPtrppAnswer=Marshal.AllocHGlobal(IntPtr.Size);inthResult=GetBestCouple_H_PP_(ppAnswer);// TCHAR* pAnswer = *ppAnswer;// の操作に相当stringanswer=Marshal.PtrToStringAuto(pAnswer);Console.WriteLine($"{answer} ({nameof(GetBestCouple_H_PP_)}) -> {hResult}");

Case 2

Case 1と同様の関数を呼び出しているが、C#側がOut参照を渡しています。(これはref/in参照でもOK)
C#の参照もポインタと同じようなものなので、ポインタの参照、つまり二重ポインタと同様に働きます。
そのため、二重ポインタから本体の領域へのポインタを取り出す操作がなくなっています。

[DllImport("D3DVisualization.dll",EntryPoint="GetBestCouple")]staticexternintGetBestCouple_H_RP_(outIntPtrpAnswer);IntPtrpAnswer;inthResult=GetBestCouple_H_RP_(outpAnswer);stringanswer=Marshal.PtrToStringAuto(pAnswer);Console.WriteLine($"{answer} ({nameof(GetBestCouple_H_RP_)}) -> {hResult}");

Case 3

Case 2では戻り値を受け取っていましたが、捨てることも可能。

[DllImport("D3DVisualization.dll",EntryPoint="GetBestCouple")]staticexternvoidGetBestCouple_V_P_E(outIntPtrpAnswer);IntPtrpAnswer;GetBestCouple_V_P_E(outpAnswer);stringanswer=Marshal.PtrToStringAuto(pAnswer);Console.WriteLine($"{answer} ({nameof(GetBestCouple_V_P_E)})");

Case 4

PreserveSig=falseを付けることで、捨てたHRESULTの代わりに、ポインタの中身を変えさせることが可能。
Case 3のようにout参照にできるものを変えさせられます。
本来のPreserveSigは、HRESULTが戻り値の時、例外に変換して投げてくれるというものなので、戻り値がHRESULTでないとつかえません。

[DllImport("D3DVisualization.dll",EntryPoint="GetBestCouple",PreserveSig=false)]staticexternIntPtrGetBestCouple_P__E();IntPtrpAnswer=GetBestCouple_P__E();stringanswer=Marshal.PtrToStringAuto(pAnswer);Console.WriteLine($"{answer} ({nameof(GetBestCouple_P__E)})");

Case 5

answerCopyBufferにコピーはされるけど、制御が戻る前にクラッシュします。
MSDNの説明と矛盾しているように思えますが🤔🤔🤔
文字列に対する既定のマーシャリング(MSDN)

[DllImport("D3DVisualization.dll",EntryPoint="GetBestCouple")]staticexternintGetBestCouple_V_RS_([MarshalAs(UnmanagedType.LPWStr),Out]outStringBuilderanswerCopyBuffer);StringBuilderanswerCopyBuffer=newStringBuilder(256);GetBestCouple_V_RS_(outanswerCopyBuffer);Console.WriteLine($"{answerCopyBuffer} ({nameof(GetBestCouple_V_RS_)})");

Case 6

ここから、二重でないポインタ版。
こういう場合、呼び出し側(C#)でポインタの先の実体のメモリ領域が確保されていることが期待されます。
answerCopyBufferは十分なメモリが確保されていないのでBufferOverflowしてクラッシュ。
長さを知るための関数が必要になってしまうのでMarshalAsは意外と使えない子な気がします。

[DllImport("D3DVisualization.dll",EntryPoint="GetBestCouple2")]staticexternintGetBestCouple_V_B_([MarshalAs(UnmanagedType.LPWStr)]StringBuilderanswerCopyBuffer);StringBuilderanswerCopyBuffer=newStringBuilder(0);GetBestCouple_V_B_(answerCopyBuffer);Console.WriteLine($"{answerCopyBuffer} ({nameof(GetBestCouple_V_B_)})");

Case 7

偶然クラッシュはしなかったものの、BufferOverflowはしています。

[DllImport("D3DVisualization.dll",EntryPoint="GetBestCouple2")]staticexternintGetBestCouple_V_S_([MarshalAs(UnmanagedType.LPWStr)]stringanswerCopyBuffer);stringanswerCopyBuffer="01";// size = 3;GCHandlehandle=GCHandle.Alloc(answerCopyBuffer,GCHandleType.Pinned);IntPtrptr=handle.AddrOfPinnedObject();// stringの中身を直接覗くGetBestCouple_V_S_(answerCopyBuffer);Console.WriteLine($"{answerCopyBuffer} ({nameof(GetBestCouple_V_S_)})");handle.Free();

まとめ

DllImportのために宣言するメソッドは意外と柔軟であることがわかりました。
そもそも、DLLのリンクはextern Cなので、関数名しか見ていませんが…(故にオーバーロードとか無理)
型互換以外(ポインタも型ですが)についてまとめましたが、型も結構自由が利き、ユーザ定義のclass/structなんがが使えたりします。
StructLayoutAttribute クラス(MSDN)

…とはいえ、DllImportしたメソッドはラップして利用することが推奨されていますし、ここまでの紹介は全てラッパーで解決できることだったりします。

参考

WinAPIからの値の受け取り方について(C# と VB.NET の質問掲示板)
PreserveSigAttribute クラス(MSDN)
文字列に対する既定のマーシャリング(MSDN)


Viewing all articles
Browse latest Browse all 9703

Trending Articles