C# 委托中的封送va_list

Marshal va_list in C# delegate

本文关键字:va list      更新时间:2023-10-16

我正在尝试从 c# 完成这项工作:

C 标头:

typedef void (LogFunc) (const char *format, va_list args);
bool Init(uint32 version, LogFunc *log)

C# 实现:

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

这里发生的情况是,对 NativeMethods.Init 的调用会回调LogMessage并将非托管代码中的数据作为参数传递。这适用于参数为字符串的大多数情况。但是,有一个调用,其格式为:

已加载版本 %d 的插件 %s。

并且 args 只包含一个字符串(插件名称(。它们不包含版本值,这很有意义,因为我在委托声明中使用了string[]。问题是,我应该如何编写委托来获取字符串和 int?

我尝试使用object[] args并得到以下异常:在从非托管 VARIANT 转换为托管对象的过程中检测到无效的 VARIANT 。将无效的 VARIANT 传递给 CLR 可能会导致意外异常、损坏或数据丢失。

编辑:我可以将委托签名更改为:

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

我可以解析格式并找出期望多少参数以及类型。 例如,对于版本 %d 的加载插件 %s。我期望一个字符串和一个整数。有没有办法从那个 Intptr 中取出这 2 个?

以防万一它对某人有帮助,这里有一个封送参数的解决方案。委托声明为:

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

argsAddress是阵列开始的非托管内存地址(我认为(。format给出数组的大小。知道了这一点,我可以创建托管数组并填充它。伪代码:

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}")

说实话,我不确定这是如何工作的。它似乎只是获得了正确的数据。不过,我并不是说这是正确的。

另一种方法是将va_list传递回本机代码,类似于在 .net 中调用 vprintf。我遇到了同样的问题,我希望它跨平台。因此,我编写了一个示例项目来演示它如何在多个平台上工作。

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

基本思想是:

您声明回调委托:

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

你像以前一样传递回调:

NativeMethods.Init(5, LogMessage);

在回调中,您可以处理不同平台的特定情况。您需要了解它在每个平台上的工作方式。根据我的测试和理解,您可以在Windows(x86,x64(和Linux x86上按原样将IntPtr传递给vprintf*系列函数,但是在Linux x64上,您需要复制一个结构才能使其工作。

有关更多解释,请参阅我的演示。

编辑:不久前我们在.net运行时的存储库上发布了一个问题,您可以在此处看到它 https://github.com/dotnet/runtime/issues/9316。不幸的是,它并没有走多远,因为我们缺乏正式的提案。

我知道 C# 中还有一个"__arglist"关键字:

  • http://www.dotnetinterop.com/faq/?q=Vararg

  • http://bartdesmet.net/blogs/bart/archive/2006/09/28/4473.aspx

.NET可以(在某种程度上(在va_listArgIterator之间封送。你可以试试这个:

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

我不确定参数将如何传递(字符串作为指针,可能(。你可能有一些运气 ArgIterator.GetNextArgType .最终,您可能必须分析格式字符串中的占位符以获取参数类型。