There are multiple structures in Windows that contain fixed sized arrays. The instance I came across recently was the KERB_QUERY_TKT_CACHE_RESPONSE
struct, which looks like this:
Windows 中有多个包含固定大小数组的结构。我最近遇到的实例是结构体 KERB_QUERY_TKT_CACHE_RESPONSE
,如下所示:
typedef struct _KERB_QUERY_TKT_CACHE_RESPONSE {
KERB_PROTOCOL_MESSAGE_TYPE MessageType;
ULONG CountOfTickets;
KERB_TICKET_CACHE_INFO Tickets[ANYSIZE_ARRAY];
} KERB_QUERY_TKT_CACHE_RESPONSE, *PKERB_QUERY_TKT_CACHE_RESPONSE;
ANYSIZE_ARRAY
is defined as 1
in winnt.h
, but the reality is that the array will be of size CountOfTickets
. This value obviously cannot be known at compile time. Translating these structs to C# can be a bit of a pain because although you can define fixed-sized arrays in an unsafe structure, you can only do so for blittable value types. That means we cannot have a fixed-sized array of another struct.
ANYSIZE_ARRAY
定义为 1
, winnt.h
但实际情况是数组的大小 CountOfTickets
为 。这个值显然在编译时是无法知道的。将这些结构转换为 C# 可能会有点麻烦,因为尽管您可以在不安全的结构中定义固定大小的数组,但只能对 blittable 值类型这样做。这意味着我们不能拥有另一个结构的固定大小的数组。
internal struct KERB_QUERY_TKT_CACHE_RESPONSE
{
public KERB_PROTOCOL_MESSAGE_TYPE MessageType;
public uint CountOfTickets;
public unsafe fixed KERB_TICKET_CACHE_INFO Tickets[1]; // <-- not valid
}
The solution most C# developers go with is to use a pointer type instead, and walk over the memory a little like this:
大多数 C# 开发人员采用的解决方案是改用指针类型,并在内存上走动,有点像这样:
for (var i = 0; i < cache.CountOfTickets; i++)
{
// deference ticket info
var ticket = *(KERB_TICKET_CACHE_INFO*)ticketPtr;
// do something with ticket
// increment pointer by size of ticket struct
ticketPtr = Unsafe.Add<byte>(ticketPtr, Marshal.SizeOf<KERB_TICKET_CACHE_INFO>());
}
I’m a fan of “unsafe” code, but the problem with pointer mathematics is that it’s quite easy to get wrong. There are two other methods that I know of to handle this type of data. I’m not saying they’re safer or better, just different.
我是“不安全”代码的粉丝,但指针数学的问题在于它很容易出错。据我所知,还有另外两种方法可以处理此类数据。我并不是说它们更安全或更好,只是不同。
The first is to create a new managed array using the CountOfTickets
value, and copy all the ticket data into it in one go.
第一种是使用该 CountOfTickets
值创建一个新的托管数组,并一次性将所有票证数据复制到其中。
// create a new array to hold all tickets
var tickets = new KERB_TICKET_CACHE_INFO[cache.CountOfTickets];
// get pointer to 0th position in array
fixed (void* arrayPtr = &tickets[0])
{
// calculate size of all tickets
var size = Marshal.SizeOf<KERB_TICKET_CACHE_INFO>() * cache.CountOfTickets;
// yeet all the data at once
Buffer.MemoryCopy(ticketPtr, arrayPtr, size, size);
}
The second can be done in a single line, by utilising the relatively new Span
feature. Note that this isn’t readily available in .NET Framework projects without installing the System.Memory
and System.Buffers
NuGet packages.
第二个可以在一行中完成,通过使用相对较新的 Span
功能。请注意,如果不安装 System.Memory
和 System.Buffers
NuGet 包,这在 .NET Framework 项目中并不容易获得。
var tickets = new Span<KERB_TICKET_CACHE_INFO>(ticketPtr, (int)cache.CountOfTickets);
Taking a span is memory efficient because it takes a slice of existing memory, rather than allocating any new memory. However, in scenarios like the above, the ticket buffer has to be freed with LsaFreeReturnBuffer
. If we did that, all the data that the span is referencing would be lost. Spans do have a .ToArray()
method which creates a copy of the data that would then fall under the scope of the garbage collector.
采用跨度是节省内存的,因为它占用现有内存的一部分,而不是分配任何新内存。但是,在上述场景中,必须使用 LsaFreeReturnBuffer
释放票证缓冲区。如果我们这样做,跨度引用的所有数据都将丢失。Span 确实有一种方法 .ToArray()
可以创建数据的副本,然后该副本将落入垃圾回收器的范围。
Happy coding. 祝您编码愉快。
原文始发于Rasta Mouse:ANYSIZE_ARRAY in C#