-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Add GCHandle<T> #111307
base: main
Are you sure you want to change the base?
Add GCHandle<T> #111307
Conversation
Note regarding the
|
1 similar comment
Note regarding the
|
public void Dispose() | ||
{ | ||
// Free the handle if it hasn't already been freed. | ||
IntPtr handle = Interlocked.Exchange(ref _handle, IntPtr.Zero); | ||
if (handle != IntPtr.Zero) | ||
{ | ||
GCHandle.InternalFree(handle); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Different from GCHandle.Free
, the Dispose
methods are not throwing. This matches the contract of IDisposable
to allow duplicated dispose calls.
[CLSCompliant(false)] | ||
public static unsafe T* GetAddressOfArrayData<T>(this PinnedGCHandle<T[]?> handle) | ||
=> (T*)handle.GetAddressOfObjectData(); | ||
|
||
/// <summary> | ||
/// Retrieves the address of string data in a <see cref="PinnedGCHandle{T}"/> of <see cref="string"/>. | ||
/// </summary> | ||
/// <returns>The address of the pinned <see cref="string"/>.</returns> | ||
[CLSCompliant(false)] | ||
public static unsafe char* GetAddressOfStringData(this PinnedGCHandle<string?> handle) | ||
=> (char*)handle.GetAddressOfObjectData(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these be nullable oblivious? Given there's no covariance, annotating either with nullable or non-nullable will give warning for the handles annotated with the opposite.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We suggested making these nullable during API review under the assumption that that would allow both without warnings. If that's not actually the case (as in, calling this with a non nullable T
still produces a warning), making these nullable oblivious makes sense to me 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will produce warning for non-nullable T
. PinnedGCHandle<string>
can't be converted to PinnedGCHandle<string?>
because Set(null)
will be warned for the former.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marking nullable oblivious will allow cause oblivious of T?*
and T*
when T
is managed type. I don't think it's a big deal since pointers to managed type is more unsafe and uncommon. Instead top level nullability will bother people more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about only making the parameter nullable oblivious? Would that be doable?
As in:
public static unsafe T* GetAddressOfArrayData<T>(
#nullable disable
this PinnedGCHandle<T[]> handle)
#nullable restore
=> (T*)handle.GetAddressOfObjectData();
So we can preserve the nullability annotation on the returned pointer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I'm not seeing any behavioral difference for type inference and nullability warning, the compiled metadata are different. Agreed that nullable oblivious range should be as short as possible.
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.T.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a few notes based on previous discussions 🙂
Thank you for taking this one!
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.T.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/PinnedGCHandle.T.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/PinnedGCHandle.T.cs
Outdated
Show resolved
Hide resolved
get | ||
{ | ||
IntPtr handle = _handle; | ||
GCHandle.ThrowIfInvalid(_handle); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All these checks should be removed. Using any instance method on an instance that's not allocated is explicitly not supported. As long as we can throw an exception (eg. this should throw NRE) so the behavior is still consistent, that's sufficient.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as we can throw an exception (eg. this should throw NRE)
This is the questionable part - the current implementation uses FCall in some cases, and the ability to throw exception from FCall will soon be removed. In other words, no NRE can be thrown, at least for some members.
With that said, providing invalid value via FromIntPtr
will still cause hard AV in unmanaged code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least for the getter, on CoreCLR and NAOT this should just be *(T*)_handle
, which should already throw NRE implicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe an implicit read (_ = *(byte*)value
) can be applied with lowest overhead? I'm not worried about garbage value from FromIntPtr
, but make new GCHandle<T>{ Target = obj }
crashing doesn't look good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by implicit read? You need to read anyway, since you're returning a value. What I'm saying is the whole getter should literally just be get => *(T*)_handle
. That will automatically NRE if handle
is null
, which is all we need.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean adding a read in managed code in setter. Currently the entire setter is executed in unmanaged code and will cause AV, which won't be caught as NRE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see what you mean, I thought you were talking about the other accessor. That would seem fine to me, but I'll defer to Jan and others to confirm 🙂
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.T.cs
Outdated
Show resolved
Hide resolved
/// <summary> | ||
/// Provides extension methods to operate with GC handles. | ||
/// </summary> | ||
public static class GCHandleExtensions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are these extension methods? They are defined in the same namespace in the same assembly. I don't understand the utility for these being extensions methods in this case as opposed to member/static functions on the actual type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I imagine it's so that they can look like instance members, but only apply to specific T
s (currently a T[]
or string
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's used effectively as generic specialization. Of course with proper generic specialization we can avoid dealing with nullability of generic parameter of this
.
IntPtr handle = _handle; | ||
GCHandle.ThrowIfInvalid(handle); | ||
|
||
return GCHandle.AddrOfPinnedObjectFromHandle(_handle); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this expected to follow GCHandle in special casing strings and arrays here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Memory<T>.Pin
and fixed
are all following the contract to return the address of first element. Not following the contract would require strong justification.
#nullable disable // Nullable oblivious because no covariance between PinnedGCHandle<T> and PinnedGCHandle<T?> | ||
this PinnedGCHandle<T[]> handle) | ||
#nullable restore | ||
=> (T*)handle.GetAddressOfObjectData(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should those be implemented separately here to avoid runtime checks GCHandle does for performance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Yes the check can't be eliminated here.
{ | ||
get | ||
{ | ||
GCHandle.CheckUninitialized(_handle); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How come this check is here, and also why are we using Unsafe.As
below? I thought we said this entire get accessor could just be get => *(T*)_handle
while preserving the same implicit semantics?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Place CheckUninitialized
under #if MONO || DEBUG
ifdef here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Under release, the two read operations (against the same copy of value) will be combined by JIT:
Coreclr provides additional check for Debug build and I'd like it to be preserved. For Release there won't be duplicated read.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using get => *(T*)_handle
would require to define another series of entries for Debug and Mono. Using Unsafe.As
allows reusing the InternalGet
methods with zero overhead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the problem for that. GCHandle
also has a couple ifdefs for release, and a different impl for Mono.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's unnecessary to introduce extra complexity when the duplicated read can go away in Release.
src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.T.cs
Outdated
Show resolved
Hide resolved
/// <summary> | ||
/// Releases this <see cref="GCHandle{T}"/>. | ||
/// </summary> | ||
public void Dispose() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we just use /// <inheritdoc/>
on all these implemented interface methods, rather than duplicating all the XML docs from those APIs manually? Applies to all implemented interface methods, overridden methods, in all 3 types in the PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure whether it works fine under the documentation requirements. For some members like operators there's no source to inherit (expect the IEqualityOperator ones, which isn't implemented by these types). For some members like Dispose
the documentation lives in api-docs but not this repo, and I'm not sure how tooling will process the <inheritdoc/>
tag. Some docs are "templated" and filled with type names.
Closes #94134.
IEquatable<T>
and equality operators are included.