C#非托管内存分配引用类型数组触发AccessViolationException问题求助
Hey there, let's dig into why you're hitting that AccessViolationException and how to fix this issue with unmanaged arrays of reference types.
Why the AccessViolationException Happens
The core issue here is that you're trying to trick the CLR into treating unmanaged memory as a managed reference-type array—and the CLR really doesn't like that. Here's the breakdown:
- CLR expects managed references to point to the managed heap: When you store a reference to your unmanaged
Testobject in the array, the CLR sees that as a normal managed object reference. The GC scans these references to track live objects, but your unmanaged "objects" aren't in the managed heap and aren't registered with the GC. When the GC tries to validate or interact with these references (even implicitly), it can stumble into invalid memory, causing the crash. - Unmanaged objects lack proper CLR object metadata: While you wrote the type pointer to unmanaged memory, CLR-managed objects have a full object header (sync block index, type handle, etc.) that's initialized by the runtime during object creation. Your manual setup skips this critical initialization, so when the CLR tries to access
array[0].Value, it's trying to parse memory that doesn't match its expected structure. - Reference-type arrays have hidden CLR requirements: Managed arrays have a specific memory layout (method table pointer, length, then elements) that your
InitializeArraymethod only partially replicates. The CLR relies on these internal details for operations like bounds checking and GC scanning, which breaks when working with unmanaged memory pretending to be a managed array.
Your workaround with structs works because structs are value types—when you store a struct containing an IntPtr in the array, the CLR doesn't treat that pointer as a managed object reference. It just sees a raw value, so the GC leaves it alone, avoiding the invalid memory access.
Solutions to Fix the Issue
Here are a few solid approaches to get this working safely:
1. Stick with Struct Wrappers (Your Proven Workaround)
Formalize the struct wrapper to encapsulate access to your unmanaged objects, so you avoid exposing raw unmanaged references to the CLR's type system:
public struct UnmanagedTest { private IntPtr _objectPtr; // Create from an existing unmanaged Test object public static UnmanagedTest From(Test unmanagedTest) { return new UnmanagedTest { _objectPtr = Unmanaged<Test>.Find(unmanagedTest) }; } // Access properties safely by reading/writing directly to unmanaged memory public int Value { get { // Calculate the offset of the Value field (adjust based on your Test class layout) int fieldOffset = IntPtr.Size; // Skip the type pointer in your unmanaged object return Marshal.ReadInt32(_objectPtr + fieldOffset); } set { int fieldOffset = IntPtr.Size; Marshal.WriteInt32(_objectPtr + fieldOffset, value); } } // Get the wrapped Test instance if needed public Test GetInstance() => Unmanaged<Test>.Create(_objectPtr); }
Then use it with your unmanaged array:
var array = UnmanagedArray<UnmanagedTest>.New(2); array[0] = UnmanagedTest.From(test); Console.WriteLine(array[0].Value); // No crash!
2. Avoid Mimicking Managed Arrays Entirely
Instead of forcing unmanaged memory to act like a T[], manage the array manually using raw IntPtr operations. This eliminates any CLR assumptions about the memory:
public static class UnmanagedArrayHelper<T> where T : class { public static IntPtr Create(int count) { // Header: 2*IntPtr.Size (method table + length) + element pointers int headerSize = 2 * IntPtr.Size; int totalSize = headerSize + count * IntPtr.Size; IntPtr arrayHandle = Marshal.AllocHGlobal(totalSize); // Write array length (skip method table if we don't need to cast to T[]) Marshal.WriteInt32(arrayHandle + headerSize - sizeof(int), count); // Initialize all element pointers to zero for (int i = 0; i < count; i++) { Marshal.WriteIntPtr(arrayHandle + headerSize + i * IntPtr.Size, IntPtr.Zero); } return arrayHandle; } public static void SetElement(IntPtr arrayHandle, int index, T obj) { int headerSize = 2 * IntPtr.Size; IntPtr elementPtr = arrayHandle + headerSize + index * IntPtr.Size; Marshal.WriteIntPtr(elementPtr, Unmanaged<T>.Find(obj)); } public static T GetElement(IntPtr arrayHandle, int index) { int headerSize = 2 * IntPtr.Size; IntPtr elementPtr = arrayHandle + headerSize + index * IntPtr.Size; IntPtr objPtr = Marshal.ReadIntPtr(elementPtr); return Unmanaged<T>.Create(objPtr); } public static void Destroy(IntPtr arrayHandle) { Marshal.FreeHGlobal(arrayHandle); } }
Usage example:
IntPtr arrayHandle = UnmanagedArrayHelper<Test>.Create(2); UnmanagedArrayHelper<Test>.SetElement(arrayHandle, 0, test); Test retrieved = UnmanagedArrayHelper<Test>.GetElement(arrayHandle, 0); Console.WriteLine(retrieved.Value); Unmanaged<Test>.Destroy(test); UnmanagedArrayHelper<Test>.Destroy(arrayHandle);
3. Use a Managed Array for Struct Wrappers
If you don't need the entire array to be in unmanaged memory, use a normal managed array of your struct wrapper. This lets the GC handle the array itself while you manage the unmanaged objects:
// Managed array of struct wrappers UnmanagedTest[] managedArray = new UnmanagedTest[2]; managedArray[0] = UnmanagedTest.From(test); Console.WriteLine(managedArray[0].Value); // Clean up unmanaged objects manually Unmanaged<Test>.Destroy(test);
Key Notes to Remember
- Unmanaged objects don't run constructors: If your
Testclass has constructor logic, you'll need to manually invoke it usingDynamicMethodor reflection on the unmanaged memory pointer. - Nested references are risky: If
Testcontains other reference-type fields (likestring), those fields will be treated as managed references by the CLR. This can lead to GC corruption unless you also wrap those nested references in structs or manage them entirely in unmanaged memory. - Always clean up unmanaged memory: Make sure you call
Destroyfor every unmanaged object and array to avoid memory leaks.
内容的提问来源于stack exchange,提问作者Feralnex

