c# P/Invoke: Varargs委托回调

本文关键字:回调 Varargs Invoke | 更新日期: 2023-09-27 18:01:59

我只是想做一些托管/非托管互操作。为了获得扩展的错误信息,我决定注册一个由dll提供的日志回调:


[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void LogCallback(void* arg1,int level,byte* fmt);

这个定义是有效的,但是我得到像"Format %s probe with size=%d and score=%d"这样的字符串。我试图添加__arglist关键字,但它不允许委托。

嗯,这对我来说不是那么戏剧性,但我只是好奇是否可以在c#中获得变量参数。我知道我可以使用c++进行互操作。那么:是否有一种方法可以完全在c#中做到这一点,并付出合理的努力?

EDIT:对于那些仍然不明白的人:我不是导入一个变量函数,而是将其导出为回调,然后称为我的本机代码。我一次只能指定一个->只能有一个重载,__arglist不起作用。

c# P/Invoke: Varargs委托回调

不,没有办法。不可能的原因是变量参数列表在c中的工作方式。

在C中,变量参数只是作为额外的参数被压入堆栈(在我们的例子中是非托管堆栈)。C在任何地方都不会记录堆栈上的参数数量,被调用的函数使用其最后一个形式参数(varargs之前的最后一个参数)获取其位置并开始从堆栈中弹出参数。

它知道有多少变量从堆栈中弹出的方式完全是基于约定的——一些其他参数指示堆栈中有多少变量参数。对于printf,它通过解析格式字符串并在每次看到格式代码时弹出堆栈来完成此操作。似乎你的回调是类似的。

要让CLR处理这个问题,它必须能够知道正确的约定,以确定需要获取多少个参数。您不能编写自己的处理程序,因为它需要访问您无法访问的非托管堆栈。因此,你不可能在c#中做到这一点。

关于这方面的更多信息,你需要阅读C调用约定

处理方法如下:它可能适用于你的情况,也可能不适用,这取决于你的回调参数是否打算与printf系列函数一起使用。

首先,从msvcrt中导入vsprintf_vscprintf:

[DllImport("msvcrt.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
public static extern int vsprintf(
    StringBuilder buffer,
    string format,
    IntPtr args);
[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int _vscprintf(
    string format,
       IntPtr ptr);

接下来,用IntPtr参数指针声明委托:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void LogCallback(
    void* arg1,
    int level, 
    [In][MarshalAs(UnmanagedType.LPStr)] string fmt,
    IntPtr args);

现在,当您的委托通过本机代码调用时,只需使用vsprintf来正确格式化消息:

private void LogCallback(void* data, int level, string fmt, IntPtr args)
{
    var sb = new StringBuilder(_vscprintf(fmt, args) + 1);
    vsprintf(sb, fmt, args);
    //here formattedMessage has the value your are looking for
    var formattedMessage = sb.ToString();
    ...
}

我不同意@shf301,这是可能的。

可以在PInvoke的情况下使用__arglist,如下所示:

[DllImport("msvcrt", CallingConvention = CallingConvention.Cdecl, EntryPoint = "printf")]
public static extern int PrintFormat([MarshalAs(UnmanagedType.LPStr)] string format, __arglist);

呼叫:PrintFormat("Hello %d", __arglist(2019));

对于委托和回调:

  1. 定义以下结构体:

    public unsafe struct VariableArgumentBuffer
    {
        public const int BufferLength = 64; // you can increase it if needed
        public fixed byte Buffer[BufferLength];
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static VariableArgumentBuffer Create(params object[] args)
        {
            VariableArgumentBuffer buffer = new VariableArgumentBuffer();
            Write(ref buffer, args);
            return buffer;
        }
        public static void Write(ref VariableArgumentBuffer buffer, params object[] args)
        {
            if (args == null)
            return;
            fixed (byte* ptr = buffer.Buffer)
            {
                int offset = 0;
                for (int i = 0; i < args.Length; i++)
                {
                    var arg = args[i];
                    if (offset + Marshal.SizeOf(arg) > BufferLength)
                        throw new ArgumentOutOfRangeException();
                    switch (arg)
                    {
                    case byte value:
                         *(ptr + offset++) = value;
                         break;
                    case short value:
                         *(short*)(ptr + offset) = value;
                         offset += sizeof(short);
                         break;
                    case int value:
                        *(int*)(ptr + offset) = value;
                        offset += sizeof(int);
                        break;
                    case long value:
                        *(long*)(ptr + offset) = value;
                        offset += sizeof(long);
                        break;
                    case IntPtr value:
                        *(IntPtr*)(ptr + offset) = value;
                        offset += IntPtr.Size;
                        break;
                    default: // TODO: Add more types
                        throw new NotImplementedException();
                  }
              }
           }
        }
     }
    
  2. 定义委托

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate int PrintFormatDelegate([MarshalAs(UnmanagedType.LPStr)] string format, VariableArgumentBuffer arglist);
    
  3. 为调用
  4. callback("Hello %d %s", VariableArgumentBuffer.Create(2019, Marshal.StringToHGlobalAnsi("Merry christmas")));
    
  5. 实施
  6. public static int MyPrintFormat(string format, VariableArgumentBuffer arglist)
    {
        var stream = new UnmanagedMemoryStream(arglist.Buffer, VariableArgumentBuffer.BufferLength);
        var binary = new BinaryReader(stream);
        ....
    }
    
    • 你必须解析format来知道什么被推入堆栈,然后使用binary读取参数。例如,如果您知道int32类型的数据被压入,那么您可以使用binary.ReadInt32()读取它。如果你不明白这部分,请在评论中告诉我,这样我可以为你提供更多的信息。

实际上在CIL中是可能的:

.class public auto ansi sealed MSIL.TestDelegate
       extends [mscorlib]System.MulticastDelegate
{
    .method public hidebysig specialname rtspecialname 
            instance void  .ctor(object 'object',
                                 native int 'method') runtime managed
    {
    }
    .method public hidebysig newslot virtual 
            instance vararg void  Invoke() runtime managed
    {
    }
}

下面的文章介绍了一个稍微不同的场景,可能会有所帮助:

如何在c#中调用VarArgs(变量参数)

您需要p/invoke编组器的支持才能实现这一点。编组程序不提供这种支持。