在C#和.NET中进行P/Invoke时的特征检测

12
我正在尝试寻找一种好的方法,在P/Invoking之前检测一个功能是否存在。例如,调用本地StrCmpLogicalW函数:
[SuppressUnmanagedCodeSecurity]
internal static class SafeNativeMethods
{
   [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
   public static extern int StrCmpLogicalW(string psz1, string psz2);
}

有些系统没有这个功能,会导致崩溃。

我不想进行版本检查, 因为这是不好的做法,有时候也可能是错误的(例如当功能被反向移植或者该功能可以被卸载时)。

正确的方法是检查 shlwapi.dll 的导出是否存在:

private static _StrCmpLogicalW: function(String psz1, String psz2): Integer;
private Boolean _StrCmpLogicalWInitialized;

public int StrCmpLogicalW(String psz1, psz2)
{
    if (!_StrCmpLogialInitialized)
    {
        _StrCmpLogicalW = GetProcedure("shlwapi.dll", "StrCmpLogicalW");
        _StrCmpLogicalWInitialized = true;
    }

    if (_StrCmpLogicalW)
       return _StrCmpLogicalW(psz1, psz2)
    else
       return String.Compare(psz1, psz2, StringComparison.CurrentCultureIgnoreCase);
}

问题在于,C#不支持函数指针,即:

_StrCmpLogicalW = GetProcedure("shlwapi.dll", "StrCmpLogicalW");

我是一位有用的助手,可以为您翻译文本。

无法完成。

因此,我正在尝试查找替代语法以在.NET中执行相同的逻辑。到目前为止,我有以下伪代码,但我遇到了难题:

[SuppressUnmanagedCodeSecurity]
internal static class SafeNativeMethods
{
   private Boolean IsSupported = false;
   private Boolean IsInitialized = false;

   [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, Export="StrCmpLogicalW", CaseSensitivie=false, SetsLastError=true, IsNative=false, SupportsPeanutMandMs=true)]
   private static extern int UnsafeStrCmpLogicalW(string psz1, string psz2);

   public int StrCmpLogicalW(string s1, string s2)
   {
       if (!IsInitialized) 
       {
          //todo: figure out how to loadLibrary in .net
          //todo: figure out how to getProcedureAddress in .net
          IsSupported = (result from getProcedureAddress is not null);
          IsInitialized = true;
       }

       if (IsSupported) 
          return UnsafeStrCmpLogicalW(s1, s2);
       else
          return String.Compare(s1, s2, StringComparison.CurrentCultureIgnoreCase);
   }
}

我需要一些帮助。


另一个我想要检测是否存在的导出项示例:

  • dwmapi.dll::DwmIsCompositionEnabled
  • dwmapi.dll::DwmExtendFrameIntoClientArea
  • dwmapi.dll::DwmGetColorizationColor
  • dwmapi.dll::DwmGetColorizationParameters(未记录1,尚未按名称导出,序数为127)
  • dwmapi.dll::127(未记录1,DwmGetColorizationParameters)

1自Windows 7 SP1以来

在.NET中,必须已经存在一种设计模式来检查操作系统功能的存在。有人能指向一个.NET中执行功能检测的首选方法的示例吗?


.NET Framework 源代码中的设计模式是检查操作系统版本号,但要像 Larry Osterman 在他的博客文章中得出结论那样智能地进行检查。我同意 Johann 的解决方案可能更好,但我也是 Win32 的人。LoadLibraryGetProcAddress 对我来说很自然。当我写 .NET 代码时,我大部分时间都在编写 P/Invoke 定义。我不确定这是否真的是一件好事。 - Cody Gray
@Cody:我不确定那是否真的是一件好事 - 可能不是,呵呵。 :-) - Johann Gerell
@CodeGray 你不能仅仅依赖版本号。一个功能可能已经被反向移植到操作系统中(使版本号不正确)。用户也可能没有安装某个功能(使版本号不正确)。 - Ian Boyd
可以的,只需要你做好研究。你必须进行足够的研究,找出你想要调用的API在哪里可以找到(即,哪个DLL),即使你采用Johann的解决方案,这与确定它是否可以选择安装或是否已经被反向移植到旧操作系统并没有太大的区别。旧的操作系统是已知的量,因为它们已经在编写代码时发布,所以你可以自己检查和验证所有这些内容。你特别询问了.NET中的设计模式,这就是它的作用。 - Cody Gray
2个回答

6
你可以使用P/Invoke调用LoadLibraryW加载shlwapi.dll,然后使用P/Invoke调用GetProcAddressW查找"StrCmpLogicalW"。如果返回值为NULL,则表示不存在。
你不需要实际的GetProcAddressW返回值——只要它不是NULL,就知道可以使用所选的P/Invoke声明。
注意,GetProcAddressW还支持按序号导出的函数。
编辑:如果想要遵循某种模式,那么这个方法可能有效:
首先定义一个帮助类NativeMethodResolver,告诉你库中的方法是否存在:
public static class NativeMethodResolver
{
    public static bool MethodExists(string libraryName, string methodName)
    {
        var libraryPtr = LoadLibrary(libraryName);
        var procPtr = GetProcAddress(libraryPtr, methodName);

        return libraryPtr != UIntPtr.Zero && procPtr != UIntPtr.Zero;
    }

    [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern UIntPtr LoadLibrary(string lpFileName);

    [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
    private static extern UIntPtr GetProcAddress(UIntPtr hModule, string lpProcName);
}

上述辅助类可以被SafeNativeMethod的派生类使用,以帮助模板化一些常见的东西:
public abstract class SafeNativeMethod
{
    private readonly string libraryName;
    private readonly string methodName;
    private bool resolved;
    private bool exists;

    protected SafeNativeMethod(string libraryName, string methodName)
    {
        this.libraryName = libraryName;
        this.methodName = methodName;
    }

    protected bool CanInvoke
    {
        get
        {
            if (!this.resolved)
            {
                this.exists = Resolve();
                this.resolved = true;
            }

            return this.exists; 
        }            
    }

    private bool Resolve()
    {
        return NativeMethodResolver.MethodExists(this.libraryName, this.methodName);
    }
}

一个派生类可以定义自己的Invoke方法,然后调用基类的CanInvoke方法,以查看是否应该返回默认值(或默认实现)来代替所寻找的本地方法的返回值。从您的问题中,我将采用shlwapi.dll/StrCmpLogicalWdwmapi.dll/DwmIsCompositionEnabled作为SafeNativeMethod的示例实现:

public sealed class SafeStrCmpLogical : SafeNativeMethod
{
    public SafeStrCmpLogical()
        : base("shlwapi.dll", "StrCmpLogicalW")
    {           
    }

    public int Invoke(string psz1, string psz2)
    {
        return CanInvoke ? StrCmpLogicalW(psz1, psz2) : 0;
    }

    [DllImport("shlwapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern int StrCmpLogicalW(string psz1, string psz2);
}

public sealed class SafeDwmIsCompositionEnabled : SafeNativeMethod
{
    public SafeDwmIsCompositionEnabled()
        : base("dwmapi.dll", "DwmIsCompositionEnabled")
    {
    }

    public bool Invoke()
    {
        return CanInvoke ? DwmIsCompositionEnabled() : false;
    }

    [DllImport("dwmapi.dll", SetLastError = true, PreserveSig = false)]
    private static extern bool DwmIsCompositionEnabled();
}

这两个变量可以像这样使用:

static void Main()
{
    var StrCmpLogical = new SafeStrCmpLogical();
    var relation = StrCmpLogical.Invoke("first", "second");

    var DwmIsCompositionEnabled = new SafeDwmIsCompositionEnabled();
    var enabled = DwmIsCompositionEnabled.Invoke();
}

3
您还可以使用Marshal.GetDelegateForFunctionPointer()将返回的地址转换为委托。 - Hans
@Hans:是的,除非您正在使用.NET Compact Framework。在那里,该方法不受“Marshal”类的支持。 - Johann Gerell
请参考pinvoke.net获取LoadLibrary、GetProcAddress和FreeLibrary的语法。 - dgvid
我正在尝试制定设计模式。我应该使用多少标志(可用、已选)?在哪里进行此测试(在静态初始化期间?首次使用时?使用执行构造期间检查的类并将该类作为单例?拥有调用内部单例的静态方法?分配委托方法并检查方法变量是否为空?一个方法委托可以是null吗?等等。我正在寻找设计模式。 - Ian Boyd

1

Marshal.Prelink可以用于获取P/Invoke“未找到”异常,而不实际调用该方法。以下是如何使用它来完成您的示例:

[SuppressUnmanagedCodeSecurity]
internal static class SafeNativeMethods
{
    [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, Export="StrCmpLogicalW", CaseSensitivie=false, SetsLastError=true, IsNative=false, SupportsPeanutMandMs=true)]
    private static extern int UnsafeStrCmpLogicalW(string psz1, string psz2);

    static bool? _UnsafeStrCmpLogicalWIsSupported;
    static bool UnsafeStrCmpLogicalWIsSupported => _UnsafeStrCmpLogicalWIsSupported ?? IsSupported("UnsafeStrCmpLogicalW", out _UnsafeStrCmpLogicalWIsSupported);

    public static int StrCmpLogicalW(string s1, string s2)
    {
        if (UnsafeStrCmpLogicalWIsSupported)
            return UnsafeStrCmpLogicalW(s1, s2);
        else
            return String.Compare(s1, s2, StringComparison.CurrentCultureIgnoreCase);
    }

    static bool IsSupported(string method, out bool? field)
    {
        var result = IsSupported(method);
        field = result;
        return result;
    }
    static bool IsSupported(string method)
    {
        var methods = typeof(SafeNativeMethods).GetMethods().Where(m => m.Name == method).ToList();
        if (methods.Count == 0)
            throw new ArgumentException("Method not found: "+method, nameof(method));
        try
        {
            foreach (var m in methods)
                Marshal.Prelink(m);
            return true;
        }
        catch { return false; }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接