Skip to content
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

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open

Add GCHandle<T> #111307

wants to merge 22 commits into from

Conversation

huoyaoyuan
Copy link
Member

@huoyaoyuan huoyaoyuan commented Jan 11, 2025

Closes #94134.

IEquatable<T> and equality operators are included.

Copy link

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

1 similar comment
Copy link

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.

@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Jan 11, 2025
Comment on lines 88 to 96
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);
}
}
Copy link
Member Author

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.

Comment on lines 18 to 28
[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();
Copy link
Member Author

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.

Copy link
Contributor

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 🤔

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Contributor

@Sergio0694 Sergio0694 Jan 11, 2025

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?

Copy link
Member Author

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.

Copy link
Contributor

@Sergio0694 Sergio0694 left a 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!

get
{
IntPtr handle = _handle;
GCHandle.ThrowIfInvalid(_handle);
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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 🙂

@MihaZupan MihaZupan added this to the 10.0.0 milestone Jan 11, 2025
/// <summary>
/// Provides extension methods to operate with GC handles.
/// </summary>
public static class GCHandleExtensions
Copy link
Member

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.

Copy link
Member

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 Ts (currently a T[] or string).

Copy link
Member Author

@huoyaoyuan huoyaoyuan Jan 11, 2025

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);
Copy link
Contributor

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?

Copy link
Member Author

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();
Copy link
Contributor

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?

Copy link
Member Author

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);
Copy link
Contributor

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?

Copy link
Member

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

@huoyaoyuan huoyaoyuan Jan 12, 2025

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.

/// <summary>
/// Releases this <see cref="GCHandle{T}"/>.
/// </summary>
public void Dispose()
Copy link
Contributor

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.

Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[API Proposal]: GCHandle<T> (like GCHandle, but this time it's great™️)
6 participants