Skip to content

An arbitrary array/span of references #38488

Closed
@IS4Code

Description

@IS4Code

Background and Motivation

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.

Proposed API

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.

Usage Examples

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);

Alternative Designs

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.

Risks

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions