我试图以这种方式分配一个结构数组:
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的IntPtr指向的数据进行操作,而不是每次都复制数据,避免使用不安全的代码。
谢谢大家!
答案 0 :(得分:11)
您可以考虑四个选项,两个仅使用“安全”代码,两个使用不安全代码。不安全的选项可能会明显加快。
在托管内存中分配您的数组,并声明您的P / Invoke函数以获取该数组。即,而不是:
[DllImport(...)]
static extern bool Foo(int count, IntPtr arrayPtr);
制作
[DllImport(...)]
static extern bool Foo(int count, NativeType[] array);
(我已将[{1}}用于您的结构名称而不是NativeType
,因为T
通常用于通用上下文。)
这种方法的问题在于,据我所知,T
数组将在每次调用NativeType[]
时被编组两次。它将从托管内存复制到非托管内存
调用之前的内存,然后从非托管内存复制到托管内存。但是,如果Foo
只读取或写入数组,则可以进行改进。在这种情况下,使用Foo
(只读)或tarray
(只写)属性装饰[In]
参数。这允许运行时跳过其中一个复制步骤。
正如您现在所做的那样,将数组分配到非托管内存中,并使用一组对[Out]
和Marshal.PtrToStructure
的调用。这可能比第一个选项执行得更糟,因为您仍然需要来回复制数组元素,并且您正在逐步执行此操作,因此您需要更多开销。另一方面,如果数组中有许多元素,但只能在调用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);
如果您可以使用不安全的代码并关注性能,那么最后一个选项可能是最干净的,因为您唯一不安全的代码是您调用本机例程的地方。如果性能不是问题(可能是因为数组的大小相对较小),或者如果你不能使用不安全的代码(也许你没有完全信任),那么第一个选项可能是最干净的,但是,正如我所提到的,如果在调用本机例程之间访问的元素数量只是数组中元素数量的一小部分,那么第二个选项会更快。
不安全的操作假设您的结构是blittable。如果没有,那么安全例程是您唯一的选择。
答案 1 :(得分:8)
“为什么
IntPtr
缺少算术?”
IntPtr
只存储一个内存地址。它没有关于该内存位置内容的任何信息。以这种方式,它类似于void*
。要启用指针运算,您必须知道指向的对象的大小。
从根本上说,IntPtr
主要设计为在托管上下文中用作不透明句柄(即,您不会在托管代码中直接取消引用,而只是保持传递给非托管代码。){{ 1}} context提供了可以直接操作的指针。
答案 2 :(得分:3)
实际上,IntPtr
类型没有自己的算术运算符。 C#支持正确(不安全)指针算术 ,但IntPtr
和Marshal
类存在“更安全”的指针使用。
我认为您需要以下内容:
int index = 1; // 2nd element of array
var v = (T)Marshal.PtrToStructure(new IntPtr(data.ToInt32() +
index * Marshal.SizeOf(typeof(T)), typeof(T));
另请注意,IntPtr
int
与IntPtr
之间没有隐式转换,因此没有运气。
通常情况下,如果您要使用指针进行任何远程复杂操作,最好选择不安全的代码。
答案 3 :(得分:2)
您可以使用IntPtr.ToInt32()
使用指针结构的整数存储器地址,但要注意平台“位数”(32/64)。
对于典型的指针算术,请使用指针(在文档中查找fixed
和unsafe
):
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
数据,而不是纯integer
或long
类型,因为它提供了一个薄层,可以防止操纵内容结构体。您可以直接操作IntPtr
的内部,但要么需要不安全的指针(再次是不安全的上下文)或反射。最后,IntPtr会自动采用平台的指针大小。
答案 4 :(得分:0)
您可以使用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));
}
但是,像往常一样,当强制转换为不同的指针类型时,您必须要注意可能的对齐问题。