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 40 commits into
base: main
Choose a base branch
from
Open

Add GCHandle<T> #111307

wants to merge 40 commits into from

Conversation

huoyaoyuan
Copy link
Member

@huoyaoyuan huoyaoyuan commented Jan 11, 2025

Closes #94134. IEquatable<T> 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
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.

Copy link
Member

Choose a reason for hiding this comment

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

This method should be only used for non-array non-string blittable types. Memory<T>.Pin and fixed do not work for non-array non-string blittable types.

I think it would make sense to make it work just for the non-array non-string blittable types and document the behavior as undefined to avoid unnecessary overhead. The overall goal with these new GCHandle types was to eliminate unnecessary overheads.

The original intention with this method was to provide a replacement for GCHandle.AddrOfPinnedObject. I think the method has different enough name to have different behavior.

#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.

/// <exception cref="NullReferenceException">If the handle is not initialized or already disposed.</exception>
[CLSCompliant(false)]
public static unsafe T* GetAddressOfArrayData<T>(
#nullable disable // Nullable oblivious because no covariance between PinnedGCHandle<T> and PinnedGCHandle<T?>
Copy link
Member

Choose a reason for hiding this comment

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

@stephentoub Is #nullable disable correct here?

Copy link
Member

@stephentoub stephentoub Jan 13, 2025

Choose a reason for hiding this comment

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

That will make the parameter nullable oblivious, which appears to be the goal.

And... that's desirable because we want to accept both nullable and non-nullable arrays as the type arg?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah that was the intent here. When I suggested making the T nullable in API review I assumed that'd have allowed also passing a non nullable T just fine, but apparently that also warns, because reasons. So we figured just making this nullable oblivious would fix that. And keeping that restricted to the parameter means at least you preserve and flow the nullability to the returned T, rather than making the whole method nullable oblivious.

{
//
// The delegate might already been garbage collected
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// The delegate might already been garbage collected
// The delegate might have already been garbage collected

@@ -211,7 +211,6 @@ public MetadataReader Reader

byte[] metadataArrayTemp = _currentBinaryEmitter.EmitToMetadataBlob();
byte[] metadataArray = GC.AllocateArray<byte>(metadataArrayTemp.Length, pinned: true);
System.Runtime.InteropServices.GCHandle.Alloc(metadataArray, System.Runtime.InteropServices.GCHandleType.Pinned);
Copy link
Member

@AaronRobinsonMSFT AaronRobinsonMSFT Jan 13, 2025

Choose a reason for hiding this comment

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

This seems a bit odd given it isn't stored anywhere, but it is also going to be creating a side effect. Do we know if this is okay to remove?

Copy link
Member Author

Choose a reason for hiding this comment

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

The array is pinned in a line above. This handle would be leaked permanently.

Copy link
Member

Choose a reason for hiding this comment

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

Is it guaranteed that the array is kept alive for as long as the unmanaged pointer into the array is in use (the unmanaged pointer is acquired a few lines below)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Realized it's not that simple. The handle is also keeping the array alive.
I'd like to leave it as-is and address in the future. The usages of GCHandle and PEReader don't look consistent in R2R.

Copy link
Contributor

Choose a reason for hiding this comment

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

Could it just be a normal handle instead though?

Copy link
Member Author

Choose a reason for hiding this comment

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

That won't make any difference.

/// or <see cref="GCHandleExtensions.GetAddressOfStringData(PinnedGCHandle{string})"/> instead.
/// </para>
/// <para>
/// This method should only be used for blittable types. The layout of non-blittable types is undefined for interop.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// This method should only be used for blittable types. The layout of non-blittable types is undefined for interop.
/// This method should only be used for blittable types.

Copy link
Member

Choose a reason for hiding this comment

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

Please commit this update.

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 unsure about what to express to users here. Should "layout is not defined" be mentioned here? Is it discussed in interop documentation, can it be linked from this documentation?

Copy link
Member

@AaronRobinsonMSFT AaronRobinsonMSFT Jan 15, 2025

Choose a reason for hiding this comment

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

Should "layout is not defined" be mentioned here?

Nope. This is unnecessary for documentation on GCHandle. If the intent here is to limit this to blittable types, unclear if that is a hard requirement, then state that. The layout of non-blittable types is possible to be defined too. One can apply the Sequential to a class with object fields and its layout would be "defined".

What is the intent of this method? State what it is intended to do.

Copy link
Member

Choose a reason for hiding this comment

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

One can apply the Sequential to a class with object fields and its layout would be "defined".

This makes the layout of the marshalled view defined. The layout of the pinned type is undefined.

Maybe this should say "The layout of pinned non-blittable types is undefined."?

Copy link
Contributor

@hamarb123 hamarb123 Jan 15, 2025

Choose a reason for hiding this comment

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

My understanding is that this API basically returns Unsafe.AsPointer(ref RuntimeHelpers.GetRawData(obj)) *, where RuntimeHelpers.GetRawData is an API that is yet to be exposed publically, but returns a managed reference to the "first field" in the type (*: it is possible that we would make a publically exposed RuntimeHelpers.GetRawData point to the first character of a string or first element in an array, instead of to some length/s, as suggested in the API proposal currently #28001).

In the case of a blittable type, this layout will match what you would get from marshalling. For unmanaged, non-blittable types, it will not necessarily match what you get from marshalling (depending on something like DisableRuntimeMarshallingAttribute I think); however this can still presumably be used from native if the actual layout matches what you have. (note: in these cases, it's still usually only useful for interop with something that is Sequential layout obviously - you could imagine uses with Auto layout still though, e.g., you might have a sequential layout field in an auto layout type & you add offset to that field & pass that pointer instead)

The layout of a type is undefined if its fields are not unmanaged, but can still be meaningfully interacted with if we had something like #94976 (and can be calculated today, I just don't think we guarantee that field offsets don't change, nor that fields are stored within the object, so it's UB for now, but probably/hopefully not forever).

So, in conclusion, I would think that a comment like this is probably most accurate/useful: This method returns a pointer to the first field of the managed layout of the type. The unmanaged layout of the type is only guaranteed to match the managed layout for blittable types. This does skip some nuance with explicit layout & stuff like DisableRuntimeMarshallingAttribute, but we presumably don't want to go into every single bit of detail here(?). Even that is potentially a bit much detail, which is why This method should only be used for blittable types. was done presumably, but I personally think this is a bit too little detail.

Copy link
Member Author

Choose a reason for hiding this comment

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

For my understandings:

Only types with true-sequential/manual layout in managed memory should be used with this API. The managed layout is largely an implementation detail, and only blittable value type are guaranteed to have the same layout in unmanaged and managed memory, thus following the expectation in [StructureLayout].

There have been multiple questions about this and I think it worth a dedicated article to discuss the guarantees. For this member, it should focus on a clear definition of supported types.

Is "blittable value types" clear enough for this purpose?

Copy link
Contributor

Choose a reason for hiding this comment

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

Is "blittable value types" clear enough for this purpose?

While blittable types (including boxed structs & classes, with sequential layout) are probably the only real uses when performing interop, presumably PinnedGCHandle<T> can be used for other cases & GetAddressOfObjectData could be useful there, so I don't think this is a useful definition, unless it's made clear that it is only a relevant consideration when passing the pointer to native for something like interop. I think the comment I have in my comment is probably more encompassing of all possible uses of the pointer & more accurate.

Copy link
Member

Choose a reason for hiding this comment

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

This method returns a pointer to the first field of the managed layout of the type.

I do not think that this valid statement. For example, if you have a type with a single field added via Edit and Continue, this API is not going to return a pointer to the first field.

Is "blittable value types" clear enough for this purpose?

I do not think so. The primary use case for this API are blittable reference types. Reference types are not value types.

I think it should say "blittable types"

presumably PinnedGCHandle can be used for other cases & GetAddressOfObjectData could be useful there

This is complex uncharted territory. I do not think we want to be defining as side-effect of introducing this API.

I know that this API is controversial. It is why it was not included in the original proposal. I think omitting it from this PR would be a perfectly fine option.

Copy link
Member

Choose a reason for hiding this comment

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

I have committed the update. I agree with Aaron that "undefined for interop" note was unnecessary here.

If we wanted to link to a doc with more information, I think it would be https://learn.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types .

@@ -734,7 +734,6 @@ private async Task CopyToAsyncInternal(Stream destination, int bufferSize, Cance
finally
{
CryptographicOperations.ZeroMemory(rentedBuffer.AsSpan(0, bufferSize));
Copy link
Member

Choose a reason for hiding this comment

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

This is now slightly extending the lifetime of the handle. These crypto APIs have been written with specific intent. @bartonjs or @vcsjones Are you okay with this change in lifetime of the GCHandle?

Copy link
Member

Choose a reason for hiding this comment

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

I don't think the lifetime extension is a big concern. The point of the pin is to make sure the GC doesn't copy it around (move it). As long as the ZeroMemory happens before the pin is let go, I think it's fine. However:

  1. It seems a little odd to return a rented buffer while it is still pinned.
  2. System.Security prefers using explicit scopes for using.

Copy link
Member

Choose a reason for hiding this comment

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

Or, more specifically, since there is a brief window of time while a pinned buffer has been returned to the pool, what happens if thread A returns a pinned buffer, thread B rents it and tries to pin it, then thread A unpins it?

It seems like the most sensible thing to do is just make sure it unpinned before it gets returned.

Copy link
Member Author

Choose a reason for hiding this comment

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

Shouldn't multiple pinning handles working simultaneously? In other words, when there's at least 1 pinning handle, the array should be pinned.
Agree that the pinning period should just over clearing.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, this should just undo the using and keep the Free/Dispose where it was. It's already a try/finally, we don't need a try/finally wrapping the try/finally.

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 multiple variables would also create nested try/finally. It should be acceptable pattern.

Copy link
Member

Choose a reason for hiding this comment

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

@huoyaoyuan Please revert this change by the request of the code owner, @bartonjs.

Copy link
Member

Choose a reason for hiding this comment

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

The using specifically. I think the nullable change is appropriate.

@jkotas jkotas requested a review from Copilot January 13, 2025 19:26

Choose a reason for hiding this comment

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

Copilot reviewed 10 out of 25 changed files in this pull request and generated no comments.

Files not reviewed (15)
  • src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj: Language not supported
  • src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems: Language not supported
  • src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/UnsafeGCHandle.cs: Evaluated as low risk
  • src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Unix.cs: Evaluated as low risk
  • src/coreclr/tools/aot/ILCompiler.ReadyToRun/TypeSystem/Mutable/MutableModule.cs: Evaluated as low risk
  • src/libraries/System.Private.CoreLib/src/System/Gen2GcCallback.cs: Evaluated as low risk
  • src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs: Evaluated as low risk
  • src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventProvider.cs: Evaluated as low risk
  • src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs: Evaluated as low risk
  • src/coreclr/nativeaot/System.Private.TypeLoader/src/Internal/Runtime/TypeLoader/TypeSystemContextFactory.cs: Evaluated as low risk
  • src/coreclr/nativeaot/System.Private.CoreLib/src/System/GC.NativeAot.cs: Evaluated as low risk
  • src/coreclr/System.Private.CoreLib/src/System/GC.CoreCLR.cs: Evaluated as low risk
  • src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventPipeEventProvider.cs: Evaluated as low risk
  • src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/InteropServices/PInvokeMarshal.cs: Evaluated as low risk
  • src/libraries/System.Private.CoreLib/src/System/IO/PinnedBufferMemoryStream.cs: Evaluated as low risk
Comments suppressed due to low confidence (3)

src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/PinnedGCHandle.T.cs:86

  • Ensure that the GetAddressOfObjectData method is adequately tested, especially for blittable types.
public readonly unsafe void* GetAddressOfObjectData()

src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/PinnedGCHandle.T.cs:125

  • Verify that the Dispose method is properly tested to ensure that resources are correctly released.
public void Dispose()

src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/GCHandle.T.cs:106

  • Add a test case to verify the correctness of the Equals method for GCHandle.
public readonly bool Equals(GCHandle<T> other) => _handle == other._handle;
{
//
// The delegate might already been garbage collected
Copy link
Member

@stephentoub stephentoub Jan 13, 2025

Choose a reason for hiding this comment

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

"might have"... what situation would result in TryGetTarget being false here but because of some reason other than GC?

Copy link
Member Author

Choose a reason for hiding this comment

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

The comment was copied from old one. Maybe it's saying in the context of the whole method?

/// <param name="target">The object that uses the <see cref="GCHandle{T}"/>.</param>
public GCHandle(T target)
{
_handle = GCHandle.InternalAlloc(target, GCHandleType.Normal);
Copy link
Member

Choose a reason for hiding this comment

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

Since we rely on IntPtr.Zero elsewhere in the implementation having special meaning, worth asserting here that _handle != IntPtr.Zero?

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 the assert go to InternalAlloc since there are more types depending on this?

// Pin the array for security.
GCHandle pinHandle = GCHandle.Alloc(rentedBuffer, GCHandleType.Pinned);
using PinnedGCHandle<byte[]> pinHandle = new PinnedGCHandle<byte[]>(rentedBuffer);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
using PinnedGCHandle<byte[]> pinHandle = new PinnedGCHandle<byte[]>(rentedBuffer);
PinnedGCHandle<byte[]> pinHandle = new PinnedGCHandle<byte[]>(rentedBuffer);

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've reverted all changes in CryptoStream.

@@ -734,7 +734,6 @@ private async Task CopyToAsyncInternal(Stream destination, int bufferSize, Cance
finally
{
CryptographicOperations.ZeroMemory(rentedBuffer.AsSpan(0, bufferSize));
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
CryptographicOperations.ZeroMemory(rentedBuffer.AsSpan(0, bufferSize));
CryptographicOperations.ZeroMemory(rentedBuffer.AsSpan(0, bufferSize));
pinHandle.Dispose();

@huoyaoyuan
Copy link
Member Author

Any further review on this?
I'm going to OOF for holidays starting from next Wednesday. Please feel free to directly update the branch if you need to drive this.

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™️)