C#使用泛型委托获取函数指针

5
我需要动态调用dll函数。
我正在使用标准的Windows API来加载dll并获取过程地址。
在我检索到proc的IntPtr之后,我尝试将其转换为委托:
Func<int> inv = (Func<int>)Marshal.GetDelegateForFunctionPointer(proc, typeof(Func<int>));

但是它失败了,因为typeof(Func)返回泛型类型。

有没有一种干净的方法来完成我想做的事情,而不仅仅是声明成员委托并将其用作类型?

我的意思是,我试图避免以下冗余:

//I need this member only for typeof operator
private delegate int RunDll();
RunDll inv = (RunDll)Marshal.GetDelegateForFunctionPointer(proc, typeof(RunDll));
6个回答

7

如果委托类型是动态的,而你不知道参数,情况将更糟。那么你可以使用.NET方法来生成它。

public static class DelegateCreator
{
    private static readonly Func<Type[],Type> MakeNewCustomDelegate = (Func<Type[],Type>)Delegate.CreateDelegate(typeof(Func<Type[],Type>), typeof(Expression).Assembly.GetType("System.Linq.Expressions.Compiler.DelegateHelpers").GetMethod("MakeNewCustomDelegate", BindingFlags.NonPublic | BindingFlags.Static));

    public static Type NewDelegateType(Type ret, params Type[] parameters)
    {
        Type[] args = new Type[parameters.Length];
        parameters.CopyTo(args, 0);
        args[args.Length-1] = ret;
        return MakeNewCustomDelegate(args);
    }
}

 

DelegateCreator.NewDelegateType(typeof(int)) //returns non-generic variant of Func<int>

2
这正是我所需要的,一个非泛型委托类型创建器,以便我可以在其上调用Marshal.GetFunctionPointerForDelegate。 :) - Ani

2
根据文档,此API不支持泛型。需要注意的是,这并不是一个很大的损失——毕竟,您只需要指定签名一次;唯一的缺点是您无法内联指定它——使用Func<int>不需要间接引用,但(由于强制转换)在某种意义上实际上更加冗余。

顺便说一下,如果DLL和函数预先已知,则可以查看普通的DllImport - 您不需要进行手动委托包装。


1

这里最初的问题可能最好由DllImport回答,但讨论启发了NativeGenericDelegates

该项目旨在提供具有通用接口的System.Delegate对象,可与P/Invoke一起使用,并且可以使用不依赖于DynamicInvokeInvoke方法从托管和非托管代码中调用。

所有方法都明确指定了非托管函数指针的调用约定,并且可选地支持返回值和参数的自定义编组行为。

来自问题的(损坏的)代码:

Func<int> inv = (Func<int>)Marshal.GetDelegateForFunctionPointer(proc, typeof(Func<int>));

可以使用以下方式实现:

var inv = INativeFunc<int>.FromFunctionPointer(proc, CallingConvention.Cdecl); // update calling convention as needed
int result = inv.Invoke(); // invoke the native function
Func<int> invFunc = inv.ToFunc(); // convert to Func<int> for use with other APIs

更复杂的方法签名详细信息(包括编组和修改后的签名)已包含在项目存储库和源文件中。

编辑:我刚刚将NativeGenericDelegates的增量生成器的早期版本推送到存储库的incremental-generator分支。这还不是功能完整的,但不依赖于System.Reflection.Emit,这意味着它应该支持AOT平台,而以前的版本则不支持。


这是一个很棒的贡献,但也很重要的是需要注意它在底层使用了 System.Reflection.Emit - 在某些平台上无法使用。 - julx
这是一个很好的观点。同样值得注意的是,其他答案中提到的System.Linq.Expressions.Compiler.DelegateHelpers.MakeNewCustomDelegate是相同代码的简化版本,也依赖于System.Reflection.Emit,因此对于不支持动态代码的平台来说,这两个选项都不可行。 - monkey0506
是的,绝对没错。我在这里发表这个评论只是因为System.Linq.Expressions.Compiler依赖于代码生成在很大程度上是众所周知的,而在你的库的情况下可能不是那么明显(至少对我来说不是)。 - julx
在 README 的末尾有一条注释提到了 System.Reflection.Emit。我可以在文件顶部附近添加更明确的通知。如果只在尝试实现代码后才意识到这个限制,那肯定会让人感到沮丧。感谢您的反馈。 - monkey0506
@julx 如果你有兴趣,我一直在学习如何编写源代码生成器。不需要进行编组的函数应该通过具体类型参数的编译时推断在所有.NET平台上工作。 - monkey0506
显示剩余2条评论

0
首先:DelegateCreator.NewDelegateType 方法的实现不正确!
public static Type NewDelegateType(Type ret, params Type[] parameters) {
  /*
  Type[] args = new Type[parameters.Length];   // Create "args" array of same length as "parameters" (must be length + 1)
  parameters.CopyTo(args, 0);                  // Copy all values of parameters to "args" array
  args[args.Length - 1] = ret;                 // Put "ret" value to last item of "args" array
  return MakeNewCustomDelegate(args);
  */
  var offset = parameters.Length;
  Array.Resize(ref parameters, offset + 1);
  parameters[offset] = ret;
  return MakeNewCustomDelegate(parameters);
}

如果我们无法获得一个类型化的委托以进行调用,那么这个帮助程序的好处是什么?请参见下面的测试示例。

using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.InteropServices;

namespace GenericDelegates {
  static class DelegateCreator {
    public static readonly Func<Type[], Type> MakeNewCustomDelegate = (Func<Type[], Type>) Delegate.CreateDelegate(
      typeof(Func<Type[], Type>),
      typeof(Expression).Assembly.GetType("System.Linq.Expressions.Compiler.DelegateHelpers").GetMethod(
        "MakeNewCustomDelegate",
        BindingFlags.NonPublic | BindingFlags.Static
      )
    );
    public static Type NewDelegateType(Type ret, params Type[] parameters) {
      var offset = parameters.Length;
      Array.Resize(ref parameters, offset + 1);
      parameters[offset] = ret;
      return MakeNewCustomDelegate(parameters);
    }
  }
  static class Kernel {
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern IntPtr GetModuleHandle(string name);
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern IntPtr GetProcAddress(IntPtr module, string name);
  }
  static class InvokeHelper {
    public static Delegate MakeDelegate(this IntPtr address, Type ret, params Type[] parameters) {
      return Marshal.GetDelegateForFunctionPointer(address, DelegateCreator.NewDelegateType(ret, parameters));
    }
  }
  class Program {
    static void Main(string[] args) {
      var kernel = Kernel.GetModuleHandle("kernel32.dll");
      var address = Kernel.GetProcAddress(kernel, "GetModuleHandleW");
      Console.WriteLine(@"
Module base: 0x{0:X8}
Entry point: 0x{1:X8}
", (int) kernel, (int) address);
      var invoke = address.MakeDelegate(typeof(IntPtr), typeof(string));
      Console.WriteLine(@"
Untyped delegate: {0}
Cast to Invoke: {1}
", invoke, invoke as Func<string, IntPtr> == null ? "Error" : "Valid"); // invoke as Func<string, IntPtr> = NULL
      Console.ReadKey();
    }
  }
}

如果我们无法获取一个类型化的委托来调用,那么这个帮助程序有什么好处呢?你不需要一个 Func<...> 来调用委托。as 运算符并不是这样工作的(在类之间进行转换)。相反,你可以使用 Delegate.DynamicInvoke。甚至可以编写一个 Func 风格的泛型类作为你创建的 Delegate 的包装器(尽管有很多样板代码来匹配 Func 的最多16个参数)。 - monkey0506
使用这样的调用需要太多的机器资源:
  1. 需要额外的包装器来调用实际的委托;
  2. 通过将实际参数锁定在堆中作为对象传递给DynamicInvoke(即使是原始类型:byte、int、long等)。
- Mega WEB
的确,DynamicInvoke 的性能比 Invoke 差,但我回应的是 delegate 无法被调用的暗示。像 Func 这样的通用 delegate 更好,但并不在每种情况下都适用。 - monkey0506
这显然是有争议的。使用DynamicInvoke的成本可能超过了为每个所需调用Marshal.GetDelegateForFunctionPointer单独命名非泛型委托的维护成本。仅因为您没有使用案例并不意味着不存在任何用例。 - monkey0506
我发现自己再次遇到这个问题,决定进一步调查。System.Reflection.Emit 中的类使得可以为委托(从 System.MulticastDelegate 派生的类类型)提供通用接口,可与 P/Invoke 一起使用,包括指定调用约定、编组行为、不同的托管和本机签名以及调用而不使用 DynamicInvoke。有关我的实现尝试的详细信息,请参见我对此问题的答案。 - monkey0506
显示剩余2条评论

0
我正在做类似这样的事情:
var calculate = FunctionDelegateAccessor.GetMethod<Calculate>(handle, "Calculate");
Console.WriteLine("10 + 10 = " + calculate(10, 10));
var multiply = FunctionDelegateAccessor.GetMethod<Multiply>(handle, "Multiply");
Console.WriteLine("10 * 10 = " + multiply(10, 10));

public delegate int Calculate(int a, int b);
public delegate int Multiply(int a, int b);

在控制台中输出:

10 + 10 = 20 
10 * 10 = 100

Math.cpp

DLL_API int Calculate(int a, int b)
{
    return a + b;
}
DLL_API int Multiply(int a, int b)
{
    return a * b;
}

Math.h

#pragma once

#define DLL_API __declspec(dllexport)

extern "C" 
{
    DLL_API int Calculate(int a, int b);
    DLL_API int Multiply(int a, int b);
}

internal static class FunctionDelegateAccessor
{
    public static TDelegate GetMethod<TDelegate>(IntPtr moduleHandle, string name) where TDelegate 
    {
        var methodProcAddress = ExternalKernelCalls.GetProcAddress(moduleHandle, name);
        if (methodProcAddress == IntPtr.Zero)
        {
            throw new Win32Exception();
        }
        
        var @delegate = Marshal.GetDelegateForFunctionPointer<TDelegate>(methodProcAddress);
        if (@delegate == null)
        {
            throw new Exception("Function delegate not found!");
        }
        return @delegate;
    }
}
internal static class ExternalKernelCalls
{
    [DllImport(Kernel32LibraryName, SetLastError = true)]
    public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);
    private const string Kernel32LibraryName = "kernel32.dll";
}

有关 GitHub 存储库和源代码的更多信息,请单击此处


0

我刚刚也在解决完全相同的问题。这是我想出的解决方案:

private T AssignDelegate<T>(string funcName) where T : Delegate
    {
        IntPtr pFuncAddr = KernelFunctions.GetProcAddress(pDll, funcName);
        if(pFuncAddr != IntPtr.Zero)
            return (T)Marshal.GetDelegateForFunctionPointer(
                pFuncAddr, typeof(T));
        else
            return null;
    }//end AssignDelegate

目前你的回答不够清晰,请[编辑]以添加更多细节,帮助其他人理解它如何回答问题。你可以在帮助中心找到有关如何编写好答案的更多信息。 - Community

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