In scenarios like reflection or interaction with other languages or platforms, there is not a generically usable and performant way to handle variable number of arbitrary arguments. C# provides syntax like params object[] or params T[] (and might support params Span<T> in the future), but that works best in cases where the parameters are homogenous. ref parameters are especially cumbersome to translate to params, as it requires copying the value back and forth in case the called method modifies it.
Prime targets are Delegate.DynamicInvoke or MethodInfo.Invoke. Here the call could be translated to invoking a method with any number of parameters, normal of ref. Calling a method with a single ref int argument requires 3 heap allocations – one for the array, one for boxing the argument as the input, and one for boxing the argument as the output.
This proposal tries to provide a way to solve this issue, without significant modifications to the runtime, and if that is deemed inappropriate, to instigate discussion about other and better options.
The core of this proposal is a type that is conceptually similar to Span<TypedReference*>, if that were a valid type.
namespace System.Runtime.CompilerServices
{
public unsafe ref struct ByRefSpan
{
readonly void** data; readonly int length;
public ByRefSpan(TypedReference** data, int length) { this.data = data; this.length = length; }
public int Length => length;
public TypedReference this[int index] => *(TypedReference*)data[index];
public ref T Get<T>(int index) => ref __refvalue(this[index], T);
public Type GetType(int index) => __reftype(this[index]);
}
}
This is also an example implementation in C# (if returning TypedReference was possible in case of the indexer). Methods of Span<T> could also be ported to ByRefSpan.
Cooperation from specific languages is needed to make the usage of this type more "beautiful", but this prepares the ground:
void DynamicMethod(ByRefSpan args)
{
args.Get<int>(0) = 1;
args.Get<bool>(1) = true;
args.Get<string>(2) = "1";
}
unsafe void Caller()
{
int arg1 = default; bool arg2 = default; string arg3 = default;
TypedReference arg1ref = __makeref(arg1), arg2ref = __makeref(arg2), arg3ref = __makeref(arg3);
var args = stackalloc void*[3] { &arg1ref, &arg2ref, &arg3ref};
DynamicMethod(new ByRefSpan((TypedReference**)args, 3));
}
In a potential future where C# supports params ByRefSpan, the boilerplate code in Caller could easily be simplified to DynamicMethod(ref arg1, ref arg2, ref arg3);
At first I experimented with simple stackalloc TypedReference[3], but the runtime doesn't track stack space allocated this way (meaning GC ignores any references to objects in it), so until such a change is made (which would enable things like stackalloc (int, string)[10] and more), or until it is possible to have a fixed-size by-value array as a local variable or another way to statically group locals together as a span, individual TypedReference variables must be used and the double reference is needed. In essence, a tuple type like (TypedReference, TypedReference, TypedReference) would be needed.
There is also place for ByRefSpan<T> storing T** in a similar fashion. However, there is no way in C# I am aware of to use such a type, so first something like the internal ByReference<T> has to be exposed.
Another option is to resurrect and improve __arglist, RuntimeTypeHandle and ArgIterator, however those are less efficient and exist only for interaction with unmanaged code. Being able to construct ByRefSpan from RuntimeArgumentHandle would be nice though.
This type is used to store a pointer to potentially stack-allocated data, like Span<T>, but the risks of a value escaping the stack are solved by using ref struct. The only other risks stem from the way this type is used, which would, hopefully, be mitigated by improvements in the language.
Is the core issue related to the language here? I do have to say, the concentration of unsafe code and undocumented keywords in the usage example is higher than most people are perhaps used to, even considering the advanced nature of the API.
Repathing this to the GC, because I'm pretty sure it would require a significant GC overhaul to support arrays / spans of ref structs. @Maoni0 would be able to provide a better analysis.
Tagging subscribers to this area: @Maoni0
Notify danmosemsft if you want to be subscribed.
@SingleAccretion
Realistically, the language team would have to do the majority of the work to make this type widely usable and applicable, but the same could be said for Span<T>. I thought this would be good starting place, as this is a feature every .NET language could make eventually find use of.
Similarly to Span<T>, existing APIs could be extended to support ByRefSpan, like DynamicInvoke etc.
Edit: Note that spans of ref struct would be helpful to have in this case, but it could be implemented as a span of pointers to ref structs (TypedReferences) located on the stack.
Realistically, the language team would have to do the majority of the work to make this type widely usable and applicable, but the same could be said for
Span<T>
Oh, well, you probably already know this, but this makes it extremely unlikely for this to even be considered. The bar for language features is very high, and this one:
Also of note here is that as a platform .NET is kind of moving away from late binding/reflection, so...
Also of note here is that as a platform .NET is kind of moving away from late binding/reflection, so...
That is unfortunate to hear. The reflection API is basically stuck in .NET 1 in its capabilities, everything being either object, object[] or Type[]. More generics, more references, things like #23716 etc. are necessary if you want to do anything serious.
Repathing this to the GC, because I'm pretty sure it would require a significant GC overhaul to support arrays / spans of ref structs
As long as these Span<TypedReference>s live on stack, it does not require any special GC support.
the language team would have to do the majority of the work to make this type widely usable and applicable
Correct, this would need language support to be a safe, widely usable construct. It does not require any special language support if this was an low-level building block that requires you to know the rules to use it safely.
CoreRT has an internal building block like that: https://github.com/dotnet/corert/search?q=LocalVariableSet. Before he left, Ati looked into turning it into a public API as one of the possible strategies to make Reflection Invoke faster.
Then we can spin up an internal prototype perhaps using https://github.com/dotnet/runtime/issues/12832 as a test case. It'll look a bit rough without compiler support, but it should at least tell us whether or not the scenario works.
Most helpful comment
As long as these
Span<TypedReference>s live on stack, it does not require any special GC support.Correct, this would need language support to be a safe, widely usable construct. It does not require any special language support if this was an low-level building block that requires you to know the rules to use it safely.
CoreRT has an internal building block like that: https://github.com/dotnet/corert/search?q=LocalVariableSet. Before he left, Ati looked into turning it into a public API as one of the possible strategies to make Reflection Invoke faster.
Related: https://github.com/dotnet/runtime/issues/26186