C# P/Invoke: Varargs delegate callback
Asked Answered
C

6

11

I was just trying to do some managed/unmanaged interop. To get extended error information I decided to register a log callback offered by the dll:


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

This definition works, but i get strings like "Format %s probed with size=%d and score=%d". I tryed to add the __arglist keyword, but it is not allowed for delegates.

Well, it is not so dramatic for me, but I am just curious wether one could get the varargs parameters in C#. I know that I could use c++ for interop. So: Is there a way to do this purely in C#, with reasonable efford?

EDIT: For those who still did not get it: I am NOT IMPORTING a varargs function BUT EXPORTING it as a callback, which is then called my native code. I can specify only one at a time -> only one overload possible and __arglist does NOT work.

Callen answered 14/7, 2011 at 14:13 Comment(2)
I ran into this question before posting #10361869. I think it's similar, and I have found an implementation that works for me. HTH.Babysit
This might be useful.Rosenstein
O
4

No there is no possible way to do it. The reason it is impossible is because of the way variable argument lists work in C.

In C variable arguments are just pushed as extra parameters on to the stack (the unmanaged stack in our case). C does not anywhere record the number of parameters on the stack, the called function takes its last formal parameter (the last argument before the varargs) gets its location and starts popping arguments off the stack.

The way that it knows how many variables to pop off the stack is completely convention based - some other parameter indicates how many variable arguments are sitting on the stack. For printf it does that by parsing the format string and popping off the stack every time it sees a format code. It seems like your callback is similar.

For the CLR to handle that, it would have to be able to know the correct convention to determine how many arguments it needed to pickup. You can't write your own handler, because it would require access to the unmanaged stack which you don't have access to. So there is no way you can do this from C#.

For more information on this you need to read up on C calling conventions.

Optometry answered 14/7, 2011 at 21:37 Comment(0)
A
5

Here is the way to deal with it. It may or may not be applicable to your case, depending on whether your callback arguments are meant to be used with printf family of functions.

First, import vsprintf and _vscprintf from msvcrt:

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

Next, declare your delegate with IntPtr args pointer:

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

Now when your delegate is invoked via native code, simply use vsprintf to format the message correctly:

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

    ...
}
Alegar answered 4/6, 2016 at 11:11 Comment(2)
Amazing! Not sure how you figured it out, but it works!Doleful
Thanks, it really helped me to get on the tracks! However I needed a way to make it cross platform... So I did it myself! Here is my work, feel free to contribute : github.com/jeremyVignelles/va-list-interop-demo .Christology
O
4

No there is no possible way to do it. The reason it is impossible is because of the way variable argument lists work in C.

In C variable arguments are just pushed as extra parameters on to the stack (the unmanaged stack in our case). C does not anywhere record the number of parameters on the stack, the called function takes its last formal parameter (the last argument before the varargs) gets its location and starts popping arguments off the stack.

The way that it knows how many variables to pop off the stack is completely convention based - some other parameter indicates how many variable arguments are sitting on the stack. For printf it does that by parsing the format string and popping off the stack every time it sees a format code. It seems like your callback is similar.

For the CLR to handle that, it would have to be able to know the correct convention to determine how many arguments it needed to pickup. You can't write your own handler, because it would require access to the unmanaged stack which you don't have access to. So there is no way you can do this from C#.

For more information on this you need to read up on C calling conventions.

Optometry answered 14/7, 2011 at 21:37 Comment(0)
M
3

I disagree with @shf301, It's possible.

You can use __arglist in case of PInvoke, like this:

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

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

In the case of delegates and callbacks:

  1. Define the following struct:

    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. Define your delegate

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

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

    public static int MyPrintFormat(string format, VariableArgumentBuffer arglist)
    {
        var stream = new UnmanagedMemoryStream(arglist.Buffer, VariableArgumentBuffer.BufferLength);
        var binary = new BinaryReader(stream);
    
        ....
    }
    
    • You have to parse format to know what is pushed into the stack, and then read arguments using binary. For example, if you know an int32 is pushed, you can read it using binary.ReadInt32(). If you don't understand this part, please tell me in comments so I can provide you more info.
Mcintire answered 27/12, 2018 at 7:22 Comment(0)
R
1

Actually it is possible in 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
    {
    }
}
Rosenstein answered 10/11, 2013 at 21:6 Comment(0)
P
0

The following article covers a slightly different scenario and may be helpful:

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

Picco answered 14/7, 2011 at 14:17 Comment(2)
There are 2 problems with this: 1.) Am EXPORTING a function pointer/callback rather that IMPORTING it via DllImport and I already tryed __arglist: It does not work with delegates! 2.) Overring all possibilitys does not work, because I can register only one callback at a time.Callen
Looks like this just another one of my questions, which noone can answer^^Callen
G
0

You'd need support from the P/invoke marshaller for this to be possible. The marshaller does not provide such support. Thus it cannot be done.

Gripping answered 14/7, 2011 at 22:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.