7

我试图以这种方式分配一个结构数组:

struct T {
    int a; int b;
}

data = Marshal.AllocHGlobal(count*Marshal.SizeOf(typeof(T));
...

我想访问分配的数据“绑定”一个结构到使用 AllocHGlobal 分配的数组中的每个元素......像这样

T v;
v = (T)Marshal.PtrToStructure(data+1, typeof(T));

但我没有找到任何方便的方法......为什么 IntPtr 缺乏算术?如何以“安全”的方式解决此问题?

有人可以确认 PtrToStructure 函数将数据复制到结构变量中吗?换句话说,修改结构体是否反映了结构体数组数据的修改?

当然,我想使用结构对 IntPtr 指向的数据进行操作,而不是每次都复制数据,避免不安全的代码。

谢谢大家!

4

5 回答 5

11

你有四个我能想到的选项,两个只使用“安全”代码,两个使用不安全代码。不安全的选项可能会明显更快。

安全的:

  • 在托管内存中分配您的数组,并声明您的 P/Invoke 函数以获取该数组。即,而不是:

    [DllImport(...)]
    static extern bool Foo(int count, IntPtr arrayPtr);
    

    做了

    [DllImport(...)]
    static extern bool Foo(int count, NativeType[] array);
    

    (我使用NativeType了您的结构名称而不是T,因为T它经常在通用上下文中使用。)

    这种方法的问题在于,据我所知,NativeType[]每次调用Foo. 它会在调用之前从托管内存复制到非托管内存,然后再从非托管内存复制到托管内存。Foo但是,如果只读取或写入数组,则可以改进它。在这种情况下,使用(只读)或(只写)属性装饰tarray参数。这允许运行时跳过其中一个复制步骤。[In][Out]

  • 正如您现在所做的那样,在非托管内存中分配数组,并使用一堆对Marshal.PtrToStructureand的调用Marshal.StructureToPtr。这可能会比第一个选项执行得更糟,因为您仍然需要来回复制数组的元素,并且您正在逐步进行,因此您有更多的开销。另一方面,如果数组中有很多元素,但在两次调用之间只访问其中的一小部分Foo,那么这可能会执行得更好。您可能需要几个小辅助函数,如下所示:

    static T ReadFromArray<T>(IntPtr arrayPtr, int index){
        // below, if you **know** you'll be on a 32-bit platform,
        // you can change ToInt64() to ToInt32().
        return (T)Marshal.PtrToStructure((IntPtr)(arrayPtr.ToInt64() +
            index * Marshal.SizeOf(typeof(T)));
    }
    // you might change `T value` below to `ref T value` to avoid one more copy
    static void WriteToArray<T>(IntPtr arrayPtr, int index, T value){
        // below, if you **know** you'll be on a 32-bit platform,
        // you can change ToInt64() to ToInt32().
        Marshal.StructureToPtr(value, (IntPtr)(arrayPtr.ToInt64() +
            index * Marshal.SizeOf(typeof(T)), false);
    }
    

不安全:

  • 在非托管内存中分配您的数组,并使用指针访问元素。这意味着所有使用数组的代码都必须在一个unsafe块中。

    IntPtr arrayPtr = Marhsal.AllocHGlobal(count * sizeof(typeof(NativeType)));
    unsafe{
        NativeType* ptr = (NativeType*)arrayPtr.ToPointer();
    
        ptr[0].Member1 = foo;
        ptr[1].Member2 = bar;
        /* and so on */
    }
    Foo(count, arrayPtr);
    
  • 在托管内存中分配您的数组,并在需要调用本机例程时将其固定:

    NativeType[] array = new NativeType[count];
    array[0].Member1 = foo;
    array[1].Member2 = bar;
    /* and so on */
    
    unsafe{
        fixed(NativeType* ptr = array)
            Foo(count, (IntPtr)ptr);
            // or just Foo(count, ptr), if Foo is declare as such:
            //     static unsafe bool Foo(int count, NativeType* arrayPtr);
    }
    

如果您可以使用不安全代码并且关心性能,那么最后一个选项可能是最干净的,因为您唯一不安全的代码是您调用本机例程的地方。如果性能不是问题(也许数组的大小相对较小),或者如果您不能使用不安全的代码(也许您没有完全信任),那么第一个选项可能是最干净的,但是,正如我所提到的,如果您在调用本机例程之间访问的元素数量是数组中元素数量的一小部分,那么第二个选项更快。

笔记:

不安全的操作假定您的结构是blittable。如果没有,那么安全程序是您唯一的选择。

于 2009-08-23T16:18:44.197 回答
8

“为什么IntPtr没有算术?”

IntPtr只存储一个内存地址。它没有任何关于该内存位置内容的信息。在这种方式下,它类似于void*. 要启用指针算法,您必须知道指向的对象的大小。


从根本上说,IntPtr它主要被设计为在托管上下文中用作不透明句柄(即,您不会在托管代码中直接取消引用,而只是保留传递给非托管代码。)unsafe上下文提供了可以直接操作的指针。

于 2009-08-23T14:45:54.650 回答
3

实际上,该IntPtr类型没有自己的算术运算符。C# 支持正确(不安全)的指针算术,但存在该类为了“更安全”地使用指针。IntPtrMarshal

我认为您想要以下内容:

int index = 1; // 2nd element of array
var v = (T)Marshal.PtrToStructure(new IntPtr(data.ToInt32() + 
    index * Marshal.SizeOf(typeof(T)), typeof(T));

另外,请注意andIntPtr之间没有隐式转换,所以那里没有运气。intIntPtr

通常,如果您要使用指针做任何远程复杂的事情,最好选择不安全的代码。

于 2009-08-23T14:24:13.233 回答
2

您可以使用指针结构的整数内存地址,IntPtr.ToInt32()但要注意平台“位”(32/64)。

对于典型的指针算术,使用指针(查找fixedunsafe在文档中):

T data = new T[count];
fixed (T* ptr = &data)
{
    for (int i = 0; i < count; i++)
    {
        // now you can use *ptr + i or ptr[i]
    }
}

编辑:

我正在考虑IntPtr允许您在不显式操作指针地址的情况下处理指向数据的指针。这允许您与 COM 和本机代码互操作,而无需声明不安全的上下文。运行时强加的唯一要求是非托管代码权限。出于这些目的,似乎大多数编组方法只接受整个IntPtr数据,而不是纯数据integerlong类型,因为它提供了一个薄层来防止操纵结构的内容。您可以直接操作 a 的内部结构IntPtr,但这需要不安全的指针(同样是不安全的上下文)或反射。最后,IntPtr 会自动采用平台的指针大小。

于 2009-08-23T14:23:09.320 回答
0

您可以使用固定数组Marshal.UnsafeAddrOfPinnedArrayElement中的 an 来获取数组中特定元素的地址。IntPtr

这是一个围绕固定数组的包装器的示例类,以便我可以将它们与 IntPtr 和编组代码一起使用:

    /// <summary>
    /// Pins an array of Blittable structs so that we can access the data as bytes. Manages a GCHandle around the array.
    /// https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.unsafeaddrofpinnedarrayelement?view=netframework-4.7.2
    /// </summary>
    public sealed class PinnedArray<T> : IDisposable
    {
        public GCHandle Handle { get; }
        public T[] Array { get; }

        public int ByteCount { get; private set; }
        public IntPtr Ptr { get; private set; }

        public IntPtr ElementPointer(int n)
        {
            return Marshal.UnsafeAddrOfPinnedArrayElement(Array, n);
        }

        public PinnedArray(T[] xs)
        {
            Array = xs;
            // This will fail if the underlying type is not Blittable (e.g. not contiguous in memory)
            Handle = GCHandle.Alloc(xs, GCHandleType.Pinned);
            if (xs.Length != 0)
            {
                Ptr = ElementPointer(0);
                ByteCount = (int) Ptr.Distance(ElementPointer(Array.Length));
            }
            else
            {
                Ptr = IntPtr.Zero;
                ByteCount = 0;
            }
        }

        void DisposeImplementation()
        {
            if (Ptr != IntPtr.Zero)
            {
                Handle.Free();
                Ptr = IntPtr.Zero;
                ByteCount = 0;
            }
        }

        ~PinnedArray()
        {
            DisposeImplementation();
        }

        public void Dispose()
        {
            DisposeImplementation();
            GC.SuppressFinalize(this);
        }
    }

恕我直言,使用 PInvoke 和 IntPtr 就像将程序集标记为不安全并在不安全的上下文中使用指针一样危险(如果不是更多的话)

如果您不介意不安全的块,您可以编写对IntPtr强制转换进行操作的扩展函数,byte*如下所示:

    public static long Distance(this IntPtr a, IntPtr b)
    {
         return Math.Abs(((byte*)b) - ((byte*)a));
    }

但是,像往常一样,您必须注意在转换为不同的指针类型时可能出现的对齐问题。

于 2019-01-31T14:39:10.623 回答