-
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
Avoid using Span<T>
on TFMs older than net45
#117
Comments
Span<T>
on TFMs older than net45
Only |
This is the generated code. Would it be acceptable to omit the internal partial struct BITMAPINFO
{
internal BITMAPINFOHEADER bmiHeader;
internal unsafe Span<RGBQUAD> bmiColors
{
get
{
fixed (void *p = &this.__bmiColors)
return new Span<RGBQUAD>(p, 1);
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct __bmiColors_1
{
private RGBQUAD _1;
}
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private __bmiColors_1 __bmiColors;
} |
Separate issue, but I'm pretty sure this member is unusable because the number of elements is wrong:
(https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfo) |
The number of elements isn't of concern. It is the number and order of fields, and the generated struct has just two, in the same order as the two documented, so we're ok there. And no, simply dropping |
In this specific case, I'm having trouble understanding how the one-element span is usable even if you do access it. The docs I quoted seem to say the number of elements is not actually 1. What is a usage example? Without that, I have a hard time knowing what a pre-net45 equivalent would be. |
Oh, I think I misunderstood your prior comment. sorry. For a workaround, you can just define your own struct with a RGBQUAD* pFirst = &bitmapInfo.bmiColors`
pFirst[0] = something;
pFirst[1] = else` It'll be on you to make sure you don't write to more memory than you allocated. |
So from the perspective of whether That will be an answer that informs my original question, which is what code should be generated on net35 instead of the current code. If it turns out that Possibly if the metadata is sophisticated enough, a span-returning member could still be exposed which reads the header fields to determine what length the color table should be. But it won't know how much memory was actually allocated. |
Yes, that's exactly what I was thinking of. And by the time someone has initialized the header's count field, the memory should have already been allocated. |
Cool, so if we tentatively assume that there will still be a span-returning internal property and a private field on newer frameworks, what would the ideal codegen be when internal partial struct BITMAPINFO
{
internal BITMAPINFOHEADER bmiHeader;
#if NET35
internal unsafe RGBQUAD* bmiColors => &this.__bmiColors;
#else
internal unsafe Span<RGBQUAD> bmiColors
{
get
{
fixed (void *p = &this.__bmiColors)
return new Span<RGBQUAD>(p, 1);
}
}
#endif
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct __bmiColors_1
{
private RGBQUAD _1;
}
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private __bmiColors_1 __bmiColors;
} It would probably be better to generate the lowest-common-denominator code for all frameworks you're targeting so that you don't have to use preprocessor in the code that consumes it though. internal partial struct BITMAPINFO
{
internal BITMAPINFOHEADER bmiHeader;
internal unsafe RGBQUAD* bmiColors => &this.__bmiColors;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct __bmiColors_1
{
private RGBQUAD _1;
}
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private __bmiColors_1 __bmiColors;
} |
A property returning a pointer to another field doesn't go down well, since the CLR cannot guarantee the caller has already pinned the struct. So we would likely do something like the metadata does today: expose the first element and leave it to the caller to get a pointer to it. |
We wouldn't need to use Does your last code snippet even compile? I'm surprised C# would let you return a pointer to your own field like that. |
How nice is that from the standpoint of And regardless of that, it would be annoying to have to use
No, you're right. It would have to be closer to what you have already, just without the internal unsafe RGBQUAD* bmiColors
{
get
{
fixed (RGBQUAD* p = &__bmiColors)
return p;
}
}
The current span-based code has this same problem, right? Just so I'm understanding.
If I'm following this part correctly, you're saying all target frameworks should do this, regardless of whether they have internal partial struct BITMAPINFO
{
internal BITMAPINFOHEADER bmiHeader;
internal RGBQUAD bmiColors;
} |
IMO, not if the lowest common denominator we're talking about is pre-net45 days, particularly if net45+ affords us the opportunity to greatly improve the API and/or improve runtime performance that we'd lose by limiting ourselves to very old APIs and language constructs. Now, if we can support net35 and simply add members for later TFMs that improve accessibility, that's certainly worth exploring. But in general, we haven't even made a commitment to support prior to net45 nor C# language versions before 9. The fact that they sometimes work is convenient only at this point.
Just fine, I expect. Whether the generated code is the same or different between TFMs, the code the debugger brings up will match the TFM.
Yes, I think so. But I'd like to verify that before I say definitely. |
I'm only asking for lowest-common-denominator support among the target frameworks listed in the project. You'd still get The alternatives to this are either forcing the use of preprocessor on the project any time it touches that member, because the type of the member varies across target frameworks, or effectively blocking net35 entirely. |
I see another alternative: define members that work on net35, and add members when targeting newer frameworks. Never use |
Nice, I like that even better! The starting point would then be to generate all code without |
If net35 was the primary target, I would agree. But since we're prioritizing getting accurate, complete API coverage that targets where most codebases are at now, that isn't where we started, nor where we're going in the short term. We have lots of work to do to help the masses get off the ground such as define more enums, fix the string parameters that show up as Once we get the basics going for the majority of folks, we can circle back and fill in for the more rare cases like net35 targeting. I estimate that's a month or two out, at least. And when we circle back, we won't take away the goodness that net45+ has, but likely just expose the private field that's already there. |
I was going to offer to implement. Is now a bad time? |
Thank you. Please wait for #121 to close as that is somewhat disruptive and I don't want anyone to have to deal with merge conflicts. I am working on it now. When you do it:
Sound good? |
Yes, thanks! All points sound good. On 2, that is my understanding also. I'm used to using On 3, the suffix idea sounds good. Underscores aren't my favorite, and neither are contractions of words, but it's up to you. What do you think about the suffix On 4, I'm good with that but who knows how quickly I (or someone else) will be back asking for |
Agreed. Friendly overloads are going to be in serious flux to fix #26 and I don't want them any more complicated than they are already. There's also a good chance we can avoid use of span where necessary as part of that work anyway. All those suffixes are reasonable. I'm not sure which is best, but I'm leaning against field since most members on the structs are fields anyway, so I'd rather we distinguish based on why this is present and possibly redundant with something else. I'm not a fan of underscores in general either, but we want something that is not expected to ever collide with other members in the struct, and ideally be distinguishable as something out of the ordinary in order to attract attention (whether to draw use or warn away, depending on what the user needs). IMO Raw or Pointer meet those goals. As for the |
I like your reasoning and prefer
I meant a ref to the field itself which could be used to set the pointer to some other pointer. Foo(out someStruct.SomePointer);
// Foo has now assigned a different pointer to the field someStruct.SomePointer Or: someStruct.SomePointer = otherPointer; The point being, if you're looking for something to assign or take a ref ( ...so, in a (rare?) scenario like this, you'll be specifically expecting and looking for raw field access. That's all. |
@tannergooding noticed that this code is unsafe because the span is assumed to have a lifetime greater than internal unsafe Span<RGBQUAD> bmiColors
{
get
{
fixed (void *p = &this.__bmiColors)
return new Span<RGBQUAD>(p, 1);
}
} You could then end up with a pointer to invalid memory with code like: var x = new BITMAPINFO();
return x.bmiColors; This makes me think that the best thing for now is to drop span properties altogether. |
From a functionality and usability perspective, my opinion is that the correct thing to do here is to not expose properties for most things and to instead just expose the raw fields publicly. The type definition in typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
} BITMAPINFO, FAR *LPBITMAPINFO, *PBITMAPINFO; This means that, from a metadata perspective, there is nothing to indicate that
If you use public partial struct BITMAPINFO
{
public BITMAPINFOHEADER bmiHeader;
[NativeTypeName("RGBQUAD [1]")]
public _bmiColors_e__FixedBuffer bmiColors;
public partial struct _bmiColors_e__FixedBuffer
{
public RGBQUAD e0;
public ref RGBQUAD this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
return ref AsSpan(int.MaxValue)[index];
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<RGBQUAD> AsSpan(int length) => MemoryMarshal.CreateSpan(ref e0, length);
}
} See also: https://source.terrafx.dev/#TerraFX.Interop.Windows/um/wingdi/BITMAPINFO.cs,0a6f50687ac11b42, https://github.com/terrafx/terrafx.interop.windows/blob/main/sources/Interop/Windows/um/wingdi/BITMAPINFO.cs The key points about this codegen are that:
I think point 1 is fairly self explanatory. It avoids the need to convert the struct when going from managed to native or vice-versa. For 2, this allows the user to directly access the field meaning there is no abstraction on top of it. For 3, this is partially a limitation of the language. The sample I gave above is assuming .NET Core and is utilizing public unsafe ref RGBQUAD this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
fixed (RGBQUAD* p0 = &e0)
{
return ref *(p0 + index);
}
}
} The remaining downside to this it that it doesn't correctly track that the length is actually based on Likewise, the entirety of a variable-sized buffer working is dependent on the containing type ( |
Thanks, @tannergooding. I think I can adapt to what you're advocating for. I can see how it works well for fixed length arrays and how it is an improvement on CsWin32's current codegen, which is: internal partial struct MainAVIHeader
{
internal uint dwMicroSecPerFrame;
internal uint dwMaxBytesPerSec;
internal uint dwPaddingGranularity;
internal uint dwFlags;
internal uint dwTotalFrames;
internal uint dwInitialFrames;
internal uint dwStreams;
internal uint dwSuggestedBufferSize;
internal uint dwWidth;
internal uint dwHeight;
internal unsafe Span<uint> dwReserved
{
get
{
fixed (void *p = &this.__dwReserved)
return new Span<uint>(p, 4);
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct __dwReserved_4
{
private uint _1, _2, _3, _4;
}
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private __dwReserved_4 __dwReserved;
} The case @jnm2 and I have spent most of this thread discussing however is one of variable length arrays where the struct only serves as a sort of header. For such, being on the GC heap is invalid, since the GC moving it around would never move the memory after it. What if we define such structs as |
I think it might be better to annotate them somehow and to construct an analyzer to help catch misuse.
I return a span from all of my own structs so I think it's fine. That being said.... The main "issue" with returning a span referencing a field on the struct is that it is "unsafe". That is, it's the case @jnm2 called out above: #117 (comment) If you have something like: internal unsafe Span<RGBQUAD> bmiColors
{
get
{
fixed (void *p = &this.__bmiColors)
return new Span<RGBQUAD>(p, properLength);
}
} You can still do: var bitmapInfo = new BITMAPINFO();
Span<RGBQUAD> result = bitmapInfo.bmiColors;
return result; and the lifetime of the |
That's a very interesting idea.
Good point. In your case of: public Span<RGBQUAD> AsSpan(int length) => MemoryMarshal.CreateSpan(ref e0, length); what prevents the result from that being kept by the caller beyond the lifespan of the struct itself? How is that better? |
Sure, happy to. Do you want bounds checking? I prefer the syntax Sharplab comparison doesn't seem to add any copies when pinning a struct field: |
I know that The concern was only with |
I think the thing that makes
|
On that point, its existence does allow you to use managed types for |
Sure. Let's start with that. We can go with your inclination as far as syntax, which sounds to be the shorter one, and check with JaredPar later on the end result. Can you include a sample of the generated code in the PR description and then we can invite him to review it. |
@AArnott I implemented everything except the bounds checks, and I can't keep my eyes open so I'll look in tomorrow. I think you should be able to ask Jared to review sans the bounds checks, right? I updated the PR body with the code taken from the actual test. |
Yes, thanks! |
just noticed this, which is exciting: Safe fixed-size buffers |
The case I'm more worried about is the GC holes that this creates. Consider the following: void M() {
record R(__dwReserved_4 Prop);
var r = new R(default);
ref r2 = ref r[0];
Other(ref r2);
} This is unsafe no matter how you implement the indexer. |
There should be no GC hole because you never have a pointer to unpinned memory, you only have a In your scenario, the issue is that var x = new BITMAPINFO();
return x.bmiColors; |
@jaredpar suggested on another thread that we declare extension methods to define a workaround indexer: public static ref int Indexer(this in _theStruct s) {
return // indexer magic here
} The usage pattern then becomes: myStruct.InlineArrayField.Indexer(3) = unmanagedValue;
// which is equivalent to:
myStruct.InlineArrayField._3 = unmanagedValue; At least until this is delivered on, which may or may not work on downlevel runtimes. |
But I'd love for @tannergooding and @jaredpar go at it here till they agree, just in case we can do something better. |
I think @jaredpar's proposal for using an external indexer is fine. It should allow the compiler to correctly track the lifetime and help avoid bugs until the nicer feature can get added. I was just disagreeing about whether or not the issue was a GC hole vs a potential leak to stack memory (which are both undesirable, but I think one is a bit worse 😄) |
@jnm2 How are you feeling at this point? I don't want to abuse your willingness to contribute, and can take over your PR if you would rather not make this design change. But if you want to continue, you're welcome to. It sounds like we'll need to collect extension method declarations into a new collection and emit them into some static class. Maybe named |
@AArnott I'm never sorry if someone takes over and frees me up to work on something else 😁 Bit busy at the moment, so I really ought to say go ahead and take over the PR, but I'll keep it moving if it's still there when I come back to it. |
If you use this code in a .NET 4.x program, you also need to reference the It's a bit weird having to add and distribute the |
Yes you're right. I had that example wrong. There is still a hole here. The returned |
Right, its a stack hole rather than a GC hole; which is still bad; just not corrupt the GC bad 😄 |
@hangy: We want to generate the best APIs available given your compiler version and target framework by default, and adding a System.Memory transitive dependency in our nuget package (when the TFM is at least net45) is how we can accomplish that. For those who don't want to ship System.Memory with their app, you can always add this to your project file: <PackageReference Include="System.Memory" Version="4.5.4" IncludeAssets="none" PrivateAssets="all" /> This should prevent the assembly from being referenced or copied to your bin folder. |
I was going to ask for that next. :-} I'd rather not have System.Memory added by default and have to opt out. I would want to maintain the package reference and version on my own, not tied to the source generator. Opt-out is better than nothing, but it's also convoluted and not obvious that the source generator considers the dependency to be optional. |
To be clear, you can set the version yourself, and you can suppress it.
It doesn't consider it to be optional. If you suppress it, some of our codegen will produce compile errors in your code. |
I would like to ask for it to be optional. That's for another thread perhaps. |
@jnm2 Modifying all the codegen to accommodate a missing System.Memory reference sounds more broadly scoped than this issue, which has now had several conversation forks, etc. So ya, I suggest we get this one taken care of and then if you hit a case where other System.Memory dependencies show up where you don't want, you can open an issue that expresses the high level goal and gives a concrete example of where you can't meet it. |
I'll be the workhorse if that makes it more likely to happen. It comes down to my consuming project being a library vs being an app, and not wanting the net4.8 target to drag along a slow span dependency just to create internal helpers that I will not likely even have a reason to use. Feels wasteful. I'll do as you suggest and follow up after this issue is closed. |
@jnm2 I will take on finishing your current PR, since we have a defect right now in how we do inline arrays I am eager to fix it and it'll close this issue at the same time. |
@AArnott Sounds good. |
This causes the project to fail to build for any target frameworks.
For example, I'm targeting a library to
net35;net48;net5.0-windows
.The text was updated successfully, but these errors were encountered: