编组变量参数 - __arglist或替代

时间:2014-10-10 17:25:33

标签: c# c++ pinvoke marshalling variadic-functions

描述我试图解决的问题的最佳方式是用代码进行交谈。我在这个论坛上看到了很多__arglist问题,但没有很多有用的答案。我知道应该避免使用_arglist,所以我可以选择其他方法

在一个C ++模块中,我有类似下面的内容

void SomeFunction(LPCWSTR pszFormat, va_args args)
{
    // this function is not exported...
    // it is designed to take a format specifier and a list of variable 
    // arguments and "printf" it into a buffer. This code 
    // allocates buffer and uses _vsnwprintf_s to format the 
    // string. 
    // I really do not have much flexibility to rewrite this function
    // so please steer away from scrutinizing this. it is what is 
    // and I need to call it from C#.

    ::_vsnwprintf_s(somebuff, buffsize, _TRUNCATE, pszFormat, args)
}

__declspec(dllexport) void __cdecl ExportedSomeFunction(LPCWSTR pszFormat, ...)
{
    // the purpose of this method is to export SomeFunction to C# code.
    // it handles any marshaling. I can change this however it makes sense
    va_list args ;
    va_start(args, pszFormat) ;
    SomeFunction(pszFormat, args) ;
    va_end(args) ;
}

在另一个C#模块中,我有代码处理所有对C ++ DLL的编组。 目的是隐藏Native API的所有复杂性并从用户代码中编组。 最终目标是C ++开发人员或C#开发人员进行SAME API调用,但代码只编写一次并导出到两者

[DllImport("mymodule.dll", CallingConvention=CallingConvention.Cdecl)]
private static extern void ExportedSomeFunction(
        [MarshalAs(UnmanagedType.LPWStr)] out string strPath, 
        /* ? what goes here ? */);
void SomeFunction(string strFormat, /*? what goes here ?*/ )
{
    // handles marshalling incoming data to what ever is needed by exported C function
    ExportedSomeFunction(strFormat, /*? something ?*/ ) ;
}

然后其他模块中的用户代码应如下所示......

SomeFunction("my format: %ld, %s", 5, "Some Useless string") ;

那将是理想的,但我准备和

一起生活
SomeFunction("my format: %ld, %s", __arglist(5, "Some Useless string")) ;

我并不关心数据是如何编组的。如果我使用__arglist或某些数组,只要我最终得到一个va_args

我就不在乎

__ arglist看起来像解决方案,我可以成功调用

ExportedSomeFunction(strFormat, __arglist(5, "Some Useless string")) ;

但我无法弄清楚如何使用变量参数调用C#SomeFunction并将__arglist传递给导出的函数。

SomeFunction("my format: %ld, %s", __arglist(5, "Some Useless string")) ;

我无法让这个工作......

[DllImport("mymodule.dll", CallingConvention=CallingConvention.Cdecl)]
private static extern void ExportedSomeFunction(
        [MarshalAs(UnmanagedType.LPWStr)] out string strPath, 
         __arglist);
void SomeFunction(string strFormat, __arglist )
{
    ExportedSomeFunction(strFormat,  __arglist) ;  // error cannot convert from RuntimeArgumentHandle to __arglist
}

这会编译,但不会产生预期的结果。在C ++中收到的参数列表是错误的。

private static extern void ExportedSomeFunction(
        [MarshalAs(UnmanagedType.LPWStr)] out string strPath, 
         RuntimeArgumentHandle args);

2 个答案:

答案 0 :(得分:5)

这是我的建议如何解决这个问题。看看varargs.h是VisualStudio的一部分。这可以让您了解va_list的含义。您可以看到:typedef char * va_list;。它只是一个指针。

__arglist不仅没有记录,I don't think it works correctly on 64-bit processes.

您需要在C#端动态构建va_list。我相信这是比未记录的__arglist更好的解决方案,而且似乎工作得很好。对于C#,您希望使用params[],并在C ++接收端使用va_list。每个可变函数都应该具有以v...开头的函数,例如vsprintf,它接收va_list,而不是摆弄堆栈中的参数。

将此美容复制/粘贴到您的解决方案中:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;

// Author: Chris Eelmaa

namespace ConsoleApplication1
{
    #region VariableCombiner

    class CombinedVariables : IDisposable
    {
        readonly IntPtr _ptr;
        readonly IList<IDisposable> _disposables;

        bool _disposed;

        public CombinedVariables(VariableArgument[] args)
        {
            _disposables = new List<IDisposable>();

            _ptr = Marshal.AllocHGlobal(args.Sum(arg => arg.GetSize()));
            var curPtr = _ptr;

            foreach (var arg in args)
            {
                _disposables.Add(arg.Write(curPtr));
                curPtr += arg.GetSize();
            }
        }

        public IntPtr GetPtr()
        {
            if(_disposed)
                throw new InvalidOperationException("Disposed already.");

            return _ptr;
        }

        public void Dispose()
        {
            if (!_disposed)
            {
                _disposed = true;

                foreach (var disposable in _disposables)
                    disposable.Dispose();

                Marshal.FreeHGlobal(_ptr);
            }
        }
    }

    #endregion

    #region VariableArgument

    abstract class VariableArgument
    {
        #region SentinelDispose

        protected  static readonly IDisposable SentinelDisposable =
              new SentinelDispose();

        class SentinelDispose : IDisposable
        {
            public void Dispose()
            {

            }
        }

        #endregion

        public abstract IDisposable Write(IntPtr buffer);

        public virtual int GetSize()
        {
            return IntPtr.Size;
        }

        public static implicit operator VariableArgument(int input)
        {
            return new VariableIntegerArgument(input);
        }

        public static implicit operator VariableArgument(string input)
        {
            return new VariableStringArgument(input);
        }

        public static implicit operator VariableArgument(double input)
        {
            return new VariableDoubleArgument(input);
        }
    }

    #endregion

    #region VariableIntegerArgument

    sealed class VariableIntegerArgument : VariableArgument
    {
        readonly int _value;

        public VariableIntegerArgument(int value)
        {
            _value = value;
        }

        public override IDisposable Write(IntPtr buffer)
        {
            Marshal.Copy(new[] { _value }, 0, buffer, 1);
            return SentinelDisposable;
        }
    }

    #endregion

    #region VariableDoubleArgument

    sealed class VariableDoubleArgument : VariableArgument
    {
        readonly double _value;

        public VariableDoubleArgument(double value)
        {
            _value = value;
        }

        public override int GetSize()
        {
            return 8;
        }

        public override IDisposable Write(IntPtr buffer)
        {
            Marshal.Copy(new[] { _value }, 0, buffer, 1);
            return SentinelDisposable;
        }
    }

    #endregion

    #region VariableStringArgument

    sealed class VariableStringArgument : VariableArgument
    {
        readonly string _value;

        public VariableStringArgument(string value)
        {
            _value = value;
        }

        public override IDisposable Write(IntPtr buffer)
        {
            var ptr = Marshal.StringToHGlobalAnsi(_value);

            Marshal.Copy(new[] {ptr}, 0, buffer, 1);

            return new StringArgumentDisposable(ptr);
        }

        #region StringArgumentDisposable

        class StringArgumentDisposable : IDisposable
        {
            IntPtr _ptr;

            public StringArgumentDisposable(IntPtr ptr)
            {
                _ptr = ptr;
            }

            public void Dispose()
            {
                if (_ptr != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(_ptr);
                    _ptr = IntPtr.Zero;
                }
            }
        }

        #endregion
    }
    #endregion
}

以及使用示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(
              AmazingSPrintf("I am %s, %d years old, %f meters tall!",
                "Chris",
                24,
                1.94));
        }

        static string AmazingSPrintf(string format, params VariableArgument[] args)
        {
            if (!args.Any()) 
                return format;

            using (var combinedVariables = new CombinedVariables(args))
            {
                var bufferCapacity = _vscprintf(format, combinedVariables.GetPtr());
                var stringBuilder = new StringBuilder(bufferCapacity + 1);

                vsprintf(stringBuilder, format, combinedVariables.GetPtr());

                return stringBuilder.ToString();
            }
        }

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

        [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
        static extern int _vscprintf(
            string format,
            IntPtr ptr);

    }
}

CombinedVariables类用于构建va_list,然后您可以将其传递给C ++方法void SomeFunction(LPCWSTR pszFormat, va_list args)

您需要处理VariableStringArgument,因为它目前适用于ANSI。您可能正在寻找Marshal.StringToHGlobalUni

答案 1 :(得分:0)

请注意,va_list...之间存在细微差别。 printf使用...,而vprintf使用va_listva_list通常是指向...的第一个元素的指针。 __arglist适用于...

对于va_list,您可以使用@Erti代码或我的代码:

public class VaList : IDisposable
{
    protected readonly List<GCHandle> handles = new List<GCHandle>();

    public VaList(bool unicode, params object[] args)
    {
        if (args == null)
        {
            throw new ArgumentNullException("args");
        }

        // The first handle is for the bytes array
        handles.Add(default(GCHandle));

        int total = 0;
        var bytes = new PrimitiveToBytes[args.Length];

        for (int i = 0; i < args.Length; i++)
        {
            int size = Convert(unicode, args[i], ref bytes[i]);

            bytes[i].Size = size;
            total += size;
        }

        // Instead of a byte[] we use a IntPtr[], so copying elements
        // inside is faster (perhaps :-) )
        var buffer = new IntPtr[total / IntPtr.Size];
        handles[0] = GCHandle.Alloc(buffer, GCHandleType.Pinned);

        for (int i = 0, j = 0; i < args.Length; i++)
        {
            buffer[j++] = bytes[i].IntPtr;

            // long or double with IntPtr == 4
            if (bytes[i].Size > IntPtr.Size)
            {
                buffer[j++] = (IntPtr)bytes[i].Int32High;
            }
        }
    }

    // Overload this to handle other types
    protected virtual int Convert(bool unicode, object arg, ref PrimitiveToBytes primitiveToBytes)
    {
        int size;

        if (arg == null)
        {
            primitiveToBytes.IntPtr = IntPtr.Zero;
            size = IntPtr.Size;
        }
        else
        {
            Type type = arg.GetType();

            TypeCode typeHandle = Type.GetTypeCode(type);

            switch (typeHandle)
            {
                case TypeCode.Boolean:
                    // Boolean converted to Int32
                    primitiveToBytes.Int32 = (bool)arg ? 1 : 0;
                    size = IntPtr.Size;
                    break;

                case TypeCode.SByte:
                    primitiveToBytes.SByte = (sbyte)arg;
                    size = IntPtr.Size;
                    break;

                case TypeCode.Byte:
                    primitiveToBytes.Byte = (byte)arg;
                    size = IntPtr.Size;
                    break;

                case TypeCode.Int16:
                    primitiveToBytes.Int16 = (short)arg;
                    size = IntPtr.Size;
                    break;

                case TypeCode.UInt16:
                    primitiveToBytes.UInt16 = (ushort)arg;
                    size = IntPtr.Size;
                    break;

                case TypeCode.Int32:
                    primitiveToBytes.Int32 = (int)arg;
                    size = IntPtr.Size;
                    break;

                case TypeCode.UInt32:
                    primitiveToBytes.UInt32 = (uint)arg;
                    size = IntPtr.Size;
                    break;

                case TypeCode.Int64:
                    primitiveToBytes.Int64 = (long)arg;
                    size = sizeof(long);
                    break;

                case TypeCode.UInt64:
                    primitiveToBytes.UInt64 = (ulong)arg;
                    size = sizeof(ulong);
                    break;

                case TypeCode.Single:
                    // Single converted to Double
                    primitiveToBytes.Double = (double)(float)arg;
                    size = sizeof(double);
                    break;

                case TypeCode.Double:
                    primitiveToBytes.Double = (double)arg;
                    size = sizeof(double);
                    break;

                case TypeCode.Char:
                    if (unicode)
                    {
                        primitiveToBytes.UInt16 = (char)arg;
                    }
                    else
                    {
                        byte[] bytes = Encoding.Default.GetBytes(new[] { (char)arg });

                        if (bytes.Length > 0)
                        {
                            primitiveToBytes.B0 = bytes[0];

                            if (bytes.Length > 1)
                            {
                                primitiveToBytes.B1 = bytes[1];

                                if (bytes.Length > 2)
                                {
                                    primitiveToBytes.B2 = bytes[2];

                                    if (bytes.Length > 3)
                                    {
                                        primitiveToBytes.B3 = bytes[3];
                                    }
                                }
                            }
                        }
                    }

                    size = IntPtr.Size;
                    break;

                case TypeCode.String:
                    {
                        string str = (string)arg;

                        GCHandle handle;

                        if (unicode)
                        {
                            handle = GCHandle.Alloc(str, GCHandleType.Pinned);
                        }
                        else
                        {
                            byte[] bytes = new byte[Encoding.Default.GetByteCount(str) + 1];
                            Encoding.Default.GetBytes(str, 0, str.Length, bytes, 0);
                            handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
                        }

                        handles.Add(handle);
                        primitiveToBytes.IntPtr = handle.AddrOfPinnedObject();
                        size = IntPtr.Size;
                    }

                    break;

                case TypeCode.Object:
                    if (type == typeof(IntPtr))
                    {
                        primitiveToBytes.IntPtr = (IntPtr)arg;
                        size = IntPtr.Size;
                    }
                    else if (type == typeof(UIntPtr))
                    {
                        primitiveToBytes.UIntPtr = (UIntPtr)arg;
                        size = UIntPtr.Size;
                    }
                    else if (!type.IsValueType)
                    {
                        GCHandle handle = GCHandle.Alloc(arg, GCHandleType.Pinned);
                        primitiveToBytes.IntPtr = handle.AddrOfPinnedObject();
                        size = IntPtr.Size;
                    }
                    else
                    {
                        throw new NotSupportedException();
                    }

                    break;

                default:
                    throw new NotSupportedException();
            }
        }

        return size;
    }

    ~VaList()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        for (int i = 0; i < handles.Count; i++)
        {
            if (handles[i].IsAllocated)
            {
                handles[i].Free();
            }
        }

        handles.Clear();
    }

    public IntPtr AddrOfPinnedObject()
    {
        if (handles.Count == 0)
        {
            throw new ObjectDisposedException(GetType().Name);
        }

        return handles[0].AddrOfPinnedObject();
    }

    [StructLayout(LayoutKind.Explicit)]
    protected struct PrimitiveToBytes
    {
        [FieldOffset(0)]
        public byte B0;
        [FieldOffset(1)]
        public byte B1;
        [FieldOffset(2)]
        public byte B2;
        [FieldOffset(3)]
        public byte B3;
        [FieldOffset(4)]
        public byte B4;
        [FieldOffset(5)]
        public byte B5;
        [FieldOffset(6)]
        public byte B6;
        [FieldOffset(7)]
        public byte B7;

        [FieldOffset(4)]
        public int Int32High;

        [FieldOffset(0)]
        public byte Byte;
        [FieldOffset(0)]
        public sbyte SByte;

        [FieldOffset(0)]
        public short Int16;
        [FieldOffset(0)]
        public ushort UInt16;

        [FieldOffset(0)]
        public int Int32;

        [FieldOffset(0)]
        public uint UInt32;

        [FieldOffset(0)]
        public long Int64;
        [FieldOffset(0)]
        public ulong UInt64;

        [FieldOffset(0)]
        public float Single;
        [FieldOffset(0)]
        public double Double;

        [FieldOffset(0)]
        public IntPtr IntPtr;
        [FieldOffset(0)]
        public UIntPtr UIntPtr;

        [FieldOffset(8)]
        public int Size;
    }
}

使用示例:

[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
static extern int vprintf(string format, IntPtr ptr);

using (var list = new VaList(false, // Ansi encoding
    true, // bool test
    short.MinValue + 1, int.MinValue + 2, long.MinValue + 3, // signed
    ushort.MaxValue - 4, uint.MaxValue - 5, ulong.MaxValue - 6, // unsigned
    float.MaxValue, double.MaxValue, // float/double
    'A', "Foo", Encoding.Default.GetBytes("Bar\0"), null, // char/string
    IntPtr.Size == sizeof(int) ? (IntPtr)(int.MinValue + 7) : (IntPtr)(long.MinValue + 7), // signed ptr
    UIntPtr.Size == sizeof(uint) ? (UIntPtr)(uint.MaxValue - 8) : (UIntPtr)(ulong.MaxValue - 8))) // unsigned ptr
{
    vprintf("%d\n %hd\n %d\n %lld\n %hu\n %u\n %llu\n %f\n %f\n %c\n %s\n %s\n %s\n %p\n %p\n", list.AddrOfPinnedObject());
}

请注意,此代码仅与用于Intel x86 / x64的Visual C ++兼容。 ARM使用另一种格式,GCC还有其他格式。