diff --git a/accepted/2022/ReflectionInvoke.md b/accepted/2022/ReflectionInvoke.md new file mode 100644 index 000000000..fa425074d --- /dev/null +++ b/accepted/2022/ReflectionInvoke.md @@ -0,0 +1,512 @@ +# Reflection Invoke for 8.0 (draft / in progress) +#### Steve Harter +#### April 26, 2023 + +# Background +For additional context, see: +- [Developers using reflection invoke should be able to use ref struct](https://github.com/dotnet/runtime/issues/45152) +- [API issue (also the original issue)](https://github.com/dotnet/runtime/issues/10057) + +The invoke APIs and capabilities have essentially remained the same since inception of .NET Framework with the primary API being [`MethodBase.Invoke`](https://learn.microsoft.com/dotnet/api/system.reflection.methodbase.invoke): +```cs +public object? Invoke(object? obj, object?[]? parameters); + +public object? Invoke( + object? obj, + BindingFlags invokeAttr, + Binder? binder, + object?[]? parameters, + CultureInfo? culture); +``` +which is implemented by [MethodInfo](https://learn.microsoft.com/dotnet/api/system.reflection.methodinfo) and [ConstructorInfo](https://learn.microsoft.com/dotnet/api/system.reflection.constructorinfo). + +Properties expose their `get` and `set` accessors from `PropertyInfo` via [`GetMethod()`](https://learn.microsoft.com/dotnet/api/system.reflection.propertyinfo.getmethod) and [`SetMethod()`](https://learn.microsoft.com/dotnet/api/system.reflection.propertyinfo.setmethod). + +Unlike properties, fields do not expose `MethodInfo` acessors since since there is no invokable code around field access. Instead, fields expose their `get` and `set` accessors from [`object? FieldInfo.GetValue(object? obj)`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.getvalue) and [`FieldInfo.SetValue(object? obj, object? value)`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.setvalue). + +The `Invoke()` APIs are easy to use and flexible: +- Based on `System.Object` to support both reference types (as a base type) and value types (through boxing). + - Note that boxing does an automatic `Nullable` to `null`. +- Automatic conversions: + - Implicit casts between primitives such as from `int` to `long`. + - `Enum` to / from its underlying type. + - Pointer (*) types to / from `IntPtr`. + +However the object-based Invoke() is not very performant: +- Boxing is required for value types. + - A box requires a heap allocation and associated GC overhead. + - A cast is required during unbox, although unbox is fast since it doesn't allocate. +- An `object[]` must be allocated (or manually cached by the caller) for the `parameters` argument. +- The automatic conversions add overhead. +- `ref` and `out` parameters require overhead after the invoke due to re-assignment (or "copy back") to the `parameters` argument. + - `Nullable` is particularly expensive due to having to convert the boxed `null` to `Nullable` in order to invoke methods with `Nullable`, and when used with `ref` / `out`, having to box `Nullable` after invoke to "copy back" to the `parameters` argument. +- The additional invoke arguments (`BindingFlags`, `Binder`, `CultureInfo`) add overhead even when not used. Plus using those is quite rare with the exception of `BindingFlags.DoNotWrapExceptions`, which is the proposed behavior for the new APIs proposed here. + +and has limitations and issues: +- Cannot be used with byref-like types like `Span` either as the target or as an argument. This is because by-ref like types cannot be boxed. **This is the key limitation expressed in this document.** +- `ref` and `out` parameters are retrieved after `Invoke()` through the `object[] parameters` argument. This is a manual mechanism performed by the user and means there is no argument or return value "aliasing" to the original variable. +- Boxing of value types makes it impossible (without using work-arounds) to invoke a mutable method on a value type, such as a property setter, and have the target `obj` updated. +- [`System.Reflection.Pointer.Box()`](https://learn.microsoft.com/dotnet/api/system.reflection.pointer.box) and `UnBox()` must be used to manually box and unbox a pointer (`*`) type. +- When an exception originates within the target method during invoke, the exception is wrapped with a `TargetInvocationException` and re-thrown. In hindsight, this approach is not desired in most cases. Somewhat recently, the `BindingFlags.DoNotWrapExceptions` flag was added to change this behavior as an opt-in. Not having the framework add its own `try-catch` to re-map the exception would help a bit with performance as well. + +Due to the existing performance and usability issues, workarounds and alternatives are used by the community including: +- [MethodInfo.CreateDelegate()](https://learn.microsoft.com/dotnet/api/system.reflection.methodinfo.createdelegate) or [`Delegate.CreateDelegate()`](https://learn.microsoft.com/dotnet/api/system.delegate.createdelegate) which supports a direct, fast method invocation. However, since delegates are strongly-typed, this approach does not work for loosely-typed invoke scenarios where the signature is not known at compile-time. +- [`System.TypedReference`](https://learn.microsoft.com/dotnet/api/system.typedreference) along with `MakeTypedReference()` or `__makeref` can be used to modify a field or nested field directly. The `FieldInfo.SetValueDirect()` and `GetValueDirect()` can be used with `TypedReference` to get/set fields without boxing the value (but still boxes the target since that is still `object`). + - [Dynamic methods](https://learn.microsoft.com/dotnet/framework/reflection-and-codedom/how-to-define-and-execute-dynamic-methods) are used which are IL-emit based and require a non-trivial implementation for even simple things like setting property values. Those who use dynamic methods must have their own loosely-typed invoke APIs, which may go as far as using generics with `Type.MakeGenericType()` or `MethodInfo.MakeGenericMethod()` to avoid boxing. In addition, a fallback to standard reflection is required to support those platforms where IL Emit is not available. +- Compiled [expression trees](https://learn.microsoft.com/dotnet/csharp/programming-guide/concepts/expression-trees/) which use dynamic methods if IL Emit is available but also conveniently falls back to standard reflection when not available. Using an expression to invoke a member isn't intuitive and brings along the large `System.Linq.Expressions.dll` assembly. + +# .NET 8 Goals +The .NET 7 release had a 3-4x perf improvement for the existing `Invoke()` APIs by using IL Emit when available and falling back to standard reflection when IL Emit is not available. Although this improvement is significant, it still doesn't replace the need to use the IL-Emit based alternatives (dynamic methods and expression trees) for highly-performance-sensitive scenarios including the `System.Text.Json` serializer. New APIs are required that don't have the overhead of the existing `Invoke()` APIs. + +For .NET 8, there are two primary goals: +1) Support byref-like types both for invoking and passing as arguments; this unblocks various scenarios. An unsafe approach may used for .NET 8 if support for `TypedReference` isn't addressed by Roslyn (covered later). +2) Support "fast invoke" so that using IL Emit with dynamic methods has little to no performance advantage. Today both `STJ (System.Text.Json)` and [DI (dependency injection)](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) use IL Emit for performance although DI uses emit through expressions while STJ uses IL Emit directly. + - Although the new APIs will be zero alloc (no `object` and boxing required) and perform faster than standard reflection, it will not necessarily be as fast as hand-coded IL Emit for specific scenarios that can optimize for constraints such as fixed-size list of parameters or by not doing full defaulting and validation of values. Note that property get/set is a subset of this case since there is either a return value (for _get_) or a single parameter (for _set_) and since property get/set is such a common case with serialization, this design does propose a separate API for this common case to enable maximum performance. + +# Design with managed pointers +In order to address the limitations and issues, a least-common-denominator approach of using _managed pointers_ for the parameters, target and return value. This supports `in/ref/out` aliasing and the various classifications of types including value types, reference types, pointer types and byref-like types. Essentially this is a different way to achieve "boxing" in order to have a single representation of any parameter. + +However, since managed pointers require a reference to a storage location, they do not directly support the same loosely-coupled scenarios as using `object` + boxing for value types. The proposed APIs, however, do make interacting with `object` possible with the new API for the cases that do not require `in/ref/out` variable aliasing for example. + +Today, a managed pointer is obtained safely through the `ref` keyword in C#. It references a storage location of an object or value type which can be a stack variable, static variable, parameter, field or array element. If the storage location is a field or array element, the managed pointer is referred to as an "interior pointer" which is supported by GC meaning that GC won't collect the owning object even if there are only interior pointers to it. + +A managed pointer can be created in several ways: +- [System.TypedReference](https://learn.microsoft.com/en-us/dotnet/api/system.typedreference). Since this a byref-like type, it is only stack-allocated. Internally, it uses a `ref byte` approach along with a reference to a `Type`. Note `TypeReference` is a special type and has its own opcodes (mkrefany, refanytype, refanyval) which translate to C# keywords (`__makeref`, `__reftype`, `__refvalue`). +- Using `ref ` for the strongly typed case or `ref byte` for the loosely-typed case. This is expanded in 7.0 due to the new "ref field" support. Previously, there was an internal `ByReference` class that was used and in 7.0 this was changed to `ByReference` in 8.0 which no longer maintains the `` type and internally just contains `ref byte`. This `ByReference` type is used today in reflection invoke when there are <=4 parameters. +- Using unsafe `void*` (or `IntPtr`) with GC tracking or pinning to make the use GC-safe. Tracking is supported internally through the use of a newer "RegisterForGCReporting()" mechanism. This approach is used today in reflection invoke when there are >=5 parameters. + +# API design principals +The shape of the API is somewhat guided by these principals: +- We don't expose a nullable value type in boxed form - such as through new APIs like `MethodInvoker.Get/SetValue(...)`. Exposing this would be a new precedent and may be abused. + - Currently the runtime hides the nullability work going on with no way for the caller to manually do the same. This will continue going forward. + - For background, the boxing behavior is, for example, an `int?` variable is boxed to an `int` if not null, and a `null` object reference if the nullable type's `HasValue` property is `false`. During unbox back to `int?`, the `int` or `null` is unboxed into `int?` automatically with `HasValue` set appropriately. +- Any value conversions should support being baked into emit. This is for perf, but not required and may come later. Whether or not they are done in emit (which may not be available), they must support being done by reflection internals without emit (like existing reflection). + - This means that conversions should occur after all parameter values are applied, and tied to the MethodBase instance so we can get the parameter/return/target types. + - Conversions include: + - Boxed value type to\from a nullable parameter. + - Convert `null` to default (for value types). + - Support `Type.Missing` for parameter defaulting. + - Casting conversions (downcast integers such as `int` to `short`; casting of reference types). + - Special type conversions (enums, `IntPtr`, `System.Pointer`). +- Expose both a safe and unsafe invoke: + - The safe `Invoke()` will do all existing reflection conversions. This will make it easier for people to migrate from existing reflection. We could add an enum here to control this a bit more if necessary. + - The unsafe `InvokeDirectUnsafe()` will not do any conversions. This means if the object-based `SetArgument()` is used to call a method with nullables, for example, then `InvokeDirectUnsafe()` will not work (we could throw a nice exception at little extra cost). Instead the caller must use `Invoke()` or the `SetArgument()` to set the appropriate nullable value. +- Single `MethodInvoker` type supporting `object` as well as by-ref values. + - It is cumbersome or impossible to always use `ref` semantics, so `object` is supported both for boxing and as the base class for any reference type. The caller only needs to use `ref` for a given parameter when necessary (e.g. passing a byref-like type) or desired (e.g. to avoid boxing or for aliasing). +- There will be a single, canonical private invoke implementation that is ref-based for the parameters, the target and the return value. + - Both the interpreted reflection code (which is implemented in low-level C++) and the NativeAOT ahead-of-time stubs only need one implementation of invoke no matter what higher-level invoke APIs do. This means all validation, conversions etc. can be done ahead of time in C# (like today). The `InvokeDirectUnsafe()` basically maps 1:1 to this canonical invoke. +- A `MethodInvoker` instance is designed to be shared across signature-compatible methods; for example several implementations of an interface member. This means it is not tied to a `MethodBase` upfront; it is only tied during the `Invoke()` by specifying the `MethodBase` parameter. This is like reflection today with the ability to re-use the `object[] parameters` allocation. This does prevent real-time conversions and validation during `SetArgument()`, but as mentioned earlier about not supporting boxed nullable and support emit-based conversions, this is desired. +- A `MethodInvoker` instance is designed to be called several times without re-specifying parameters, unless they need to be re-specified because they changed from the previous invoke due to any parameters being `ref/out`. +- The `MethodInvoker.Invoke()` will not make trimmability worse than it is today. + - Specifically, this means we will not add the `System.Linq.Expressions.Expression.Convert()` functionality that supports any custom conversion operators (explicit or implicit) for each parameter type. Users of expressions moving to `MethodInvoker` will have to consider this although that is expected to be somewhat rare. + +# Proposed APIs + +## MethodInvoker +This ref struct is the mechanism to specify the target + arguments (including return value) and supports these mechanisms: +- `object` (including boxing). Loose coupling scenarios are supported, like reflection today. +- `ref `. Supports new scenarios as mentioned earlier; type must be known ahead-of-time and due to no language support, cannot be a byref-like type like `Span`. +- `void*`. Unsafe cases used to support byref-like types in an unsafe manner. +- `TypedReference`. Not shown below for now; pending language asks, it may make supporting byref-like types a safe operation. + +```cs +namespace System.Reflection +{ + public ref struct MethodInvoker + { + // Takes a variable-length number of arguments: + public unsafe MethodInvoker(ArgumentValue* argumentStorage, int argCount) + + // Dispose needs to be called to unregister GC tracking + public void Dispose() + + // Target + public object? GetTarget() + public ref T GetTarget() + public void SetTarget(object value) + public unsafe void SetTarget(void* value, Type type) + public void SetTarget(ref T value) + + // Arguments + public object? GetArgument(int index) + public ref T GetArgument(int index) + public void SetArgument(int index, object? value) + public unsafe void SetArgument(int index, void* value, Type type) + public void SetArgument(int index, ref T value) + + // Return + public object? GetReturn() + public ref T GetReturn() + public void SetReturn(object value) + public unsafe void SetReturn(void* value, Type type) + public void SetReturn(ref T value) + + // Unsafe versions; no conversions or validation + public unsafe void InvokeDirectUnsafe(MethodBase method) + // Faster for fixed parameter count (object-only) and no ref\out. Any extra args are ignored + public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target) + public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target, ReadOnlySpan args) + public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target, object? arg1) + public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target, object? arg1, object? arg2) + public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target, object? arg1, object? arg2, object? arg3) + public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target, object? arg1, object? arg2, object? arg3, object? arg4) + + // Safe versions; validation and conversions as in reflection today + public void Invoke(MethodBase method) + public static object? Invoke(MethodBase method, object? target) + public static object? Invoke(MethodBase method, ReadOnlySpan target) + public static object? Invoke(MethodBase method, object? target, object? arg1) + public static object? Invoke(MethodBase method, object? target, object? arg1, object? arg2) + public static object? Invoke(MethodBase method, object? target, object? arg1, object? arg2, object? arg3) + public static object? Invoke(MethodBase method, object? target, object? arg1, object? arg2, object? arg3, object? arg4) + } + + // This is used to define the correct storage requirements for the MethodInvoker variable-length cases. + // Internally it is based on 3 IntPtrs that are for 'ref', 'object value' and 'Type': + // - 'ref' points to either the 'value' location or a user-provided location with "void*", "ref " or TypedReference. + // - 'object value' captures any user-provided object value in a GC-safe manner. + // - 'Type' is used in "void*", "ref " and TypedReference cases for validation and in rare cases to prevent + // Types from being GC'd. + public struct ArgumentValue { } +``` + +## PropertyInfo / FieldInfo +For `PropertyInfo`, this is an alternative of using more heavy-weight `MethodInvoker`. For `FieldInfo`, this expands on the existing `Set/GetValueDirect` to also use `TypedReference` for the `value`. + +```diff +namespace System.Reflection +{ + public abstract class PropertyInfo + { ++ [System.CLSCompliantAttribute(false)] + public virtual void GetValueDirect(TypedReference obj, TypedReference result); + ++ [System.CLSCompliantAttribute(false)] + public virtual void SetValueDirect(TypedReference obj, TypedReference value); + + // Possible for performance in System.Text.Json: ++ public virtual Func CreateGetterDelegate(); ++ public virtual Action CreateSetterDelegate(); + } + + public abstract class FieldInfo + { ++ [System.CLSCompliantAttribute(false)] + public virtual void GetValueDirect(TypedReference obj, TypedReference result); + ++ [System.CLSCompliantAttribute(false)] + public virtual void SetValueDirect(TypedReference obj, TypedReference value); + + // Possible for performance in System.Text.Json: ++ public virtual Func CreateGetterDelegate(); ++ public virtual Action CreateSetterDelegate(); + } +} +``` + +## Examples +### Variable-length object arguments +```cs +unsafe +{ + using (MethodInvoker invoker = new MethodInvoker(argCount: 3)) + { + invoker.SetArgument(0, new MyClass()); + invoker.SetArgument(1, null); + invoker.SetArgument(2, 42); + invoker.SetArgument(3, "Hello"); + invoker.InvokeDirectUnsafe(method); + } +} +``` + +### Avoiding boxing +Value types can be references to avoid boxing. + +```cs +int i = 42; +int ret = 0; +using (MethodInvoker invoker = new MethodInvoker(argCount: 3)) +{ + invoker.SetArgument(0, new MyClass()); + invoker.SetArgument(1, null); + invoker.SetArgument(2, ref i); // No boxing (argument not required to be byref) + invoker.SetArgument(3, "Hello"); + invoker.SetReturn(ref ret); // No boxing; 'ret' variable updated automatically + unsafe + { + invoker.InvokeDirectUnsafe(method); + } +} +``` + +### Pass a `Span` to a method +```cs +Span span = new int[] { 42, 43 }; +ArgumentValuesFixed args = new(1); + +unsafe +{ + using (MethodInvoker invoker = new MethodInvoker(ref args)) + { + #pragma warning disable CS8500 + // Ideally in the future we can use __makeref(span) here instead. + void* ptr = (void*)new IntPtr(&span); + #pragma warning restore CS8500 + + invoker.SetArgument(0, ptr, typeof(Span)); + invoker.InvokeDirectUnsafe(method); + } +} +``` +# Design addendum +## STJ and DI +As a litmus test, STJ and DI will be changed (or prototyped) to use the new APIs proposed here. This is more important to DI since, unlike STJ which has a source generator that can avoid reflection, DI is better suited to reflection than source generation. See also https://github.com/dotnet/runtime/issues/66153 which should be addressed by having a fast constructor invoke that can be used by DI. + +### STJ use of reflection +See the [source for the non-emit strategy](https://github.com/dotnet/runtime/blob/3f0106aed2ece86c56f9f49f0191e94ee5030bff/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionMemberAccessor.cs) which includes: +- [`Activator.CreateInstance(Type type, nonPublic: false)`](https://learn.microsoft.com/dotnet/api/system.activator.createinstance?#system-activator-createinstance(system-type-system-boolean)). Note that this is used instead of `ConstructorInfo` for zero-parameter public constructors since it is already super fast and does not use IL Emit. +- [`ConstructorInfo.Invoke(object?[]?)`](https://learn.microsoft.com/dotnet/api/system.reflection.constructorinfo.invoke?#system-reflection-constructorinfo-invoke(system-object())) for binding to an explicitly selected constructor during deserialization for cases where property setters or fields are not present. +- [`MethodBase.Invoke(object? obj, object?[]? parameters)`](https://learn.microsoft.com/dotnet/api/system.reflection.methodbase.invoke?view=system-reflection-methodbase-invoke(system-object-system-object())) for property get/set. +- [`FieldInfo.GetValue(object? obj)`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.getvalue). +- [`FieldInfo.SetValue(object? obj, object? value)`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.setvalue). + +### DI use of reflection +- [`Array.CreateInstance(Type elementType, int length`](https://learn.microsoft.com/en-us/dotnet/api/system.array.createinstance?view=net-7.0#system-array-createinstance(system-type-system-int32)) via the [source](https://github.com/dotnet/runtime/blob/5b8ebeabb32f7f4118d0cc8b8db28705b62469ee/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteRuntimeResolver.cs#L165). +- [`ConstructorInfo.Invoke(BindingFlags.DoNotWrapException, binder: null, object?[]?, culture:null)`](https://learn.microsoft.com/en-us/dotnet/api/system.reflection.constructorinfo.invoke?view=net-7.0#system-reflection-constructorinfo-invoke(system-reflection-bindingflags-system-reflection-binder-system-object()-system-globalization-cultureinfo)) via the [source](https://github.com/dotnet/runtime/blob/5b8ebeabb32f7f4118d0cc8b8db28705b62469ee/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteRuntimeResolver.cs#L69). + +### Byref-like (or `ref struct`) support +Currently, C# allows one to construct a `TypedReference` to a reference type or value type, but not a byref-like type: +```cs +int i = 42; +TypedReference tr1 = __makeref(i); // okay + +Span span = stackalloc int[42]; +TypedReference tr2 = __makeref(span); // Error CS1601: Cannot make reference to variable of type 'Span' +``` + +An 8.0 ask from Roslyn is to allow this to compile. See [C# ask for supporting TypedReference + byref-like types](https://github.com/dotnet/roslyn/issues/65255). + +Since `TypedReference` internally stores a reference to the **storage location**, and not the actual value or managed-reference-passed-by-value, it effectively supports the `ref/out/in` modifiers. Also, it does this with an implicit `ref` - attempting to use the `ref` keyword is not allowed: +```cs +int i = 42; +TypedReference tr = __makeref(ref i); // error CS1525: Invalid expression term 'ref' +``` +and not necessary due to `__refvalue`: +```cs +int i = 42; +TypedReference tr = __makeref(i); +__refvalue(tr, int) = 100; +Console.WriteLine(i); // 100 +``` +or by using `ref` along with `__refvalue`: +```cs +int i = 42; +TypedReference tr = __makeref(i); +ChangeIt(tr); +Console.WriteLine(i); //100 + +public static void ChangeIt(TypedReference tr) +{ + ref int i = ref __refvalue(tr, int); + i = 100; +} +``` + +## Byref-like support + simplification constraints +Below are some simplification constraints that may help with any Roslyn implementation around lifetime rules. + +**A `TypedReference` can't reference another `TypedReference`** +```cs +TypedReference tr = ... +TypedReference tr2 = __makeref(tr); // error CS1601: Cannot make reference to variable of type 'TypedReference' +``` + +This is similar to trying to make a `TypedReference` to a `Span` above -- both are byref-like types. + +However, this limitation for `TypedReference-to-TypedReference` does **not** need to be removed for the goals in this design if it helps with simplifying lifetime rules. + +**Just support what is allowed today.** No new capabilities are expected; reflection invokes existing methods which must have already been compiled according to existing rules: +```cs +internal class Program +{ + static void Main() + { + // Case 1 + MyRefStruct rs = default; + TypedReference tr = __makeref(rs._span); // Assume we allow instead of CS1601 + ChangeIt(tr); + + // Case 2 + Span heap = new int[42]; + Span stack = stackalloc int[42]; + CallMe(ref stack, heap); // okay + CallMe(ref heap, stack); // CS8350 as expected + } + + public static void ChangeIt(TypedReference tr) + { + // This compiles today: + ref Span s = ref __refvalue(tr, Span); + + // And can be assigned to default + s = default; + + Span newspan = stackalloc int[42]; + + // But this causes CS8352 as expected: + s = ref newspan; + } + + public static void CallMe(ref Span span1, Span span2) { } +} + +public ref struct MyRefStruct +{ + public Span _span; +} +``` + +Wherever `__makeref` is allowed today then it should also support byref-like types: +```cs +internal class Program +{ + static void Main() + { + + Span span = stackalloc int[42]; + TypedReference tr = __makeref(span); // Assume we allow instead of CS1601 + + // Using a proposed invoke API; calling should be supported passing byvalue + MethodInfo mi1 = typeof(Program).GetMethod(nameof(ChangeIt1)); + mi1.InvokeDirectUnsafe(target: default, arg1: tr); + + // and supported passing byref + MethodInfo mi2 = typeof(Program).GetMethod(nameof(ChangeIt2)); + mi2.InvokeDirectUnsafe(target: default, arg1: tr); + + // Just like these methods can be called today: + ChangeIt1(span); + ChangeIt2(ref span); + } + + public static void ChangeIt1(Span span) { } + public static void ChangeIt2(ref Span span) { } +} +``` + +FWIW `__arglist` (only supported on Windows) currently compiles with byref-like types: +```cs +Span s1 = stackalloc byte[1]; +Span s2 = stackalloc byte[11]; +Span s3 = stackalloc byte[111]; +CallMe(__arglist(s1, s2, s3)); + +static unsafe void CallMe(__arglist) +{ + // However, when enumerating __arglist here, the runtime throws when accessing a byref-like + // type although that limitation is easily fixable on Windows (just a runtime limitation; not compiler) +``` + +# Future +Holding area of features discussed but not planned yet. + +## Variable-length, safe collections +The API proposal below does have a variable-length stack-only approach that uses an internal GC tracking mechanism. A easier-to-pass or callback version is not expected in 8.0; see https://github.com/dotnet/runtime/issues/75349. + +## `__arglist` +`TypedReference` is also used by the undocumented `__arglist` along with `System.ArgIterator` although `__arglist` is Windows-only. The approach taken by `__arglist` will not be leveraged or expanded upon in this design. It would, however, allow a pseudo-strongly-typed approach like +```cs +string s = ""; +int i = 1; +object o = null; +// This is kind of nice, but not proposed for 8.0: +methodInfo.Invoke(__arglist(s, ref i, o)); +``` +## TypedReference +This is currently optional and being discussed. If `TypedReference` ends up supporting references to byref-like types like `Span` then it will be much more useful otherwise just the existing `ref ` API can be used instead. The advantage of `TypedReference` is that it does not require generics so it can be made to work with `Span` easier than adding a feature that would allowing generic parameters to be a byref-like type. + +To avoid the use of C#-only "undocumented" keywords, wrappers for `__makeref`, `__reftype`, `__refvalue` which also enable other languages. +```diff +namespace System +{ + public ref struct TypedReference + { + // Equivalent of __makeref except for a byref-like type since they can't be a generic parameter - see + // see https://github.com/dotnet/runtime/issues/65112 for reference. ++ public static TypedReference Make(ref T? value); ++ public static unsafe TypedReference Make(Type type, void* value); + // Helper used for boxed or loosely-typed cases ++ public static TypedReference Make(ref object value, Type type); + + // Equivalent of __refvalue ++ public ref T GetValue(); + + // Equivalent of __reftype ++ public Type Type { get; }; + } +} +``` + +## MethodInvoker +Add TypedReference: +```cs + public void SetTarget(TypedReference value) + public void SetArgument(int index, TypedReference value) + public void SetReturn(TypedReference value) +``` + +## ArgumentValuesFixed +For perf, we may add this constructor to MethodInvoker: +```cs + public MethodInvoker(ref ArgumentValuesFixed values) +``` +with this new type: +```cs +namespace System.Reflection +{ + public ref partial struct ArgumentValuesFixed + { + public const int MaxArgumentCount; // 8 shown here (pending perf measurements to find optimal value) + + // Used when non-object arguments are specified later. + public ArgumentValuesFixed(int argCount) + + // Faster way to pass objects: + public ArgumentValuesFixed(object? obj1) + public ArgumentValuesFixed(object? obj1, object? o2) // ("obj" not "o" assume for naming) + public ArgumentValuesFixed(object? obj1, object? o2, object? o3) + public ArgumentValuesFixed(object? obj1, object? o2, object? o3, object? o4) + public ArgumentValuesFixed(object? obj1, object? o2, object? o3, object? o4, object? o5) + public ArgumentValuesFixed(object? obj1, object? o2, object? o3, object? o4, object? o5, object? o6) + public ArgumentValuesFixed(object? obj1, object? o2, object? o3, object? o4, object? o5, object? o6, object? o7) + public ArgumentValuesFixed(object? obj1, object? o2, object? o3, object? o4, object? o5, object? o6, object? o7, object? o8) + } +} +``` +### Fixed-length arguments (sample) +```cs +MethodInfo method = ... // Some method to call +ArgumentValuesFixed values = new(4); // 4 parameters +MethodInvoker invoker = new MethodInvoker(ref values); +invoker.SetArgument(0, new MyClass()); +invoker.SetArgument(1, null); +invoker.SetArgument(2, 42); +invoker.SetArgument(3, "Hello"); + +// Can inspect before or after invoke: +object o0 = invoker.GetArgument(0); +object o1 = invoker.GetArgument(1); +object o2 = invoker.GetArgument(2); +object o3 = invoker.GetArgument(3); + +invoker.InvokeDirectUnsafe(method); +int ret = (int)invoker.GetReturn(); +``` + +### Fixed-length object arguments (sample) +```cs +ArgumentValuesFixed args = new(new MyClass(), null, 42, "Hello"); +MethodInvoker invoker = new MethodInvoker(ref args); +invoker.InvokeDirectUnsafe(method); +```