-
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
[API Proposal]: GCHandle<T> (like GCHandle, but this time it's great™️) #94134
Comments
Tagging subscribers to this area: @dotnet/area-system-runtime Issue DetailsThis API proposal is extracted from #94113 (comment) and supersedes/fixes:
OverviewThe
We have a reference implementation which can be a starting point for this. cc. @jkotas @AaronRobinsonMSFT @tannergooding API Proposalnamespace System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct UnsafeGCHandle : System.IDisposable
{
public static UnsafeGCHandle Alloc(object? value, GCHandleType type = GCHandleType.Normal);
public readonly bool IsAllocated { get; }
public object? Target { readonly get; set; }
public readonly unsafe IntPtr GetAddrOfPinnedObject();
public void Dispose();
} Open questions
API UsageSame as // In a constructor (or wherever)
this.handle = UnsafeGCHandle.Alloc(myObj);
// Later on
object? target = this.handle.Target(); Alternative DesignsThe alternative would be to change RisksNot really any risk, since this would be a brand new type.
|
Tagging subscribers to this area: @dotnet/interop-contrib Issue DetailsThis API proposal is extracted from #94113 (comment) and supersedes/fixes:
OverviewThe
We have a reference implementation which can be a starting point for this. cc. @jkotas @AaronRobinsonMSFT @tannergooding API Proposalnamespace System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct UnsafeGCHandle : System.IDisposable
{
public static UnsafeGCHandle Alloc(object? value, GCHandleType type = GCHandleType.Normal);
public readonly bool IsAllocated { get; }
public object? Target { readonly get; set; }
public readonly unsafe IntPtr GetAddrOfPinnedObject();
public void Dispose();
} Open questions
API UsageSame as // In a constructor (or wherever)
this.handle = UnsafeGCHandle.Alloc(myObj);
// Later on
object? target = this.handle.Target(); Alternative DesignsThe alternative would be to change RisksNot really any risk, since this would be a brand new type.
|
Unnecessary.
It should be a constructor (same as what we have done for
This method needs to do several checks against the type of the object. It may be better to have
|
Updated the proposal integrating your suggestions 🙂 For Any thoughts on whether we can just make |
I assume that this API would be a shorthand for
Yes. I think the idea is that this is maximally unsafe type. |
Should
Given we have and allow public static object* CreateGCHandle(object? value, GCHandleType type = GCHandleType.Normal);
public static void DisposeGCHandle(object* value); Given these two APIs, a user could trivially create their own unsafe wrapper with whatever name they wanted. They could still convert to and store it as |
I was first a bit skeptical of going as far as exposing I'm not sure they belong on namespace System.Runtime.InteropServices;
public struct GCHandle
{
public static object* CreateUnsafe(object? value, GCHandleType type = GCHandleType.Normal);
public static void DisposeUnsafe(object* value);
} ? Eg. in ComputeSharp I have this field. Would certainly be much more readable if it could just be Additional benefit: after you initially cast the pointer type, you can also preserve nullability annotations. |
Noting that using Alternative names than |
From/ToIntPtr returns opaque handle.
|
Has there been any consideration to making the handle struct generic, to make it more type-safe and ergonomic? |
Well, alright then we can ignore the whole
I've updated the API shape integrating all the new suggestions:
Is there anything else we might be missing? The API shape looks pretty nice to me now? 🙂 Random thought: should the to/from methods for Like, just spitballing here, maybe: public static UnsafeGCHandle FromOpaqueHandle(nint value);
public static nint ToOpaqueHandle(UnsafeGCHandle value); Or does this just not matter and it's better to keep that naming scheme for historic reasons maybe? |
We have approved and introduced more APIs that follow the FromIntPtr/ToIntPtr pattern just a few months ago #67090 (comment) |
Ah, cool, well then the API proposal should be pretty much ready 😄 |
On a slightly related note: it's a shame that there's we can't express |
For GetStringDataPointer, should it return Also, if we were to go the generic route, then we could limit GetStringDataPointer and GetArrayDataPointer to only being on handles of valid types by implementing them as extension methods. |
Making |
Is GetArrayDataPointer going to work for both single and multidimensional arrays (that comes with a small perf penalty) or is GetArrayDataPointer going to work for single dimensional arrays only? |
Mmhh... I was assuming that it'd roughly be the same as Alternatively, if (1) the overhead for the general MD logic was considered too much for the SZ case, and (2) we did want to have built-in helpers for this in general, should we consider just having a separate API that'd work in all cases? As in, spitballing: public readonly void* GetSZArrayDataPointer();
public readonly void* GetArrayDataPointer(); (I will say I can't really come up with a good name for these two here though) |
I think that getting rid of the pointer getters makes the most sense here, the users can already achieve then with Unsafe.As anyway. |
I assume the intent is to make it a bit nicer. I mean yes you can do it manually but it's a bit ugly: // String
char* p = (char*)Unsafe.AsPointer(ref Unsafe.AsRef(in Unsafe.As<string>(handle.Target!).GetPinnableReference()));
// SZ array
T* p = (T*)Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(Unsafe.As<T[]>(handle.Target!)));
// MD array
T* p = (T*)Unsafe.AsPointer(ref MemoryMarshal.GetArrayDataReference(Unsafe.As<Array>(handle.Target!))); I mean it's both ugly and also not really the ideal code to have to write manually due to how "questionable" it is. Related question — should these APIs return |
To follow the idea of maximally unsafe type idea, these APIs should throw NullReferenceException when the target is null. (Same as MemoryMarshal.GetArrayDataReference throws NullReferenceException for null.) |
Sounds good! So it seems the only open question is the naming? Should I just leave those names as placeholders and then we can just bikeshed those during API review, as long as the API shape itself is fine? To recap, that'd basically be, for those 3: public readonly void* GetSZArrayDataPointer();
public readonly void* GetArrayDataPointer();
public readonly char* GetStringDataPointer(); Actually I have a question on Otherwise, perhaps we could just have |
Generic wrapper would address this nicely. Note that you can always use |
@jkoritzinsky idea was to make |
Oh, right. So something like this? namespace System.Runtime.InteropServices;
public struct GCHandle<T> : System.IDisposable
where T : class
{
public GCHandle(T? value, GCHandleType type = GCHandleType.Normal);
public readonly bool IsAllocated { get; }
public T? Target { readonly get; set; }
public static UnsafeGCHandle FromIntPtr(IntPtr value);
public static IntPtr ToIntPtr(UnsafeGCHandle value);
public void Dispose();
}
public static class GCHandleExtensions
{
public static T* GetArrayDataPointer<T>(this GCHandle<T[]> handle);
public static void* GetArrayDataPointer(this GCHandle<Array> handle);
public static char* GetStringDataPointer(this GCHandle<string> handle);
} |
To be clear, I meant that the generic parameter should be the type of the object being retained, i.e.
Annoyingly, we can't use Side note, I might be in the minority here, but I feel like having these |
|
Yeah I was just not really sure how to express that one otherwise 😅 About the type being generic, are we worried at all about the fact it would be "inconsistent" with |
Hey folks! Trying to revive this now that .NET 9 is done, perhaps we can make this happen in 10? 😄 To recap, the current API proposal integrating all the previous comments would be this: namespace System.Runtime.InteropServices;
public struct GCHandle<T> : System.IDisposable
where T : class
{
public GCHandle(T? value, GCHandleType type = GCHandleType.Normal);
public readonly bool IsAllocated { get; }
public T? Target { readonly get; set; }
public static GCHandle<T> FromIntPtr(IntPtr value);
public static IntPtr ToIntPtr(GCHandle<T> value);
public void Dispose();
}
public static class GCHandleExtensions
{
public static T* GetArrayDataPointer<T>(this GCHandle<T[]> handle);
public static char* GetStringDataPointer(this GCHandle<string> handle);
} If this looks good, perhaps we could mark this as ready to review and go from there? 🙂 |
I am not sure about |
public static GCHandle<T> FromIntPtr(IntPtr value);
public static IntPtr ToIntPtr(GCHandle<T> value); Are we considering Trying to understand if the intent is to have |
I thought the intent there was to make things more convenient in the spirit of "maximally unsafe use" you mentioned? 😅 |
My thought is that we can deprecate |
Non-generic GCHandle uses lower bit of the handle to indicate whether the handle is pinned. This proposal wants to remove this bit to reduce overhead per the top post. It means that IntPtrs from
I made that comment when the proposal was non-generic |
Right. This is an important semantic detail we need to discuss during API review. |
Perhaps they should be |
That is possible, but that shouldn't change the conversation much. There will be an inherent confusion if something named Do we have another API where one is generic, the other not, and they are fundamentally incompatible? |
Just replying to this point, this makes me wonder whether perhaps the original non-generic Other proposal, for context: public struct UnsafeGCHandle : System.IDisposable
{
public UnsafeGCHandle(object? value, GCHandleType type = GCHandleType.Normal);
public readonly bool IsAllocated { get; }
public object? Target { readonly get; set; }
public static UnsafeGCHandle FromIntPtr(IntPtr value);
public static IntPtr ToIntPtr(UnsafeGCHandle value);
public readonly void* GetArrayDataPointer();
public readonly void* GetStringDataPointer();
public void Dispose();
} We might even call those two |
The |
I like the generic
Prior art for naming methods that do stuff like this is |
Sounds good! Updated API proposal then, for context: namespace System.Runtime.InteropServices;
public struct GCHandle<T> : System.IDisposable
where T : class
{
public GCHandle(T? value, GCHandleType type = GCHandleType.Normal);
public readonly bool IsAllocated { get; }
public T? Target { readonly get; set; }
public static GCHandle<T> FromIntPtr(IntPtr value);
public static IntPtr ToIntPtr(GCHandle<T> value);
public void Dispose();
}
public static class GCHandleExtensions
{
public static T* UnsafeAddrOfPinnedArrayElement<T>(this GCHandle<T[]> handle);
public static char* UnsafeAddrOfPinnedString(this GCHandle<string> handle);
} Something like this? I suppose the last one might be "OfPnnedStringData" or something similar as well, possibly. |
We have |
Going back to this now that .NET 10 planning is starting 🙂
I personally like this one better, I feel like the "Data" suffix makes it clear that it's giving you a pointer to the raw object data. Revised API proposal: namespace System.Runtime.InteropServices;
public struct GCHandle<T> : System.IDisposable
where T : class
{
public GCHandle(T? value, GCHandleType type = GCHandleType.Normal);
public readonly bool IsAllocated { get; }
public T? Target { readonly get; set; }
public static GCHandle<T> FromIntPtr(IntPtr value);
public static IntPtr ToIntPtr(GCHandle<T> value);
public void Dispose();
}
public static class GCHandleExtensions
{
public static T* UnsafeAddrOfPinnedArrayData<T>(this GCHandle<T[]> handle);
public static char* UnsafeAddrOfPinnedStringData(this GCHandle<string> handle);
} Let me know if this looks reasonable and/or if we need any more tweaks to get this ready to review 🙂 |
This will also benefit |
This API proposal is extracted from #94113 (comment) and supersedes/fixes:
Overview
The
GCHandle
type currently has a number of issues/inefficiencies, that are impossible to completely fix without introducing some (major) breaking changes, which could cause all sorts of problems for people having a dependency on the current behaviors. It would be beneficial to instead add a new handle type, which solves all of these problems from the start:Free
IDisposable
We have a reference implementation which can be a starting point for this.
cc. @jkotas @AaronRobinsonMSFT @tannergooding
API Proposal
Open questions
I'm assuming that theTarget
getter can be updated to just do*(object*)_handle
, hence being as fast as possible already (ie. not needing that internal call thatGCHandle
has in its current implementation). If that's the case, then there's no need to add a separateGetTargetUnsafe()
method —Target
would already be as fast as it can be from the start.Should we consider using "better" names, since this is a new API? For instance:TheAlloc
abbreviation is against the naming convention. Should it beAllocate
? Or maybeCreate
?Same forGetAddrOfPinnedObject()
, should it beGetAddressOfPinnedObject()
? Or maybe justPinnedObjectAddress
?Since we said the type should implementIDisposable
, I've just addedDispose()
. Do we also want aFree()
method?Shold we includeFromIntPtr(IntPtr)
andToIntPtr()
as well?API Usage
Same as
GCHandle
, really:Alternative Designs
The alternative would be to change
GCHandle
, but as mentioned, it seems impossible to fix all issues with back-compat.Risks
Not really any risk, since this would be a brand new type.
The text was updated successfully, but these errors were encountered: