-
Notifications
You must be signed in to change notification settings - Fork 90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Treat zero-length array fields as variable length, inline arrays #387
Comments
As discussed in #589 this is also an issue for |
Fix #195 together with this one. |
We have |
We have a couple options for APIs that we could generate to make using variable-length arrays inside structs easier. All of them involve allocating native memory or pinning managed memory, as necessary because these structs have arbitrary length and must cross the interop boundary. Option 1: minimal assistancePros
Cons
Sample usage [Fact]
public unsafe void FlexibleArraySizing()
{
const int count = 3;
PAGESET* pPageSet = PAGESET.Alloc(count);
try
{
pageSet->cPageRange = count;
Span<__PAGERANGE_INLINE> pageRange = pPageSet->rgPages_UnsafeCreateSpan(count);
for (int i = 0; i < count; i++)
{
pageRange[i].iFrom = i * 2;
pageRange[i].cPage = (i * 2) + 1;
}
}
finally
{
PAGESET.Free(pPageSet);
}
} Option 2: Employ the C#
|
😆 ... or I could go with the simple and elegant solution @tannergooding uses here. That makes access easy, though it does nothing for the allocation of the memory, which I'm still interested in making easier. |
Looking at public struct BITMAPINFO
{
public BITMAPINFOHEADER bmiHeader;
[FlexibleArray]
public RGBQUAD[] bmiColors;
} I don't have a lot of CsWin32 context in my brain at the moment, so forgive my silly question: Can CsWin32 generator expose BITMAPINFO bitmapInfo = new();
bitmapInfo.biColors = new[] { 0x11223344, 0x44332211 };
PInvoke.CreateDIBSection(..., bitmapInfo, ...);
BITMAPINFO bitmapInfo = new();
bitmapInfo.biColors = new RGBQUAD[512];
PInvoke.GetDIBits(..., ref bitmapInfo, ...); |
Thanks for looking, @riverar. No, in C# structs cannot have And your What I like about @tannergooding's approach in terrafx is that the struct field almost feels like an array or span. Its indexer "just works" and you can get a span through calling a method directly on that field. IMO far more intuitive than the options I suggested earlier. But I want to keep the |
Oh right, Span is stack-only, derp. I think his approach provides the most natural C# developer UX. If we can build on top of that, I think that's ideal. If I had to type out any of the other options above, I'd be very angry. Especially since it's all in the name of avoiding unnecessary allocations at all costs, a CsWin32 principle that I (respectfully) never agreed with and that developers may not even care about. |
Your concern about CsWin32's principle to "avoiding unnecessary allocations at all costs" seems more extreme than the actual principle I'm following. After all, CsWin32 defaults to generating SafeHandle overloads, and allocations are incurred by that. But really what I am curious about is what bearing that has on this particular thread. Variable length structs are fundamentally tricky in C#. I can't think of a way to significantly simplify the requirement on the user by allowing the generated code to make allocations. If you can, I'm interested in considering that option. I've consolidated and revised the samples above to one that follows the TerraFX pattern below. I've added a [Fact]
public unsafe void FlexibleArraySizing()
{
const int count = 3;
PAGESET* pPageSet = (PAGESET*)Marshal.AllocHGlobal(PAGESET.SizeOf(count));
try
{
pPageSet->rgPages[0].nFromPage = 0;
Span<PAGERANGE> pageRange = pPageSet->rgPages.AsSpan(count);
for (int i = 0; i < count; i++)
{
pageRange[i].nFromPage = i * 2;
pageRange[i].nToPage = (i * 2) + 1;
}
}
finally
{
Marshal.FreeHGlobal((IntPtr)pPageSet);
}
} |
BTW regarding the internal static unsafe int SizeOf(int count)
{
int v = sizeof(PAGESET);
if (count > 1)
{
checked
{
v += (count - 1) * sizeof(PAGERANGE);
}
}
else if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
return v;
} vs internal static int SizeOf(int count)
{
int v = Marshal.SizeOf<PAGESET>();
if (count > 1)
{
checked
{
v += (count - 1) * Marshal.SizeOf<PAGERANGE>();
}
}
else if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
return v;
} |
@tannergooding since I have to emit code that works on .NET Framework as well, my [UnscopedRef]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Span<T> AsSpan(int length)
{
#if NET5_0_OR_GREATER
return MemoryMarshal.CreateSpan(ref this.e0, length);
#else
unsafe
{
fixed (void* p = &this.e0)
{
return new Span<T>(p, length);
}
}
#endif
} |
This unfortunately isn't safe/doesn't work because So if you had For .NET Framework, really the only safe thing is to expose an indexer which uses
For a blittable type, they should be reporting identical sizes. However, For a non-blittable type, they aren't equivalent. |
Thanks, @tannergooding. The sizeof stuff makes sense. I consider (and enforce) all such structs to be Regarding the use of Span, I understand and agree in general. However in this case, your syntax of True? If not, I can scope out the AsSpan method to only appear on .NET Core easily enough. |
Note that
This isn't strictly a guarantee. For example, a dev could opt to do this as That is notably unlikely and you might decide that it isn't worth supporting or that its fine to tell devs on .NET Framework "don't do that". But it is something to at least consider from a general correctness perspective. This also becomes relevant when deciding how to handle other types of |
Thanks again. CsWin32 doesn't leverage .NET 8 and |
See microsoft/win32metadata#571 and microsoft/win32metadata#266 as examples.
1-length arrays apparently appear to represent variable length as well: microsoft/win32metadata#912
The text was updated successfully, but these errors were encountered: