-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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]: RuntimeHelpers.Box to create a box around a dynamically-typed byref #97341
Comments
Tagging subscribers to this area: @dotnet/area-system-runtime Issue DetailsBackground and motivationIn interop scenarios where we have unbounded generic APIs that can take blittable or non-blittable types, we can end up in a scenario where we have a generic type If we want to do any thing with the type though (like copy values around, go from a pointer into a buffer to a managed representation of it), we end up needing to use the APIs on We hit this scenario in CsWinRT when we are trying to marshal from WinRT a .NET already has a mechanism to represent a byref with a dynamically-determined type: API Proposalnamespace System;
public ref struct TypedReference
{
+ public TypedReference(ref byte target, RuntimeTypeHandle type);
} API Usageint length = ...;
IntPtr data = ...;
if (data == IntPtr.Zero)
{
return null;
}
var array = new T[length];
var data = (byte*)data.ToPointer();
var abi_element_size = Marshal.SizeOf(AbiType); // TODO: another API proposal to remove this usage of Marshal.SizeOf
for (int i = 0; i < abi.length; i++)
{
var abi_element_ref = new TypedReference(ref *data, AbiType.TypeHandle);
array[i] = Marshaler<T>.FromAbi(abi_element_ref.ToObject());
data += abi_element_size;
}
return array; Alternative DesignsWe could provide another API on We could make this method a static method with an Risks
|
We typically deal with this problem by adding the method somewhere under System.Runtime.InteropServices or System.Runtime.CompilerServices namespace (for example, Should we introduce unsafe getter for the byref as well while we are on it? |
Would that be the same as
Something like this? namespace System.Runtime.CompilerServices;
public static class RuntimeHelpers
{
public static TypedReference CreateTypedReferenceUnsafe(ref byte target, RuntimeTypeHandle type);
public static ref byte GetValueRefUnsafe(TypedReference reference);
} |
Random idea: would it perhaps make sense to add a Ie. namespace System.Runtime.InteropServices;
public static class TypedReferenceMarshal
{
public static TypedReference CreateTypedReference(ref byte target, RuntimeTypeHandle type);
public static ref byte GetValueRef(TypedReference reference);
} |
It would not be the same.
We do not use
This is a question for API review discussion. When there is a choice, we typically avoid introducing a new type with just a few members and add the methods to the existing type instead. For example, |
Unfortunately this API will require Roslyn work first - we cannot have APIs that return |
Well that's quite unfortunate and seems like it'd make this feature much less cheap than expected 🥲 namespace System.Runtime.CompilerServices;
public static class RuntimeHelpers
{
public static object? CreateObjectUnsafe(ref byte target, RuntimeTypeHandle type);
} Like, the whole reason we needed a |
Is the expectation here that |
Yup that's correct! The pointer points to some memory (managed or native, could be either) holding the data for a given So eg. in the example Jeremy posted, we'd use the API like this: for (int i = 0; i < abi.length; i++)
{
array[i] = Marshaler<T>.FromAbi(RuntimeHelpers.CreateObjectUnsafe(ref *data, AbiType.TypeHandle);
data += abi_element_size;
} |
Alright. Based on this statement then, my next assumption would be the memory itself could either be unmanaged (on the native heap) or managed (on the GC heap, pinned of course), but contained no managed references. For example, the following would be an ABI type: // Unmanaged according to C# language rules
struct ABIType
{
public int* Data;
public int Len;
} Where as the following would not: // Managed according to C# language rules
struct NotABIType
{
public int[] Data;
} |
I would expect |
Yup! All the ABI types are either handcrafted in CsWinRT (eg. for |
Exactly. Ultimatley we'd like to end up with something like this in CsWinRT, being fully AOT-safe: var array = new T[length];
var data = (byte*)data.ToPointer();
var abi_element_size = RuntimeHelpers.SizeOf(AbiType.TypeHandle); // 97344
for (int i = 0; i < abi.length; i++)
{
array[i] = Marshaler<T>.FromAbi(RuntimeHelpers.CreateObjectUnsafe(ref *data, AbiType.TypeHandle); // 97341
data += abi_element_size;
} |
I've opened microsoft/CsWinRT#1463 to enable |
It should be called |
Makes sense. And I assume the unsafe is already implied like you said. So, updated API proposal: namespace System.Runtime.CompilerServices;
public static class RuntimeHelpers
{
public static object? Box(ref byte target, RuntimeTypeHandle type);
} |
I would also like to see if we can add a max length. It seems we should be able to compute that is most cases, no? |
I would assume once we also got #97344, callers would have to ensure they do have that size available at the memory area pointed at by Also: should we make that |
In most case, the callers would just to call |
That is a different problem. I am looking at this from the interop perspective. In the proposed use case the input is coming from a native context via a raw pointer. My assumption here is that pointer, when passed to native code, may also come with a length or perhaps some other byte size component. My desire here would be to pass that along not necessarily as a "this is the correct size" but instead as a "the max length to consider is X". This is similar to some native APIs where a max length to consider is provided with a pointer. It doesn't mean the end will be at that length, but it does provide the notion of "if you read past this, you've gone too far". |
This kinda makes me wonder: if the implementation of the method is going to have to validate the number of bytes actually being read against that additional "max number of bytes" parameter, would it make sense to have this method also return some |
That seems reasonable to me and aligns with the desired validation from both the caller and callee I would appreciate. The managed object size is an community request though so I don't think this API can replaced that. |
Just to clarify, are you referring to some other proposal other than #97344? Because if not, Jeremy opened that one as part of the same scenario in CsWinRT, so if eg. we made the API from this proposal also return the number of bytes read, we would have no need for the other one anymore. |
This API is not interop specific. I am looking at this API as a primitive building block that you can use to build interpreters. The CsWinRT use case is replacing a static generic code specialization by dynamic code specialization. It is effectively manually written universal shared generic code - it is slower, but one instance of the code can handle all specializations. |
The notion of managed object size has been around for a long time. See issue #24200. If I recall, there was a COM interface in .NET Framework that I think gave you details on instance size. I would consider #97344 as a special case of the idea.
You and your interpreter examples :) Yeah, I get that. Okay, I accept the proposed shape is a better fit in that case. I retract the size argument suggestion. |
My general concern with using However, I talked with Aaron and he allayed my concerns. We could implement this with a new "UnboxInto" API like the new I'll change the API proposal to the proposed |
If the method is going to take a pointer and a length, should the API then be: namespace System.Runtime.CompilerServices;
public static class RuntimeHelpers
{
+ public static object? Box(ReadOnlySpan<byte> target, RuntimeTypeHandle type);
} |
Yep, but I wanted to discuss the length first prior to going down the span abstraction. I think the length argument has been settled so it isn't needed here. |
I'm unclear on why the length argument is settled - if anything the interpreter use case is another reason to have extra validation. The API is bringing together two unrelated arguments: a chunk of memory and a type descriptor and essentially doing a memcpy of a number of bytes (determined by the type) from the chunk of memory. There's no reason to believe the chunk of memory is big enough. |
This is very unsafe low-level API. Validating the length is not going to make this API safe, but it is going to make it slower and more cumbersome to use. For example, consider how something like If you want to validate the memory size for your case, you can do that by building a helper method that checks the length using API proposed in #97344 and then calls this one. |
Is this ready to be marked |
namespace System.Runtime.CompilerServices;
public static partial class RuntimeHelpers
{
public object? Box(ref byte target, RuntimeTypeHandle type);
} |
Background and motivation
In interop scenarios where we have unbounded generic APIs that can take blittable or non-blittable types, we can end up in a scenario where we have a generic type
T
that has a corresponding typeTAbi
. However, there is no way to representTAbi
as a generic argument, so we have a mechanism to retrieve it as aSystem.Type
instance. This allows us to still reason about the type.If we want to do anything with the type though (like copy values around, go from a pointer into a buffer to a managed representation of it), we end up needing to use the APIs on
System.Runtime.InteropServices.Marshal
that take aSystem.Type
, which are slow and not AOT-compatible.We hit this scenario in CsWinRT when we are trying to marshal from WinRT a
T[]
whereT
is non-blittable. Today CsWinRT uses the APIs on Marshal even though the type will always be blittable as there's no alternative APIs.API Proposal
namespace System.Runtime.CompilerServices; public static class RuntimeHelpers { + public object? Box(ref byte target, RuntimeTypeHandle type); }
API Usage
Alternative Designs
Mentioned below in the collapsable section.
Risks
Minimal risk.
Previous Proposal
API Proposal
namespace System; public ref struct TypedReference { + public TypedReference(ref byte target, RuntimeTypeHandle type); }
API Usage
Alternative Designs
We could provide another API on
RuntimeHelpers
to read and box an unmanaged value-type from a buffer, or even provide a higher level API over aReadOnlySpan<byte>
, like aMemoryMarshal.Read(ref ReadOnlySpan<byte>, System.Type)
method.We could make this method a static method with an
Unsafe
suffix to further describe its unsafeness.Risks
System.TypedReference
is a rarely used type. There may be issues in the runtime implementations around supporting it.The text was updated successfully, but these errors were encountered: