【.NET】プラットフォーム呼び出し(P/Invoke)、DllImport関連の説明と注意事項、

静的外部メソッドにDllImportこの機能をマークすると、C# を使用してアンマネージ ダイナミック リンク ライブラリの関数を呼び出すことができます。このメソッドはプラットフォーム呼び出し (Platform Invoke、または P/Invoke) と呼ばれます。

基本的な使い方:

以下では、コンソール ウィンドウのハンドルを取得する関数を使用した最も基本的なプラットフォーム呼び出しを示します。

using System.Runtime.InteropServices;

[DllImport("kernel32.dll")]                # DllImport 特性与函数所在链接库
static extern IntPtr GetConsoleWindow();   # 方法基本声明 (静态外部方法)

IntPtr currentConsoleWindow = GetConsoleWindow();
Console.WriteLine($"当前控制台的窗口句柄是: 0x{
      
      currentConsoleWindow:X}");

DllImport 属性メンバーフィールド

1.EntryPoint(エントリーポイント)

呼び出す DLL エントリ ポイントの名前または序数を示します。

エントリ ポイント関数名を指定することもできますが、エントリ ポイントは #1 のように先頭に # 記号が付いたエントリ ポイント シーケンス番号によっても識別できます。このフィールドを省略した場合、CLR は現在のDllImport属性でマークされたメソッド名をエントリ ポイント名として使用します。

2. CharSet(文字セット)

メソッドを呼び出すときに使用される文字セットを示します。CLR は、指定された文字セットに従って、受信文字列パラメーターを対応する関数にマーシャリングします。

このフィールドを enum のメンバーとともにCharSet使用すると、文字列パラメーターのマーシャリング動作を指定し、呼び出すエントリ ポイント名 (指定された正確な名前、または 'A' または 'W' の接尾辞が付いた名前) を指定できます。デフォルトの enum メンバーC# および Visual Basic の場合は ですCharSet.Ansi。C++ のデフォルトの列挙メンバーは ですCharSet.None。これは と同等ですCharSet.Ansi。Visual Basic では、Declareステートメントを使用してCharSetフィールドを指定します。

たとえば、エントリ ポイント名が GetWindowText で、CharSet が Unicode の場合、実際に呼び出されるメソッドは GetWindowTextW です (
GetWindowText 関数は存在しませんが、CLR が GetWindowTextW 関数を検索するため、この関数が呼び出されます)。

3. SetLastError (最後のエラーを設定)

呼び出し元が戻る前にエラーを設定するかどうかを指定します (WindowsSetLastErrorまたは他のプラットフォームで呼び出されますerrno)。

このフィールドが に設定されている場合true、ランタイムmarshaler1 はGetLastError、他の API 呼び出しによって上書きされないように、またはを呼び出しerrnoて戻り値をキャッシュします。これは、.NET 6.0 以降、またはGetLastPInvokeError.NET 5 以前のバージョンおよび .NET Framework で呼び出すことができます。を呼び出してGetLastWin32Errorエラーコードを受信します。

.NET では、このフィールドが に設定されている場合true、呼び出し先を呼び出す前にエラー メッセージがクリア (0 に設定) されます。しかし、.NET Framework では、エラー メッセージはクリアされません。これは、 および によって返されることを意味しますGetLastPInvokeError .NET は、属性が に設定されたGetLastWin32Error最後のプラットフォーム呼び出しからのエラー メッセージのみを表します。.NET Framework では、このエラー メッセージは、あるプラットフォーム呼び出しから次のプラットフォーム呼び出しまで保持されます。DllImportAttribute.SetLastErrortrueDlllImport

4. ExtractSpelling (正確なスペル)

CLR が CharSet フィールドの値に基づいてアンマネージ DLL 内のエントリ ポイント名を検索するか、指定されたエントリ ポイント名を直接使用するかを制御します。

値が false で、指定されたエントリ ポイント名の関数が見つからない場合、CLR はCharSetフィールドの値に従ってエントリ ポイント名を検索します。このとき、 が の場合、CharSetエントリCharSet.Ansiポイントの呼び出しを試みます。末尾に文字 'A' が追加された名前、 の場合CharSetCharSet.Unicode文字 'W' が追加されたエントリ ポイント名の呼び出しが試行されます。通常、マネージド コンパイラはこのフィールドの値を設定します。

VB および C# および C++ では、ExactSpelling の値は CharSet の値に応じて異なる動作をします。

言語 ANSI ユニコード 自動
ビジュアルベーシック 正確なスペル := True 正確なスペル := True 正確なスペル := False
C# 正確なスペル = false 正確なスペル = false 正確なスペル = false
C++ 正確なスペル = false 正確なスペル = false 正確なスペル = false

つまり、VB では、CharSet を正確な文字セット (ANSI または Unicode) に設定すると、ExactSpelling が True になり、CLR は文字セットに一致する名前を検索せず、一致した名前を直接使用します。 C++ では、正確な文字セットを指定した場合でも、指定したエントリ ポイント名が見つからない場合、CLR は文字セットに一致する関数を検索しようとします。

5. CallingConvertion (呼び出し規約)

エントリポイント2の呼び出し規則を指定します。

You can set it as CallingConvertiona member of the enum.CallingConvertionこのフィールドのデフォルト値は Convention でありWinapi、Windows プラットフォームではデフォルトの Convention に設定されStdCallCdecl他のすべてのプラットフォームでは Convention に設定されます。

6. BestFitMapping(ベストフィットマッピング)

Unicode 文字を ANSI 文字に変換するときの最適なマッピング動作を有効または無効にします。

7. 署名を保存する

HRESULT戻り値を持つアンマネージ メソッドが直接変換されるか、またはHRESULT戻り値が自動的に例外に変換されるかを示します。

8. ThrowOnUnmappableChar

ANSI "?" 文字に変換されたマップ不可能な Unicode 文字が見つかった場合の例外の発生を有効または無効にします。


文字列の扱い

1. 文字列を渡しますが、変更しないでください。

たとえば、ダイナミック リンク ライブラリには次の関数があります。

#include <windows.h>
extern "C" __declspec(dllexport) void PrintW(wchar_t* lpstr)
{
    
    
    wprintf(L"%s\n", lpstr);
}

ワイド文字列ポインタを受け取り、それを標準出力ストリームに出力します。一般的に使用される文字列値の転送メソッドを次に示します。

  1. 文字列を直接渡す
    [DllImport("test.dll", EntryPoint = "PrintW", CharSet = CharSet.Unicode)]
    extern static void Print1(string str);
    
  2. StringBuilder を渡す
    [DllImport("test.dll", EntryPoint = "PrintW", CharSet = CharSet.Unicode)]
    extern static void Print1(StringBuilder str);
    
  3. 文字配列を渡す
    [DllImport("test.dll", EntryPoint = "PrintW", CharSet = CharSet.Unicode)]
    extern static void Print1(char[] str);
    
  4. 文字ポインタを渡す
    [DllImport("test.dll", EntryPoint = "PrintW", CharSet = CharSet.Unicode)]
    extern static void Print1(char* str);
    

2. 文字列を渡して変更を加える

function 内の文字列に変更がある場合は、渡される もstring変更される可能性があることに注意する必要があります。たとえば、次のような関数があります。

#define _CRT_SECURE_NO_WARNINGS
#include <stdlib.h>
#include <windows.h>

extern "C" __declspec(dllexport) void FuckYouWorldW(wchar_t* lpstr)
{
    
    
    wsprintf(lpstr, L"Fuck you world, 撒比世界");
}

extern "C" __declspec(dllexport) void FuckYouWorldA(char* lpstr)
{
    
    
    sprintf(lpstr, "Fuck you world, 撒比世界");
}
  1. Unicode に対応した関数を使用する場合、文字列は次のように変更されます。
    [DllImport("test.dll", EntryPoint = "FuckYouWorldW",CharSet = CharSet.Unicode)]
    extern static void FuckYouWorldW(string str);
    
    string buf = new string('\0', 32);   // 声明一个长度为 32 的字符串
    FuckYouWorldW(buf);
    
    Console.WriteLine(buf);              // 你会得到一个 "Fuck you world, 撒比世界"
                                         // 但是注意, buf 字符串后面还是有很多 \0 的, 只是没打印出来
    
  2. ただし、ANSI に対応した関数を使用すると、入力文字列が変更されても反映されません。
    [DllImport("test.dll", EntryPoint = "FuckYouWorldA",CharSet = CharSet.Ansi)]
    extern static void FuckYouWorldA(string str);
    
    string buf = new string('\0', 32);   // 声明一个长度为 32 的字符串
    
    // 在传入 buf 时, Marshaler 会帮我们将字符串转为 ANSI 字符串, 并传入指针
    FuckYouWorldA(buf);
    // 这导致, 虽然函数变更了指针指向的字符串值, 但没有对我们原来的字符串有任何更改
    
    Console.WriteLine(buf);              // 你什么也看不到
    
  3. これを使用するとStringBuilder、Unicode メソッドでも ANSI メソッドでも、関数が文字列に変更されたことがわかります。Marshal は、Unicode 文字列と ANSI 文字列の間の変換を処理し、文字列変更の結果を処理するのに役立ちます。とにかく変更された文字列を確認できます。
  4. を使用する場合、使用する文字セットに応じて と同じようにchar[]動作します。string
  5. を使用するとchar*、元のポインターが直接渡されますが、ANSI 関数を使用している場合は、取得した変更された文字列は正常に表示されません。これは、ANSI 形式であり、C# は Unicode に従っているためです。文字列をデコードします。

参考:マネージ コードとアンマネージ コードの間のマーシャリング
原文は見つからないようです。CSDN の再版のみです。

3. 文字列を渡すにはどのメソッドを使用すればよいですか?

文字列を関数に渡す必要があり、その文字列が関数によって変更されない場合は、それを直接渡すことをお勧めします。これが最もstring便利です。GetWindowTextWthis に似た関数を呼び出す場合StringBuilderは、 を使用し、事前に容量を設定し、関数を呼び出すときにこの容量を渡すことをお勧めします。このようにして、関数は正しく処理でき、メモリ アクセスに関連する例外は発生しません。

もちろん、必要に応じて他の型を渡すことも可能です。たとえば、多くのアンマネージ型を操作し、コードがポインターで囲まれている場合、関数をポインターとして宣言して直接呼び出すことができます。 。

4. 文字列コピーについて

アンマネージ ダイナミック リンク ライブラリを呼び出すと、CLR がstring使用するコピーを作成すると考える人もいますが、これは完全に真実ではありません。

Unicode バージョンの関数 ( CharSetUnicode に設定) を使用している場合、呼び出し時にコピーはまったく行われませんが、ソース文字列が直接使用されます。そのため、この場合、関数は文字列処理を実行できます。そして結果を正しく取得します

ANSI バージョンの関数 (set CharSet) を使用している場合、呼び出し時に CLR が .NET 文字列をアンマネージ ANSI 文字列に変換し、ポインターを渡すのに役立ちます。明らかに、このプロセスでは文字列のコピーが発生します。

5. どの文字セット関数を使用すればよいですか?

少なくとも Windows プラットフォームでは、Unicode 文字セットの関数、つまりサフィックス W の付いた関数を使用することをお勧めします。ワイド文字列 (ワイド文字列) は、上位バージョンのWindows プラットフォームでは、C# の文字列を何も操作せずに直接渡すことができるため、パフォーマンスが向上します。

参考: Wide String vs String 、Windows C++ のパフォーマンスに影響しますか
ここでは、Windows カーネルがワイド文字を使用していることを示しています

知らせ:

文字列のアドレスを直接取得して渡そうとしないでください。取得したアドレスは直接使用することはできませんが、wchar_t*スタック上の変数のアドレスを使用する必要があります。指针取值文字列のアドレス値を取得する操作を実行する必要があります。スタックとstringヒープに保存されるストレージ コンテンツには「型ヘッダー」も含まれているため、少なくともこのオフセットを計算する必要があります。全体として、これは非常に複雑なので、実行しないでください。


ポインタの扱い

ダイナミック リンク ライブラリの関数がintポインターを渡し、そのポインターが戻り値として指すものを変更する場合はint、C#outキーワードを直接使用してこのパラメーターを宣言できます。

extern "C" __declspec(dllexport) void add(int a, int b, int* result)
{
    
    
    *result = a + b;
}
[DllImport("test.dll", EntryPoint = "add")]
extern static void add(int a, int b, out int result);

同様に、関数がポインターを渡す場合、ポインターが指す値をint読み取り、ポインターが指す値も変更します。その場合、C#キーワードを使用してこのパラメーターを宣言できます。intref

extern "C" __declspec(dllexport) void add114514(int* val)
{
    
    
    *val += 114514;
}
[DllImport("test.dll", EntryPoint = "add")]
extern static void add114514(ref int val);

ただし、注意してください。使用できませんout string。このスズメの餌は許可されていません。文字列を送信したい場合は、次を使用する必要があります。StringBuilder

構造物の取扱い

実際、アンマネージ ダイナミック リンク ライブラリ関数呼び出しを行うとき、CLR は使用するすべての値をアンマネージ形式に変換し、それらをマーシャリングします。たとえば、C# では 1 バイトを占めますが、boolWinAPIboolではCLR は呼び出しパラメータと戻り値の変換に参加します。

たとえば、次のような C++ 関数があります。

struct StringWrapper
{
    
    
    wchar_t* StrPtr;
    int Length;
};

extern "C" __declspec(dllexport) void print(StringWrapper* str)
{
    
    
    for (int i = 0; i < str->Length; i++) {
    
    
        putwchar(str->StrPtr[i]);
    }

    putwchar(L'\n');
}

C# を書くときは、次のように書くことができます。

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]   // 注意, 这里需要写明字符集
struct StringWrapper
{
    
    
    string _value;       // 字符串会自动被转换为正确的 wchar_t 指针以供使用
    int    _length;      // 整数的话, 则是原封不动的传过去

    public StringWrapper()
    {
    
    
        _value = string.Empty;
        _length = 0;
    }

    public StringWrapper(string value)
    {
    
    
        _value = value;
        _length = value.Length;
    }
}

[DllImport("test.dll", EntryPoint = "print", CharSet = CharSet.Unicode)]
private extern static void PrintString([In, Optional] ref StringWrapper str);

ただし、パラメーターが構造体へのポインターであり、その構造体を CLR によってマーシャリングする場合は、そこでメソッド パラメーターをDllImport構造体への直接ポインターとしてではなく、reforパラメーターとして宣言する必要があることに注意してください。out

また、構造体がマーシャリングされるとき、内部の文字列は構造体StructLayoutで指定されたものに従ってCharSetマーシャリングされることが非常に重要です。デフォルトは ANSI であるため、 としてマーシャリングされます。char*を使用したい場合はwchar_t*、次のように指定する必要があります。CharSetとしてCharSet.Unicode

ポインタを直接渡す場合でも構造体を使用できるようにするには、構造体のメモリ レイアウトをアンマネージ構造体のメモリ レイアウトと一致させる必要があります。参考: [.NET] 構造体レイアウトの詳細構造体と整合させるための具体的
方法メモリ


エントリポイントルックアップの近似ロジック

疑似コードは次のとおりです。

如果存在 Xxx 函数:
    调用 Xxx 函数;
    返回;
否则:
    如果 ExactSpelling:   // 精确拼写
        抛异常("入口点找不到");
    否则:
        如果 CharSet 是 Auto:
            如果是高系统版本:
                确认 CharSet 为 Unicode;
            否则:
                确认 CharSet 为 ANSI;
                
        如果 CharSet 是 Unicode:
            如果存在 XxxW 函数:   // 就是后面加了个 W 后缀
                调用 XxxW 函数;
                返回;
            否则:
                抛异常("入口点找不到");
        否则如果 CharSet 是 ANSI:
            如果存在 XxxA 函数:   // 就是后面加了个 A 后缀
                调用 XxxA 函数;
                返回;
            否则:
                抛异常("入口点找不到");
        否则:
            抛异常("入口点找不到");

注釈

  1. マーシャラー:アンマネージ ダイナミック リンク ライブラリを操作するときに、マネージド型をアンマネージド型に、またはアンマネージド型からマネージド型に変換する役割を担う CLR。
  2. 呼び出し規約:呼び出し規約 (Calling Convention) は、サブ関数 (プロシージャ) がパラメーターをどのように取得し、どのように返すかを規定するスキームで、通常はアーキテクチャーやコンパイラーなどに関連します。

この記事は CSDN の内容に応じて随時更新されます。

おすすめ

転載: blog.csdn.net/m0_46555380/article/details/128571174