相互運用性

# アンマネージ C++ DLL から関数をインポート

アンマネージ C++ DLL で定義されている関数をインポートする方法の例を次に示します。 「myDLL.dll」の C++ ソース コードでは、関数 add が定義されています:

extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
{
    return a + b;
}

その後、次のように C# プログラムに含めることができます:

class Program
{
    // This line will import the C++ method.
    // The name specified in the DllImport attribute must be the DLL name.
    // The names of parameters are unimportant, but the types must be correct.
    [DllImport("myDLL.dll")]
    private static extern int add(int left, int right);

    static void Main(string[] args)
    {
        //The extern method can be called just as any other C# method.
        Console.WriteLine(add(1, 2));
    }
}

extern "C" の理由については、呼び出し規則と C++ 名マングリングを参照してください。 そして __stdcall

# 動的ライブラリの検索

extern メソッドが最初に呼び出されると、C# プログラムは適切な DLL を検索してロードします。 DLL を見つけるために が検索される場所、および検索場所に影響を与える方法の詳細については、このスタックオーバーフローの質問を参照してください。

# 呼び出し規約

関数の呼び出しにはいくつかの規則があり、誰 (呼び出し元または呼び出し先) がスタックから引数をポップするか、引数がどのように、どのような順序で渡されるかを指定します。 C++ は Cdecl を使用します デフォルトでは呼び出し規約ですが、C# では StdCall が必要です 、通常は Windows API で使用されます。どちらかを変更する必要があります:

  • C++ で呼び出し規約を `StdCall` に変更:
    extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
    
    
    [DllImport("myDLL.dll")]
    
    
  • または、C# で呼び出し規約を `Cdecl` に変更します:
    extern "C" __declspec(dllexport) int /*__cdecl*/ add(int a, int b)
    
    
    [DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl)]
    
    
  • Cdeclで関数を使いたい場合 呼び出し規約とマングル名を使用すると、コードは次のようになります:

    __declspec(dllexport) int add(int a, int b)
    
    
    [DllImport("myDLL.dll", CallingConvention = CallingConvention.Cdecl,
               EntryPoint = "?add@@YAHHH@Z")]
    
    
  • **thiscall**(**__thiscall**) は、主にクラスのメンバーである関数で使用されます。
  • 関数が **thiscall**(**__thiscall**) を使用すると、クラスへのポインターが最初のパラメーターとして渡されます。
  • # C++ 名マングリング

    C++ コンパイラは、エクスポートされた関数の名前 (引数の型など) に追加情報をエンコードして、さまざまな引数を使用したオーバーロードを可能にします。このプロセスは名前マングリングと呼ばれます。 int add(int a, int b) の名前のように、これにより、C# での関数のインポート (および一般的に他の言語との相互運用) で問題が発生します。 関数はもはや add ではありません 、 ?add@@YAHHH@Z の場合があります 、 _add@8 コンパイラと呼び出し規約によって異なります。

    名前マングリングの問題を解決するには、いくつかの方法があります:

  • extern "C"` を使用して関数をエクスポートし、C 名マングリングを使用する C 外部リンケージに切り替える:
    extern "C" __declspec(dllexport) int __stdcall add(int a, int b)
    
    
    [DllImport("myDLL.dll")]
    
    

    関数名は引き続きマングルされます (_add@8 )、しかし StdCall +extern "C" 名前マングリングは C# コンパイラによって認識されます。

  • エクスポートされた関数名を myDLL.def で指定する モジュール定義ファイル:

    EXPORTS
      add
    
    
    int __stdcall add(int a, int b)
    
    
    [DllImport("myDLL.dll")]
    
    

    関数名は純粋な add になります

  • 壊れた名前をインポートしています。マングルされた名前を表示するには、いくつかの DLL ビューアーが必要です。その後、明示的に指定できます。

    __declspec(dllexport) int __stdcall add(int a, int b)
    
    
    [DllImport("myDLL.dll", EntryPoint = "?add@@YGHHH@Z")]
    
    
  • # アンマネージ DLL の動的ロードおよびアンロード

    DllImport を使用する場合 コンパイル時に正しい dll とメソッド名を知っている必要があります。 . 実行時でより柔軟に決定したい場合 どの dll とメソッドを読み込むか、Windows API メソッド LoadLibrary() を使用できます 、 GetProcAddress()FreeLibrary() .これは、使用するライブラリが実行時の条件に依存する場合に役立ちます。

    GetProcAddress() によって返されるポインタ Marshal.GetDelegateForFunctionPointer() を使用してデリゲートにキャストできます .

    次のコード サンプルは、myDLL.dll を使用してこれを示しています。 前の例から:

    class Program
    {
        // import necessary API as shown in other examples
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr LoadLibrary(string lib);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern void FreeLibrary(IntPtr module);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr GetProcAddress(IntPtr module, string proc);
    
        // declare a delegate with the required signature
        private delegate int AddDelegate(int a, int b);
    
        private static void Main()
        {
            // load the dll
            IntPtr module = LoadLibrary("myDLL.dll");
            if (module == IntPtr.Zero) // error handling
            {
                Console.WriteLine($"Could not load library: {Marshal.GetLastWin32Error()}");
                return;
            }
    
            // get a "pointer" to the method
            IntPtr method = GetProcAddress(module, "add");
            if (method == IntPtr.Zero) // error handling
            {
                Console.WriteLine($"Could not load method: {Marshal.GetLastWin32Error()}");
                FreeLibrary(module);  // unload library
                return;
            }
                
            // convert "pointer" to delegate
            AddDelegate add = (AddDelegate)Marshal.GetDelegateForFunctionPointer(method, typeof(AddDelegate));
        
            // use function    
            int result = add(750, 300);
            
            // unload library   
            FreeLibrary(module);
        }
    }
    
    

    # Win32 エラーの処理

    相互運用メソッドを使用する場合、GetLastError を使用できます API 呼び出しに関する追加情報を取得するための API。

    DllImport 属性 SetLastError 属性

    SetLastError=true

    呼び出し先が SetLastError (Win32 API 関数) を呼び出すことを示します。

    SetLastError=false

    呼び出し先がしないことを示します SetLastError (Win32 API 関数) を呼び出すため、エラー情報は取得されません。

  • SetLastError が設定されていない場合は、false (デフォルト値) に設定されます。
  • Marshal.GetLastWin32Error メソッドを使用してエラー コードを取得できます:
  • 例:

    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern IntPtr OpenMutex(uint access, bool handle, string lpName);
    
    

    存在しないミューテックスを開こうとすると、GetLastError は ERROR_FILE_NOT_FOUND を返します。 .

    var lastErrorCode = Marshal.GetLastWin32Error();
    
    if (lastErrorCode == (uint)ERROR_FILE_NOT_FOUND)
    {
        //Deal with error         
    }
    
    

    システム エラー コードは次の場所にあります:

    https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx

    GetLastError API

    ネイティブの GetLastError があります 同様に使用できる API :

    [DllImport("coredll.dll", SetLastError=true)]
    static extern Int32 GetLastError();
    
    
    • マネージ コードから Win32 API を呼び出すときは、常に Marshal.GetLastWin32Error を使用する必要があります。 .

    理由は次のとおりです:

    エラーを設定する (SetLastError を呼び出す) Win32 呼び出しの間に、CLR は SetLastError を呼び出すことができる他の Win32 呼び出しを呼び出すことができます。 同様に、この動作はエラー値を上書きする可能性があります。このシナリオで、GetLastError を呼び出すと、 無効なエラーを取得できます。

    SetLastError =true の設定 、他の Win32 呼び出しを実行する前に、CLR がエラー コードを取得することを確認します。

    # Marshal で構造を読む

    Marshal クラスには PtrToStructure という名前の関数が含まれています 、この関数は、アンマネージ ポインターによって構造体を読み取る機能を提供します。

    PtrToStructure function には多くのオーバーロードがありますが、それらはすべて同じ意図を持っています。

    ジェネリック PtrToStructure :

    public static T PtrToStructure<T>(IntPtr ptr);
    
    

    T - 構造タイプ。

    ptr - 管理されていないメモリ ブロックへのポインタ。

    例:

    NATIVE_STRUCT result = Marshal.PtrToStructure<NATIVE_STRUCT>(ptr);       
    
    
    • ネイティブ構造の読み取り中に管理対象オブジェクトを扱う場合は、オブジェクトを固定することを忘れないでください 😃
    
    T Read<T>(byte[] buffer)
        {
            T result = default(T);
            
            var gch = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        
            try
            {
                result = Marshal.PtrToStructure<T>(gch.AddrOfPinnedObject());
            }
            finally
            {
                gch.Free();
            }
            
            return result;
        }
    
    

    # com のクラスを公開する簡単なコード

    using System;
    using System.Runtime.InteropServices;
     
    namespace ComLibrary
    {
        [ComVisible(true)]
        public interface IMainType
        {
            int GetInt();
     
            void StartTime();
     
            int StopTime();
        }
     
        [ComVisible(true)]
        [ClassInterface(ClassInterfaceType.None)]
        public class MainType : IMainType
        {
            private Stopwatch stopWatch;
     
            public int GetInt()
            {
                return 0;
            }
     
            public void StartTime()
            {
                stopWatch= new Stopwatch();
                stopWatch.Start();
            }
     
            public int StopTime()
            {
                return (int)stopWatch.ElapsedMilliseconds;
            }
        }
    }
    
    

    # ピン留めされたオブジェクト

    GC (ガベージ コレクター) は、ゴミを掃除する責任があります。

    GC 中 ガベージをクリーンアップし、ヒープの断片化を引き起こす未使用のオブジェクトをマネージ ヒープから削除します。 GC の場合 削除が完了すると、ヒープ上のオブジェクトの移動を伴うヒープ圧縮 (最適化) が実行されます。

    GC 以来 管理対象オブジェクトの参照/ポインタをネイティブ コードに渡す場合、GC は決定論的ではありません いつでも開始できます。Inerop 呼び出しの直後に発生した場合、オブジェクト (ネイティブに渡された参照) がマネージド ヒープ上で移動される可能性が非常に高くなります。その結果、マネージド サイドで無効な参照が取得されます。 .

    このシナリオでは、固定する必要があります ネイティブ コードに渡す前のオブジェクト。

    ピン留めされたオブジェクト

    ピン留めされたオブジェクトは、GC によって移動が許可されていないオブジェクトです。

    Gc 固定ハンドル

    Gc.Alloc を使用してピン オブジェクトを作成できます メソッド

    GCHandle handle = GCHandle.Alloc(yourObject, GCHandleType.Pinned); 
    
    
    • 固定された GCHandle の取得 特定のオブジェクトを GC で移動できないオブジェクトとしてマークする 、ハンドルを解放するまで

    例:

    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern void EnterCriticalSection(IntPtr ptr);
    
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern void LeaveCriticalSection(IntPtr ptr);
           
    public void EnterCriticalSection(CRITICAL_SECTION section)
    {
        try
        {
            GCHandle handle = GCHandle.Alloc(section, GCHandleType.Pinned); 
            EnterCriticalSection(handle.AddrOfPinnedObject());
            //Do Some Critical Work
            LeaveCriticalSection(handle.AddrOfPinnedObject());
        }
        finaly
        {
            handle.Free()
        }
    }
    
    

    注意事項

    • (特に大きなもの) オブジェクトをピン留めするときは、ピン留めされた GcHandle を解放してみてください ヒープの最適化を中断するため、できるだけ速く。
    • GcHandle を解放するのを忘れた場合 何もしません。安全なコード セクション (finaly など) で行う

    # コメント

    C# を使用した Win32 API の操作

    Windows は、Win32 API の形式で多くの機能を公開しています。これらの API を使用すると、Windows で直接操作を実行でき、アプリケーションのパフォーマンスが向上します。ソース ここをクリック

    Windows は幅広い API を公開しています。さまざまな API に関する情報を取得するには、pinvoke などのサイトをチェックしてください。