C# P/Invoke:可变参数委托回调

Posted

技术标签:

【中文标题】C# P/Invoke:可变参数委托回调【英文标题】:C# P/Invoke: Varargs delegate callback 【发布时间】:2011-07-14 14:13:39 【问题描述】:

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


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

这个定义有效,但我得到了像“格式 %s 用 size=%d 和 score=%d 探测的字符串”。 我尝试添加 __arglist 关键字,但代表不允许使用它。

嗯,这对我来说并不那么戏剧化,但我只是好奇是否可以在 C# 中获取可变参数。 我知道我可以使用 c++ 进行互操作。 那么:有没有办法在合理的努力下纯粹在 C# 中做到这一点?

编辑:对于那些仍然没有得到它的人:我不导入一个可变参数函数但将它作为回调导出,它然后称为我的本机代码。我一次只能指定一个 -> 只能指定一个重载并且 __arglist 不起作用。

【问题讨论】:

我在发布***.com/questions/10361369/… 之前遇到了这个问题。我认为它是相似的,并且我找到了一个适合我的实现。 HTH。 This 可能有用。 【参考方案1】:

不,没有办法做到这一点。不可能的原因是变量参数列表在 C 中的工作方式。

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

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

为了让 CLR 处理这个问题,它必须能够知道正确的约定来确定它需要获取多少参数。您不能编写自己的处理程序,因为它需要访问您无权访问的非托管堆栈。因此,您无法从 C# 中执行此操作。

有关这方面的更多信息,您需要阅读 C 调用约定。

【讨论】:

【参考方案2】:

这是处理它的方法。它可能适用于您的情况,也可能不适用,具体取决于您的回调参数是否与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 args 指针声明您的委托:

[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();

    ...

【讨论】:

太棒了!不知道你是怎么想出来的,但它有效! 谢谢,它真的帮助我走上正轨!但是我需要一种让它跨平台的方法......所以我自己做了!这是我的工作,请随时贡献:github.com/jeremyVignelles/va-list-interop-demo。【参考方案3】:

其实在 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
    
    

【讨论】:

【参考方案4】:

以下文章涵盖了稍微不同的场景,可能会有所帮助:

How to P/Invoke VarArgs (variable arguments) in C#

【讨论】:

这有两个问题:1.) 我正在导出函数指针/回调,而不是通过 DllImport 导入它,我已经尝试过 __arglist:它不适用于委托! 2.) 覆盖所有可能性不起作用,因为我一次只能注册一个回调。 看来这只是我的另一个问题,没有人可以回答^^【参考方案5】:

您需要 P/invoke 编组器的支持才能实现这一点。编组器不提供此类支持。因此无法做到。

【讨论】:

【参考方案6】:

我不同意@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));

在委托和回调的情况下:

    定义以下结构:

    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();
                  
              
           
        
     
    

    定义你的委托

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate int PrintFormatDelegate([MarshalAs(UnmanagedType.LPStr)] string format, VariableArgumentBuffer arglist);
    

    打电话

    callback("Hello %d %s", VariableArgumentBuffer.Create(2019, Marshal.StringToHGlobalAnsi("Merry christmas")));
    

    实施

    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() 读取它。如果您不理解这部分,请在 cmets 中告诉我,以便我为您提供更多信息。

【讨论】:

以上是关于C# P/Invoke:可变参数委托回调的主要内容,如果未能解决你的问题,请参考以下文章

带有可变参数的回调函数 tkinter 按钮

C# 是不是支持可变数量的参数,以及如何支持?

c#可变参数params的介绍

[c#]params可变参数

13.3.5 接口和委托的泛型可变性限制和说明

C#方法:个数可变的参数 params -0027