【问题标题】:IntPtr arithmeticsIntPtr 算术
【发布时间】:2010-11-22 01:34:46
【问题描述】:

我尝试以这种方式分配结构数组:

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 函数将数据复制到 struct 变量中吗?也就是说,修改struct是否反映了结构数组数据的修改?

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

谢谢大家!

【问题讨论】:

  • PtrToStructure 复制数据。如果您发现自己不得不做这种事情,请编写一个非托管库并 p/invoke 它。
  • 也许您想解释一下为什么要从非托管内存中使用一组值开始。您可以创建一个托管数组并将其来回编组为非托管代码。你也说“安全”,但这是否意味着标记为不安全的代码是不可能的,或者你只是说你不想犯错误?
  • 我正在为 OpenGL 实现缓冲区对象抽象。分配的数据可能包含顶点、法线......我虽然使用 AllocHGlobal 分配内存并管理数据布局,但没有放弃我的实现提供的语义。一个例子是 glVertexPointer:它需要一个缓冲区地址和有关内存布局的信息。我可以在不复制数据的情况下将 T[] 转换为 IntPtr(直接通过 IntPt 使用数组内存)吗?

标签: c# marshalling intptr


【解决方案1】:

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

这是一个sample class for a wrapper around pinned arrays,以便我可以将它们与 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));
    }

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

【讨论】:

    【解决方案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 类型,因为它提供了一个薄层来防止操纵结构的内容。您可以直接操作IntPtr 的内部,但这需要不安全的指针(同样是不安全的上下文)或反射。最后,平台指针大小自动采用 IntPtr。

    【讨论】:

      【解决方案3】:

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

      安全:

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

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

        做起来

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

        (我使用NativeType 作为结构名称而不是T,因为T 经常用于通用上下文中。)

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

      • 正如您现在所做的那样,在非托管内存中分配数组,并使用一组对Marshal.PtrToStructureMarshal.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。如果没有,那么安全例程是您唯一的选择。

      【讨论】:

        【解决方案4】:

        确实,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));
        

        另外,请注意IntPtrintIntPtr 之间没有隐式转换,所以没有运气。

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

        【讨论】:

        • IntPtr 是一个 .NET 原始类型。检查typeof(System.IntPtr).IsPrimitive
        • 是的,这就是我要找的。但也许最好分配一个结构数组并将其作为 IntPtr 传递;但我仍在讨论如何保证结构字段正确对齐...
        • Luca: [System.Runtime.InteropServices.StructLayout] 指定结构的对齐和打包参数。
        【解决方案5】:

        “为什么IntPtr 缺少算术?”

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


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

        【讨论】:

          猜你喜欢
          • 2011-08-01
          • 2017-05-18
          • 2016-03-10
          • 2012-12-16
          • 2022-01-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多