Marshal va_list in C# delegate
Asked Answered
E

4

6

I'm trying to make this work from c#:

C header:

typedef void (LogFunc) (const char *format, va_list args);

bool Init(uint32 version, LogFunc *log)

C# implementation:

static class NativeMethods
{
    [DllImport("My.dll", SetLastError = true)]
    internal static extern bool Init(uint version, LogFunc log);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl, SetLastError = true)]
    internal delegate void LogFunc(string format, string[] args);
}

class Program
{
    public static void Main(string[] args)
    {
         NativeMethods.Init(5, LogMessage);
         Console.ReadLine();
    }

    private static void LogMessage(string format, string[] args)
    {
         Console.WriteLine("Format: {0}, args: {1}", format, DisplayArgs(args));
    }
}

What happens here is that the call to NativeMethods.Init calls back LogMessage and passes data from unmanaged code as parameters. This works for most cases in which the arguments are strings. However, there is a call on which the format is:

Loaded plugin %s for version %d.

and the args contains only a string (the plugin name). They do not contain the version value, which makes sense since I used string[] in the delegate declaration. Question is, how should I write the delegate to get both the string and the int?

I tried using object[] args and got this exception: An invalid VARIANT was detected during a conversion from an unmanaged VARIANT to a managed object. Passing invalid VARIANTs to the CLR can cause unexpected exceptions, corruption or data loss.

EDIT: I could change the delegate signature to this:

internal delegate void LogFunc(string format, IntPtr args);

I could parse the format and find out how many arguments to expect and of what type. E.g. for Loaded plugin %s for version %d. I would expect a string and an int. Is there a way to get these 2 out of that IntPtr?

Escapee answered 28/4, 2012 at 7:1 Comment(1)
Marshaling the arguments is only part of the problem, you can only format the string correctly by calling vsprintf(). You'll need to write a little adapter in the C++/CLI language.Anecdotist
E
5

Just in case it helps someone, here's a solution for marshaling the arguments. The delegate is declared as:

[UnmanagedFunctionPointer(CallingConvention.Cdecl, SetLastError = true)] // Cdecl is a must
internal delegate void LogFunc(string format, IntPtr argsAddress);

The argsAddress is the unmanaged memory address where the array starts (I think). The format gives the size of the array. Knowing this I can create the managed array and fill it. Pseuso-code:

size <- get size from format
if size = 0 then return

array <- new IntPtr[size]
Marshal.Copy(argsAddress, array, 0, size);
args <- new string[size]

for i = 0 to size-1 do
   placeholder <- get the i-th placeholder from format // e.g. "%s"
   switch (placeholder)
       case "%s": args[i] <- Marshal.PtrToStringAnsi(array[i])
       case "%d": args[i] <- array[i].ToString() // i can't explain why the array contains the value, but it does
       default: throw exception("todo: handle {placeholder}")

To tell the truth, I'm not sure how this works. It just seems to get the right data. I'm not claiming it is correct though.

Escapee answered 30/4, 2012 at 7:37 Comment(1)
Thank you so much, I was trying to to exactly the same thing, using the same unmanaged API (I guessed from what your messages look like).Garcon
L
3

Another approach is to pass the va_list back to native code, something like calling vprintf in .net. I had the same issue, and I wanted it cross platform. So I wrote a sample project to demonstrate how it could work on several platforms.

See https://github.com/jeremyVignelles/va-list-interop-demo

The basic idea is :

You declare your callback delegate:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate void LogFunc(string format, IntPtr args);

You pass your callback as you did:

NativeMethods.Init(5, LogMessage);

In the callback, you handle the specific cases of the different platforms. You need to understand how it works on each platform. From my testing and understanding, you can pass the IntPtr as-is to the vprintf* family of functions on Windows (x86,x64) and Linux x86, but on Linux x64, you will need to copy a structure for that to work.

See my demo for more explanations.

EDIT : We posted an issue on .net runtime's repository a while back, you can see it here https://github.com/dotnet/runtime/issues/9316 . Unfortunately, it didn't went far because we lacked a formal proposal.

Lampblack answered 3/12, 2017 at 0:24 Comment(0)
I
1

I understand there's also an "__arglist" keyword available in C#:

Impure answered 28/4, 2012 at 7:14 Comment(1)
it is, but it does not work with delegates. I tried to use it like this: internal delegate void LogFunc(string format, __arglist);Escapee
O
1

.NET can (to some extent) marshal between va_list and ArgIterator. You can try this:

[UnmanagedFunctionPointer(CallingConvention.Cdecl, SetLastError = true)]
internal delegate void LogFunc(string format, ArgIterator args);

I am not sure how the arguments are going to be passed (strings as pointers, probably). You may have some luck with ArgIterator.GetNextArgType. Eventually, you will probably have to parse the placeholders in the format string to get the argument types.

Oney answered 16/4, 2015 at 12:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.