2

Visual Studio 2015 ソリューションに複数のプロジェクトがあります。これらのプロジェクトのいくつかは、次のような P/Invoke を行います。

 [DllImport("IpHlpApi.dll")]
        [return: MarshalAs(UnmanagedType.U4)]
        public static extern int GetIpNetTable(IntPtr pIpNetTable, [MarshalAs(UnmanagedType.U4)]
        ref int pdwSize, bool bOrder);

そこで、すべての P/Invokes を別のクラス ライブラリに移動し、単一のクラスを次のように定義しました。

namespace NativeMethods
{
    [
    SuppressUnmanagedCodeSecurityAttribute(),
    ComVisible(false)
    ]

    public static class SafeNativeMethods
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
        public static extern int GetTickCount();

        // Declare the GetIpNetTable function.
        [DllImport("IpHlpApi.dll")]
        [return: MarshalAs(UnmanagedType.U4)]
        public static extern int GetIpNetTable(IntPtr pIpNetTable, [MarshalAs(UnmanagedType.U4)]
        ref int pdwSize, bool bOrder);
    }
}

他のプロジェクトから、このコードは次のように呼び出されます。

 int result = SafeNativeMethods.GetIpNetTable(IntPtr.Zero, ref bytesNeeded, false);

すべてがエラーまたは警告なしでコンパイルされます。

コードで FxCop を実行すると、次の警告が表示されます。

警告 CA1401 P/Invoke 'SafeNativeMethods.GetIpNetTable(IntPtr, ref int, bool)' のアクセシビリティを変更して、アセンブリの外部から見えないようにします。

Ok。次のようにアクセシビリティを internal に変更します。

[DllImport("IpHlpApi.dll")]
[return: MarshalAs(UnmanagedType.U4)]
internal static extern int GetIpNetTable(IntPtr pIpNetTable, [MarshalAs(UnmanagedType.U4)]
ref int pdwSize, bool bOrder);

次のハード エラーが発生するようになりました。

エラー CS0122 'SafeNativeMethods.GetIpNetTable(IntPtr, ref int, bool)' は、保護レベルが原因でアクセスできません

では、エラーや警告なしでこれを機能させるにはどうすればよいでしょうか?

私は何時間も輪になっていたので、助けてくれてありがとう!

4

1 に答える 1

16

PInvoke メソッドは、C# コードから呼び出すのが最も楽しいものではないという声明に同意することは間違いありません。

彼らです:

  1. それほど厳密に型付けされていません - 多くの場合、IntPtrおよびByte[]パラメーターでいっぱいです。
  2. エラーが発生しやすい - 間違った長さのバッファや、構造体のサイズに初期化されていないフィールドを持つ構造体など、誤って初期化されたパラメータを渡すのは簡単です...
  3. 何か問題が発生した場合に例外をスローしないことは明らかです。戻りコードまたはMarshal.GetLastError()それを確認するのは消費者の責任です。そして多くの場合、誰かがそれを忘れてしまい、追跡が困難なバグにつながります。

これらの問題と比較すると、FxCop の警告はわずかなスタイルのチェッカーです。


それで、あなたは何ができますか?これら 3 つの問題に対処すれば、FxCop は自然に機能します。

これらは私があなたに行うことをお勧めすることです:

  1. API を直接公開しないでください。複雑な関数には重要ですが、どの関数にも適用すると、実際には主要な FxCop の問題に対処できます。

    public static class ErrorHandling
    {
        // It is private so no FxCop should trouble you
        [DllImport(DllNames.Kernel32)]
        private static extern void SetLastErrorNative(UInt32 dwErrCode);
    
        public static void SetLastError(Int32 errorCode)
        {
            SetLastErrorNative(unchecked((UInt32)errorCode));
        }
    }
    
  2. 安全なハンドルIntPtrを使用できる場合は使用しないでください。

  3. 単にラッパー メソッドから、Booleanまたは(U)Int32ラッパー メソッドから返すのではなく、ラッパー メソッド内の戻り値の型を確認し、必要に応じて例外をスローします。例外のない方法でメソッドを使用する場合は、Tryそれが例外のないメソッドであることを明確に示すようなバージョンを提供します。

    public static class Window
    {
        public class WindowHandle : SafeHandle ...
    
        [return: MarshalAs(UnmanagedType.Bool)]
        [DllImport(DllNames.User32, EntryPoint="SetForegroundWindow")]
        private static extern Boolean TrySetForegroundWindowNative(WindowHandle hWnd);
    
        // It is clear for everyone, that the return value should be checked.
        public static Boolean TrySetForegroundWindow(WindowHandle hWnd)
        {
            if (hWnd == null)
                throw new ArgumentNullException(paramName: nameof(hWnd));
    
            return TrySetForegroundWindowNative(hWnd);
        }
    
        public static void SetForegroundWindow(WindowHandle hWnd)
        {
            if (hWnd == null)
                throw new ArgumentNullException(paramName: nameof(hWnd));
    
            var isSet = TrySetForegroundWindow(hWnd);
            if (!isSet)
                throw new InvalidOperationException(
                    String.Format(
                        "Failed to set foreground window {0}", 
                        hWnd.DangerousGetHandle());
        }
    }
    
  4. で渡される通常の構造体を使用できる場合は、 IntPtrorを使用しないでくださいByte[]ref/out。当たり前だとおっしゃるかもしれませんが、強く型付けされた構造体を渡すことができる多くの場合、IntPtr代わりに使用されているのを見てきました。out公開メソッドでパラメーターを使用しないでください。ほとんどの場合、これは不要です。値を返すだけでかまいません。

    public static class SystemInformation
    {
        public struct SYSTEM_INFO { ... };
    
        [DllImport(DllNames.Kernel32, EntryPoint="GetSystemInfo")]
        private static extern GetSystemInfoNative(out SYSTEM_INFO lpSystemInfo);
    
        public static SYSTEM_INFO GetSystemInfo()
        {
            SYSTEM_INFO info;
            GetSystemInfoNative(out info);
            return info;
        }
    }
    
  5. 列挙型。WinApi は、多くの列挙値をパラメーターまたは戻り値として使用します。C スタイルの列挙型であるため、実際には単純な整数として渡されます (返されます)。しかし、C# の列挙型も実際には整数にすぎないため、基になる型を適切に設定していると仮定すると、メソッドをはるかに簡単に使用できるようになります。

  6. ビット/バイトのいじり- 値を取得したり、その正確性を確認したりするためにマスクが必要であることがわかった場合は、カスタム ラッパーを使用すると、より適切に処理できることがわかります。場合によってはFieldOffsetで処理されることもあれば、実際にちょっとした調整を行う必要があることもありますが、いずれにせよ、単純で便利なオブジェクト モデルを提供する 1 か所でのみ行われます。

    public static class KeyBoardInput
    {
        public enum VmKeyScanState : byte
        {
            SHIFT = 1,
            CTRL = 2, ...
        }           
    
        public enum VirtualKeyCode : byte
        {
            ...
        }
    
        [StructLayout(LayoutKind.Explicit)]
        public struct VmKeyScanResult
        {
            [FieldOffset(0)]
            private VirtualKeyCode _virtualKey;
            [FieldOffset(1)]
            private VmKeyScanState _scanState;
    
            public VirtualKeyCode VirtualKey
            {
                get {return this._virtualKey}
            }
            public VmKeyScanState ScanState
            {
                get {return this._scanState;}
            }
    
            public Boolean IsFailure
            {
                get
                {
                    return 
                        (this._scanState == 0xFF) &&
                        (this._virtualKey == 0xFF)
                }                   
            }
        }
    
    
        [DllImport(DllNames.User32, CharSet=CharSet.Unicode, EntryPoint="VmKeyScan")]
        private static extern VmKeyScanResult VmKeyScanNative(Char ch);
    
        public static VmKeyScanResult TryVmKeyScan(Char ch)
        {
            return VmKeyScanNative(ch);
        }
    
        public static VmKeyScanResult VmKeyScan(Char ch)
        {
            var result = VmKeyScanNative(ch);   
            if (result.IsFailure)
                throw new InvalidOperationException(
                    String.Format(
                        "Failed to VmKeyScan the '{0}' char",
                        ch));
            return result;
        }
    }
    

PS:正しい関数シグネチャ (ビット数やその他の問題)、型のマーシャリング、レイアウト属性、および文字セットを忘れないでください (また、使用することを忘れないことDllImport(... SetLastError = true)最も重要です)。http://www.pinvoke.net/が役立つことがよくありますが、使用するのに最適な署名が常に提供されるとは限りません。

PS1:そして、1 つのクラスに整理するのNativeMethodsではなく、まったく異なるメソッドの 1 つの巨大な管理不能な山になるので、それらを個別のクラスにグループ化することをお勧めします (実際にはpartial、機能領域ごとに 1 つのルート クラスとネストされたクラスを使用します)。 - 少し面倒なタイピングですが、コンテキストとインテリセンスがはるかに優れています)。クラス名については、MSDN が API 関数をグループ化するために使用するのと同じ分類を使用するだけです。GetSystemInfoと同様に、「システム情報関数」です。


したがって、これらのアドバイスをすべて適用すると、不必要な複雑さとエラーが発生しやすい構造をすべて隠した、堅牢で使いやすいネイティブ ラッパー ライブラリを作成できるようになります。

于 2016-03-05T20:35:49.633 に答える