From 04219e98ac1ae3f81f519a1572d76c0f2fe81f06 Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Thu, 29 Dec 2022 16:51:49 -0600 Subject: [PATCH 1/5] Initial draft --- accepted/2022/ReflectionInvoke.md | 354 ++++++++++++++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 accepted/2022/ReflectionInvoke.md diff --git a/accepted/2022/ReflectionInvoke.md b/accepted/2022/ReflectionInvoke.md new file mode 100644 index 000000000..8c49c8d55 --- /dev/null +++ b/accepted/2022/ReflectionInvoke.md @@ -0,0 +1,354 @@ +# Reflection Invoke for 8.0 (draft \ in progress) +#### Steve Harter +#### December 15, 2022 + +# Background +For additional context, see: +- [Developers using reflection invoke should be able to use ref struct](https://github.com/dotnet/runtime/issues/45152) +- [Originally library issue; needs refreshing\updating](https://github.com/dotnet/runtime/issues/10057) + +The invoke APIs and capabilities have essentially remained the same since inception of .NET Framework. Basically, [`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). Fields do not have an `Invoke()` and instead have `GetValue()` and `SetValue()` methods since there is no invokable code around field access. + +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. + - Requires a heap allocation and associated GC overhead. + - A cast is required during unbox. + - Value types require a copy during box and unbox. +- An `object[]` must be allocated (or manually cached by the caller) to contain parameter values. +- 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 arguments (`BindingFlags`, `Binder`, `CultureInfo`) add overhead even when not used. Plus using those are actually quite rare with the exception of `BindingFlags.DoNotWrapExceptions` which is proposed to be the default going forward. + +and has limitations and issues: +- Cannot be used with byref-like types like `Span` either has 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 `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 not possible to invoke a mutable method and have the target `obj` parameter updated. +- [`System.Reflection.Pointer.Box()`](https://learn.microsoft.com/dotnet/api/system.reflection.pointer.box?view=net-7.0) and `UnBox()` must be used to manually box and unbox a pointer (`*`) type. +- When an exception occurs in the target method, the exception is wrapped with a `TargetInvocationException` and re-thrown. This approach is not desired in most cases and somewhat recently, `BindingFlags.DoNotWrapExceptions` was added to change this behavior as an opt-in. Not having a `try\catch` would help a bit with performance as well. + +Due to the performance issues and usability issues, workarounds and alternatives are used including: +- [MethodInfo.CreateDelegate()](https://learn.microsoft.com/dotnet/api/system.reflection.methodinfo.createdelegate) which supports a direct method invocation and thus is strongly-typed and not appropriate for loosely-typed invoke scenarios. +- [Dynamic methods](https://learn.microsoft.com/dotnet/framework/reflection-and-codedom/how-to-define-and-execute-dynamic-methods) which are IL-emit based and include a non-trivial implementation. +- Compiled [expression trees](https://learn.microsoft.com/dotnet/csharp/programming-guide/concepts/expression-trees/) which use dynamic method if IL Emit is available but also conveniently fallback to standard reflection when IL Emit is not available. + +# 8.0 Goals +The 7.0 release had a 3-4x perf improvement for `Invoke()` by using IL Emit when available and falling back to standard reflection when not available, keeping the same object-based APIs. Although this improvement is significant, it doesn't replace the need to use the IL-Emit based alternatives for the highly-performance-sensitive scenarios such as the `System.Text.Json` serializer. New APIs are required. + +For 8.0, there are two primary goals: +1) Support byref-like types both for invoking and passing as arguments. An unsafe approach may need to be used for V8. This unblocks scenarios. +2) Support "fast invoke" so that dynamic methods no longer have to be used for performance. Today both `System.Text.Json` and [dependency injection](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) use IL emit for performance. As a litmus test, these areas will be changed to use the new APIs proposed here. + +# Design +In order to address the limitations and issues: +- A least-common-denominator approach of using stack-based "interior pointers" for the invoke 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 (byref-like types may require additional Roslyn support). Essentially this is a different way to achieve "boxing" in order to have a common representation of any parameter type. +- Optionally, depending on final performance of the "interior pointer" approach, add support for creating an object-based delegate for properties and fields, like `MethodInfo.CreateDelegate()` but taking `object` as the target. This would be used for `System.Text.Json` and other areas that have strongly-typed `parameters` but a weakly-typed `target` and need maximum performance from that. + +## Interior pointers +An interior pointer differs from a managed reference: +- Can only exist on the stack (not the heap) +- References an existing storage location, either a method parameter, a field, a variable, or an array element. +- To be GC-safe, either requires pinning the pointer or letting the runtime track it by various means (more on this below). This may require overhead during a GC when physical memory locations change, but that is somewhat rare due to the nature of it being stack-only. + +See also [C++\CLI documentation](https://learn.microsoft.com/en-us/cpp/extensions/interior-ptr-cpp-cli?view=msvc-170). + +An interior pointer is 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 the `ref byte` approach below. +- Using `ref byte`. This is possible in 7.0 due to the new "ref field" support. Previously, there was an internal `ByReference` class that was used. This `ref byte` approach is used today in reflection invoke when there are <=4 parameters. +- Using `IntPtr` with tracking or pinning. Currently, tracking is only supported internally through the use of a "RegisterForGCReporting()" mechanism. This approach is used today in reflection invoke when there are >=5 parameters. + +This design proposes the use of `TypedReference` for the safe version of APIs, and either `ref byte` or `IntPtr` approaches for the unsafe version. The `ref byte` and `IntPtr` approaches require the use of pointers, through the internal `object.GetRawData()` and other means. + +## TypedReference +The internal reflection implementation will not likely be based on `TypedReference` -- instead it will take, from public APIs, `TypedReference` instances and "peel off" the interior pointer as an `IntPtr` along with tracking to support GC. The use of `TypedReference` is essentially to not require the use of unsafe code in the public APIs. Unsafe pubic APIs will likely be added as well that should have slightly better performance characteristics. + +### Existing usages +`TypedReference` is used in `FieldInfo` via [`SetValueDirect(TypeReference obj, object? value)`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.setvaluedirect) and [`object? GetValueDirect(TypeReference obj)`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.getvaluedirect). The approach taken by `FieldInfo` will be expanded upon in the design here. The existing approach supports the ability to get\set a field on a target value type without boxing. Boxing the target through through [`FieldInfo.SetValue()`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.setvalue) instead is useless since the changes made to the boxed value type would not be reflected back to the original value type instance. + +`TypedReference` is also used by the undocumented `__arglist` along with `System.ArgIterator`. 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)); +``` + +### API additions +Currently `TypedReference` is not designed for general use, although it has been part of the CLR since inception. It must be created through the undocumented `__makeref` C# keyword. There is also a `__refvalue` to obtain the value and a `__reftype` to obtain the underlying `Type`. The design here will add new APIs that are expected to be used instead of these keywords to make the APIs more mainstream and accessible from other languages. + +### 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 without using the `ref` keyword. 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 simplfication 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; +} +``` + +If allowed today in strongly-typed manner, then reflection should also allow: +```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.InvokeDirect(target: default, arg1: tr); + + // and supported passing byref + MethodInfo mi2 = typeof(Program).GetMethod(nameof(ChangeIt2)); + mi2.InvokeDirect(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` 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 could be removed (just a runtime limitation; not compiler) +``` + +## Variable-length, safe collections +(todo; see https://github.com/dotnet/runtime/issues/75349. _I have a local prototype that does a callback that doesn't require any new language or runtime features however it is not a clean programming model. The callback is necessary to interop with hide our internal GC "tracking" feature via "RegisterForGCReporting()"._) + +## Exception handling +Todo; discuss using `BindingFlags.DoNotWrapExceptions` semantics only (no wrapping of exceptions). + +# Proposed APIs +(work in progress; various prototypes exist here) +## TypedReference +```diff +namespace System +{ + public ref struct TypedReference + { + // Equivalent of __makeref (except for a byref-like type since they can't be a generic parameter) ++ public static TypedReference Make(ref T? 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(); + // Used for byref-like types or loosely-typed cases with >4 parameters ++ public readonly unsafe ref byte TargetRef { get; } + } +} +``` + +## MethodInfo +(these may be extension methods instead) + +```diff +namespace System.Reflection +{ + public abstract class MethodBase + { + // Helpers for <= 4 parameters + + // Note that 'default(TypedReference)' can be specified for any "not used" arguments, such as for + // static methods ('target' is 'default'), 'void'-returning methods ('result' is 'default') or + // for any trailing unused arguments (e.g. 'arg4' can be 'default'). + + // We may also want to consider using 'ref byte' instead of or in addition to 'TypedReference' + // along with new helper methods like 'public static ref byte GetRawData(object o)' ++ [System.CLSCompliantAttribute(false)] ++ public virtual void InvokeDirect( ++ TypedReference target, ++ TypedReference result); + ++ [System.CLSCompliantAttribute(false)] ++ public virtual void InvokeDirect( ++ TypedReference target, ++ TypedReference arg1, ++ TypedReference result); + ++ [System.CLSCompliantAttribute(false)] ++ public virtual void InvokeDirect( ++ TypedReference target, ++ TypedReference arg1, ++ TypedReference arg2, ++ TypedReference result); + ++ [System.CLSCompliantAttribute(false)] ++ public virtual void InvokeDirect( ++ TypedReference target, ++ TypedReference arg1, ++ TypedReference arg2, ++ TypedReference arg3, ++ TypedReference result); + ++ [System.CLSCompliantAttribute(false)] ++ public virtual void InvokeDirect( ++ TypedReference target, ++ TypedReference arg1, ++ TypedReference arg2, ++ TypedReference arg3, ++ TypedReference arg4, ++ TypedReference result); + + // Unsafe (todo: more on this; samples) ++ public virtual unsafe void InvokeDirect( ++ TypedReference target, ++ TypedReference* parameters, ++ TypedReference result); + + // Also consider a safe variable-length callback that does tracking internally + // since we don't support a safe variable-length collection mechanism + // or the ability to use a Span + // (todo: more on this?; prototype exists) + } +} +``` + +## PropertyInfo \ FieldInfo +```diff +namespace System.Reflection +{ + public abstract class PropertyInfo + { ++ [System.CLSCompliantAttribute(false)] + public virtual void GetValueDirect(TypedReference target, TypedReference result); + ++ [System.CLSCompliantAttribute(false)] + public virtual void SetValueDirect(TypedReference target, 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 target, TypedReference result); + ++ [System.CLSCompliantAttribute(false)] + public virtual void SetValueDirect(TypedReference target, TypedReference value); + + // Possible adds to get max performance in System.Text.Json: ++ public virtual Func CreateGetterDelegate(); ++ public virtual Action CreateSetterDelegate(); + } +} +``` From 7c46b9cfd9d15e1dfb3ae11df546cead3542e5b9 Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Wed, 4 Jan 2023 15:50:59 -0600 Subject: [PATCH 2/5] Various feedback and minor changes --- accepted/2022/ReflectionInvoke.md | 90 +++++++++++++++++++------------ 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/accepted/2022/ReflectionInvoke.md b/accepted/2022/ReflectionInvoke.md index 8c49c8d55..0e90e9fbc 100644 --- a/accepted/2022/ReflectionInvoke.md +++ b/accepted/2022/ReflectionInvoke.md @@ -1,13 +1,13 @@ # Reflection Invoke for 8.0 (draft \ in progress) #### Steve Harter -#### December 15, 2022 +#### Jan 4, 2023 # Background For additional context, see: - [Developers using reflection invoke should be able to use ref struct](https://github.com/dotnet/runtime/issues/45152) - [Originally library issue; needs refreshing\updating](https://github.com/dotnet/runtime/issues/10057) -The invoke APIs and capabilities have essentially remained the same since inception of .NET Framework. Basically, [`MethodBase.Invoke`](https://learn.microsoft.com/dotnet/api/system.reflection.methodbase.invoke): +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); @@ -18,7 +18,11 @@ public object? Invoke( 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). Fields do not have an `Invoke()` and instead have `GetValue()` and `SetValue()` methods since there is no invokable code around field access. +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 [`MethodInfo? GetMethod()`](https://learn.microsoft.com/dotnet/api/system.reflection.propertyinfo.getmethod) and [`MethodInfo? 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). @@ -33,41 +37,56 @@ However the object-based Invoke() is not very performant: - Requires a heap allocation and associated GC overhead. - A cast is required during unbox. - Value types require a copy during box and unbox. -- An `object[]` must be allocated (or manually cached by the caller) to contain parameter values. +- 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 arguments (`BindingFlags`, `Binder`, `CultureInfo`) add overhead even when not used. Plus using those are actually quite rare with the exception of `BindingFlags.DoNotWrapExceptions` which is proposed to be the default going forward. +- The additional invoke arguments (`BindingFlags`, `Binder`, `CultureInfo`) add overhead even when not used. Plus using those is actually 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 has 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.** +- Cannot be used with byref-like types like `Span` either as the target or 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 `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 not possible to invoke a mutable method and have the target `obj` parameter updated. -- [`System.Reflection.Pointer.Box()`](https://learn.microsoft.com/dotnet/api/system.reflection.pointer.box?view=net-7.0) and `UnBox()` must be used to manually box and unbox a pointer (`*`) type. -- When an exception occurs in the target method, the exception is wrapped with a `TargetInvocationException` and re-thrown. This approach is not desired in most cases and somewhat recently, `BindingFlags.DoNotWrapExceptions` was added to change this behavior as an opt-in. Not having a `try\catch` would help a bit with performance as well. +- Boxing of value types makes it not possible to invoke a mutable method on a value type and have the target `obj` parameter 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. 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 a `try\catch` would help a bit with performance as well. + +Due to the performance and usability issues, workarounds and alternatives are used 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. +- [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 fallback 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 dynamic methods no longer have to be used for performance. 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. -Due to the performance issues and usability issues, workarounds and alternatives are used including: -- [MethodInfo.CreateDelegate()](https://learn.microsoft.com/dotnet/api/system.reflection.methodinfo.createdelegate) which supports a direct method invocation and thus is strongly-typed and not appropriate for loosely-typed invoke scenarios. -- [Dynamic methods](https://learn.microsoft.com/dotnet/framework/reflection-and-codedom/how-to-define-and-execute-dynamic-methods) which are IL-emit based and include a non-trivial implementation. -- Compiled [expression trees](https://learn.microsoft.com/dotnet/csharp/programming-guide/concepts/expression-trees/) which use dynamic method if IL Emit is available but also conveniently fallback to standard reflection when IL Emit is not available. +## STJ and DI +As a litmus test, STJ and DI will be changed 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 must continue to use reflection. 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. -# 8.0 Goals -The 7.0 release had a 3-4x perf improvement for `Invoke()` by using IL Emit when available and falling back to standard reflection when not available, keeping the same object-based APIs. Although this improvement is significant, it doesn't replace the need to use the IL-Emit based alternatives for the highly-performance-sensitive scenarios such as the `System.Text.Json` serializer. New APIs are required. +### 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). -For 8.0, there are two primary goals: -1) Support byref-like types both for invoking and passing as arguments. An unsafe approach may need to be used for V8. This unblocks scenarios. -2) Support "fast invoke" so that dynamic methods no longer have to be used for performance. Today both `System.Text.Json` and [dependency injection](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) use IL emit for performance. As a litmus test, these areas will be changed to use the new APIs proposed here. +### 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). # Design In order to address the limitations and issues: -- A least-common-denominator approach of using stack-based "interior pointers" for the invoke 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 (byref-like types may require additional Roslyn support). Essentially this is a different way to achieve "boxing" in order to have a common representation of any parameter type. +- A least-common-denominator approach of using stack-based "interior pointers" for the invoke 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. - Optionally, depending on final performance of the "interior pointer" approach, add support for creating an object-based delegate for properties and fields, like `MethodInfo.CreateDelegate()` but taking `object` as the target. This would be used for `System.Text.Json` and other areas that have strongly-typed `parameters` but a weakly-typed `target` and need maximum performance from that. ## Interior pointers An interior pointer differs from a managed reference: -- Can only exist on the stack (not the heap) +- Can only exist on the stack (not the heap). - References an existing storage location, either a method parameter, a field, a variable, or an array element. -- To be GC-safe, either requires pinning the pointer or letting the runtime track it by various means (more on this below). This may require overhead during a GC when physical memory locations change, but that is somewhat rare due to the nature of it being stack-only. +- To be GC-safe, either requires pinning the pointer or letting the runtime track it by various means (more on this below). This may require overhead during a GC in order to lookup the parent object, if any, from the interior pointer, but that is somewhat rare due to the nature of it being stack-only and short-lived. See also [C++\CLI documentation](https://learn.microsoft.com/en-us/cpp/extensions/interior-ptr-cpp-cli?view=msvc-170). @@ -76,15 +95,15 @@ An interior pointer is created in several ways: - Using `ref byte`. This is possible in 7.0 due to the new "ref field" support. Previously, there was an internal `ByReference` class that was used. This `ref byte` approach is used today in reflection invoke when there are <=4 parameters. - Using `IntPtr` with tracking or pinning. Currently, tracking is only supported internally through the use of a "RegisterForGCReporting()" mechanism. This approach is used today in reflection invoke when there are >=5 parameters. -This design proposes the use of `TypedReference` for the safe version of APIs, and either `ref byte` or `IntPtr` approaches for the unsafe version. The `ref byte` and `IntPtr` approaches require the use of pointers, through the internal `object.GetRawData()` and other means. +This design proposes the use of `TypedReference` for the safe version of APIs, and either `ref byte`, `IntPtr` or `TypeReference*` approaches for the unsafe version. The `ref byte` and `IntPtr` approaches require the use of pointers through the internal `object.GetRawData()` and other means which would need to be exposed publicaly or wrapped in some manner. ## TypedReference -The internal reflection implementation will not likely be based on `TypedReference` -- instead it will take, from public APIs, `TypedReference` instances and "peel off" the interior pointer as an `IntPtr` along with tracking to support GC. The use of `TypedReference` is essentially to not require the use of unsafe code in the public APIs. Unsafe pubic APIs will likely be added as well that should have slightly better performance characteristics. +The internal reflection implementation will not likely be based on `TypedReference` -- instead it will take, from public APIs, `TypedReference` instances and "peel off" the interior pointer either as a `ref byte` or as an `IntPtr` along with tracking to support GC. The use of `TypedReference` is essentially to not require the use of unsafe code in the public APIs. Unsafe public APIs will likely be added as well that should have slightly better performance characteristics. ### Existing usages -`TypedReference` is used in `FieldInfo` via [`SetValueDirect(TypeReference obj, object? value)`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.setvaluedirect) and [`object? GetValueDirect(TypeReference obj)`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.getvaluedirect). The approach taken by `FieldInfo` will be expanded upon in the design here. The existing approach supports the ability to get\set a field on a target value type without boxing. Boxing the target through through [`FieldInfo.SetValue()`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.setvalue) instead is useless since the changes made to the boxed value type would not be reflected back to the original value type instance. +`TypedReference` is used in `FieldInfo` via [`SetValueDirect(TypeReference obj, object? value)`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.setvaluedirect) and [`object? GetValueDirect(TypeReference obj)`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.getvaluedirect). The approach taken by `FieldInfo` will be expanded upon in the design here. The existing approach supports the ability to get\set a field on a target value type without boxing. Boxing the target through [`FieldInfo.SetValue()`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.setvalue) is useless since the changes made to the boxed value type would not be reflected back to the original value type instance. -`TypedReference` is also used by the undocumented `__arglist` along with `System.ArgIterator`. 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 +`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; @@ -108,7 +127,7 @@ TypedReference tr2 = __makeref(span); // Error CS1601: Cannot make reference to 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 without using the `ref` keyword. Attempting to use the `ref` keyword is not allowed: +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' @@ -135,7 +154,7 @@ public static void ChangeIt(TypedReference tr) ``` ### Byref-like support + simplification constraints -Below are some simplfication constraints that may help with any Roslyn implementation around lifetime rules. +Below are some simplification constraints that may help with any Roslyn implementation around lifetime rules. **A `TypedReference` can't reference another `TypedReference`** ```cs @@ -188,7 +207,7 @@ public ref struct MyRefStruct } ``` -If allowed today in strongly-typed manner, then reflection should also allow: +Wherever `__makeref` is allowed today then it should also support byref-like types: ```cs internal class Program { @@ -216,7 +235,7 @@ internal class Program } ``` -FWIW `__arglist` currently compiles with byref-like types: +FWIW `__arglist` (only supported on Windows) currently compiles with byref-like types: ```cs Span s1 = stackalloc byte[1]; Span s2 = stackalloc byte[11]; @@ -226,7 +245,7 @@ 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 could be removed (just a runtime limitation; not compiler) + // type although that limitation is easily fixable on Windows (just a runtime limitation; not compiler) ``` ## Variable-length, safe collections @@ -243,7 +262,8 @@ namespace System { public ref struct TypedReference { - // Equivalent of __makeref (except for a byref-like type since they can't be a generic parameter) + // 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); // Helper used for boxed or loosely-typed cases + public static TypedReference Make(ref object value, Type type); @@ -266,9 +286,11 @@ namespace System.Reflection { // Helpers for <= 4 parameters - // Note that 'default(TypedReference)' can be specified for any "not used" arguments, such as for - // static methods ('target' is 'default'), 'void'-returning methods ('result' is 'default') or - // for any trailing unused arguments (e.g. 'arg4' can be 'default'). + // Note that 'default(TypedReference)' can be specified for: + // - any "not used" arguments + // - any trailing unused arguments (e.g. 'arg4' can be 'default' if the method only takes 3 args) + // - static methods ('target' is 'default') + // - 'void'-returning methods ('result' is 'default') // We may also want to consider using 'ref byte' instead of or in addition to 'TypedReference' // along with new helper methods like 'public static ref byte GetRawData(object o)' From dc246e05ca1d7acab798ae0e7da4866836c511d1 Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Thu, 13 Apr 2023 14:47:53 -0500 Subject: [PATCH 3/5] Update based on latest work and prototyping --- accepted/2022/ReflectionInvoke.md | 442 ++++++++++++++++++------------ 1 file changed, 270 insertions(+), 172 deletions(-) diff --git a/accepted/2022/ReflectionInvoke.md b/accepted/2022/ReflectionInvoke.md index 0e90e9fbc..9da3be63b 100644 --- a/accepted/2022/ReflectionInvoke.md +++ b/accepted/2022/ReflectionInvoke.md @@ -1,6 +1,6 @@ # Reflection Invoke for 8.0 (draft \ in progress) #### Steve Harter -#### Jan 4, 2023 +#### April 13, 2023 # Background For additional context, see: @@ -20,7 +20,7 @@ public object? Invoke( ``` 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 [`MethodInfo? GetMethod()`](https://learn.microsoft.com/dotnet/api/system.reflection.propertyinfo.getmethod) and [`MethodInfo? SetMethod()`](https://learn.microsoft.com/dotnet/api/system.reflection.propertyinfo.setmethod). +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). @@ -34,86 +34,294 @@ The `Invoke()` APIs are easy to use and flexible: However the object-based Invoke() is not very performant: - Boxing is required for value types. - - Requires a heap allocation and associated GC overhead. - - A cast is required during unbox. - - Value types require a copy during box and unbox. + - 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 actually quite rare with the exception of `BindingFlags.DoNotWrapExceptions`, which is the proposed behavior for the new APIs proposed here. +- 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 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 `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 not possible to invoke a mutable method on a value type and have the target `obj` parameter updated. +- 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. 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 a `try\catch` would help a bit with performance as well. +- 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 a `try\catch` would help a bit with performance as well. Due to the performance and usability issues, workarounds and alternatives are used 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. -- [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 fallback 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. +- [`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 dynamic methods no longer have to be used for performance. 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. +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 performace faster than standard reflection, it will not necessarily be as fast as hand-coded IL Emit for specific scenarios that can optimize for any 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. -## STJ and DI -As a litmus test, STJ and DI will be changed 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 must continue to use reflection. 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. +# 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. -### 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). +However, since managed pointers require a reference to a storage location, they does 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. -### 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). +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. -# Design -In order to address the limitations and issues: -- A least-common-denominator approach of using stack-based "interior pointers" for the invoke 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. -- Optionally, depending on final performance of the "interior pointer" approach, add support for creating an object-based delegate for properties and fields, like `MethodInfo.CreateDelegate()` but taking `object` as the target. This would be used for `System.Text.Json` and other areas that have strongly-typed `parameters` but a weakly-typed `target` and need maximum performance from that. +A managed pointer can becreated 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# keyworkds (`__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. -## Interior pointers -An interior pointer differs from a managed reference: -- Can only exist on the stack (not the heap). -- References an existing storage location, either a method parameter, a field, a variable, or an array element. -- To be GC-safe, either requires pinning the pointer or letting the runtime track it by various means (more on this below). This may require overhead during a GC in order to lookup the parent object, if any, from the interior pointer, but that is somewhat rare due to the nature of it being stack-only and short-lived. +# Proposed APIs -See also [C++\CLI documentation](https://learn.microsoft.com/en-us/cpp/extensions/interior-ptr-cpp-cli?view=msvc-170). +## MethodInvoker +This ref struct is the mechanism to specify the target + arguments (including return value) and supports these mechanisms: +- `object` (including boxing). Supports 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`. Optional for now; pending language asks, it may make supporting byref-like types a safe operation. -An interior pointer is 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 the `ref byte` approach below. -- Using `ref byte`. This is possible in 7.0 due to the new "ref field" support. Previously, there was an internal `ByReference` class that was used. This `ref byte` approach is used today in reflection invoke when there are <=4 parameters. -- Using `IntPtr` with tracking or pinning. Currently, tracking is only supported internally through the use of a "RegisterForGCReporting()" mechanism. This approach is used today in reflection invoke when there are >=5 parameters. +```cs +namespace System.Reflection +{ + public ref struct MethodInvoker + { + // Zero-arg case: + public MethodInvoker() + + // Variable-length number of arguments: + public unsafe MethodInvoker(ArgumentValue* argumentStorage, int argCount) + + // Fixed length (say up to 8) + public MethodInvoker(ref ArgumentValuesFixed values) + + // Dispose needs to be called with variable-length case + public void Dispose() + + // Target + public object? GetTarget() + public ref T GetTarget() + + public void SetTarget(object value) + public void SetTarget(TypedReference 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 void SetArgument(int index, TypedReference 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 void SetReturn(TypedReference value) + public unsafe void SetReturn(void* value, Type type) + public void SetReturn(ref T value) + + // Invoke direct (limited validation and defaulting) + public unsafe void InvokeDirect(MethodBase method) + + // Invoke (same validation and defaulting as reflection today) + public void Invoke(MethodBase method) + } + + // 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 { } +``` + +## ArgumentValuesFixed +This class is used for cases where the known arguments are small. -This design proposes the use of `TypedReference` for the safe version of APIs, and either `ref byte`, `IntPtr` or `TypeReference*` approaches for the unsafe version. The `ref byte` and `IntPtr` approaches require the use of pointers through the internal `object.GetRawData()` and other means which would need to be exposed publicaly or wrapped in some manner. +```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) + + // Fastest 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) + } +} +``` ## TypedReference -The internal reflection implementation will not likely be based on `TypedReference` -- instead it will take, from public APIs, `TypedReference` instances and "peel off" the interior pointer either as a `ref byte` or as an `IntPtr` along with tracking to support GC. The use of `TypedReference` is essentially to not require the use of unsafe code in the public APIs. Unsafe public APIs will likely be added as well that should have slightly better performance characteristics. +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. -### Existing usages -`TypedReference` is used in `FieldInfo` via [`SetValueDirect(TypeReference obj, object? value)`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.setvaluedirect) and [`object? GetValueDirect(TypeReference obj)`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.getvaluedirect). The approach taken by `FieldInfo` will be expanded upon in the design here. The existing approach supports the ability to get\set a field on a target value type without boxing. Boxing the target through [`FieldInfo.SetValue()`](https://learn.microsoft.com/dotnet/api/system.reflection.fieldinfo.setvalue) is useless since the changes made to the boxed value type would not be reflected back to the original value type instance. +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); -`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 + // Equivalent of __refvalue ++ public ref T GetValue(); + + // Equivalent of __reftype ++ public Type Type { get; }; + } +} +``` + +## 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 +### Fixed-length arguments ```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)); +MethodInfo method = ... // Some method to call +ArgumentValuesFixed values = new(4); // 4 parameters +InvokeContext context = new InvokeContext(ref values); +context.SetArgument(0, new MyClass()); +context.SetArgument(1, null); +context.SetArgument(2, 42); +context.SetArgument(3, "Hello"); + +// Can inspect before or after invoke: +object o0 = context.GetArgument(0); +object o1 = context.GetArgument(1); +object o2 = context.GetArgument(2); +object o3 = context.GetArgument(3); + +context.InvokeDirect(method); +int ret = (int)context.GetReturn(); +``` + +### Fixed-length object arguments (faster) +```cs +ArgumentValuesFixed args = new(new MyClass(), null, 42, "Hello"); +InvokeContext context = new InvokeContext(ref args); +context.InvokeDirect(method); +``` + +### Variable-length object arguments +Unsafe and slightly slower than fixed-length plus requires `using` or `try\finally\Dispose()`. +```cs +unsafe +{ + ArgumentValue* args = stackalloc ArgumentValue[4]; + using (InvokeContext context = new InvokeContext(ref args)) + { + context.SetArgument(0, new MyClass()); + context.SetArgument(1, null); + context.SetArgument(2, 42); + context.SetArgument(3, "Hello"); + context.InvokeDirect(method); + } +} ``` -### API additions -Currently `TypedReference` is not designed for general use, although it has been part of the CLR since inception. It must be created through the undocumented `__makeref` C# keyword. There is also a `__refvalue` to obtain the value and a `__reftype` to obtain the underlying `Type`. The design here will add new APIs that are expected to be used instead of these keywords to make the APIs more mainstream and accessible from other languages. +### Avoiding boxing +Value types can be references to avoid boxing. + +```cs +int i = 42; +int ret = 0; +ArgumentValuesFixed args = new(4); +InvokeContext context = new InvokeContext(ref args); +context.SetArgument(0, new MyClass()); +context.SetArgument(1, null); +context.SetArgument(2, ref i); // No boxing (argument not required to be byref) +context.SetArgument(3, "Hello"); +context.SetReturn(ref ret); // No boxing; 'ret' variable updated automatically +context.InvokeDirect(method); +``` + +### Pass a `Span` to a method +```cs +Span span = new int[] { 42, 43 }; +ArgumentValuesFixed args = new(1); + +unsafe +{ + InvokeContext context = new InvokeContext(ref args); +#pragma warning disable CS8500 + void* ptr = (void*)new IntPtr(&span); +#pragma warning restore CS8500 + // Ideally we can use __makeref(span) instead of the above. + + context.SetArgument(0, ptr, typeof(Span)); + context.InvokeDirect(method); +} +``` +# Design ext +## 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: @@ -153,7 +361,7 @@ public static void ChangeIt(TypedReference tr) } ``` -### Byref-like support + simplification constraints +## 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`** @@ -248,129 +456,19 @@ static unsafe void CallMe(__arglist) // type although that limitation is easily fixable on Windows (just a runtime limitation; not compiler) ``` -## Variable-length, safe collections -(todo; see https://github.com/dotnet/runtime/issues/75349. _I have a local prototype that does a callback that doesn't require any new language or runtime features however it is not a clean programming model. The callback is necessary to interop with hide our internal GC "tracking" feature via "RegisterForGCReporting()"._) - -## Exception handling -Todo; discuss using `BindingFlags.DoNotWrapExceptions` semantics only (no wrapping of exceptions). - -# Proposed APIs -(work in progress; various prototypes exist here) -## TypedReference -```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); - // Helper used for boxed or loosely-typed cases -+ public static TypedReference Make(ref object value, Type type); - - // Equivalent of __refvalue -+ public ref T GetValue(); - // Used for byref-like types or loosely-typed cases with >4 parameters -+ public readonly unsafe ref byte TargetRef { get; } - } -} -``` - -## MethodInfo -(these may be extension methods instead) - -```diff -namespace System.Reflection -{ - public abstract class MethodBase - { - // Helpers for <= 4 parameters - - // Note that 'default(TypedReference)' can be specified for: - // - any "not used" arguments - // - any trailing unused arguments (e.g. 'arg4' can be 'default' if the method only takes 3 args) - // - static methods ('target' is 'default') - // - 'void'-returning methods ('result' is 'default') - - // We may also want to consider using 'ref byte' instead of or in addition to 'TypedReference' - // along with new helper methods like 'public static ref byte GetRawData(object o)' -+ [System.CLSCompliantAttribute(false)] -+ public virtual void InvokeDirect( -+ TypedReference target, -+ TypedReference result); - -+ [System.CLSCompliantAttribute(false)] -+ public virtual void InvokeDirect( -+ TypedReference target, -+ TypedReference arg1, -+ TypedReference result); - -+ [System.CLSCompliantAttribute(false)] -+ public virtual void InvokeDirect( -+ TypedReference target, -+ TypedReference arg1, -+ TypedReference arg2, -+ TypedReference result); - -+ [System.CLSCompliantAttribute(false)] -+ public virtual void InvokeDirect( -+ TypedReference target, -+ TypedReference arg1, -+ TypedReference arg2, -+ TypedReference arg3, -+ TypedReference result); - -+ [System.CLSCompliantAttribute(false)] -+ public virtual void InvokeDirect( -+ TypedReference target, -+ TypedReference arg1, -+ TypedReference arg2, -+ TypedReference arg3, -+ TypedReference arg4, -+ TypedReference result); - - // Unsafe (todo: more on this; samples) -+ public virtual unsafe void InvokeDirect( -+ TypedReference target, -+ TypedReference* parameters, -+ TypedReference result); - - // Also consider a safe variable-length callback that does tracking internally - // since we don't support a safe variable-length collection mechanism - // or the ability to use a Span - // (todo: more on this?; prototype exists) - } -} -``` - -## PropertyInfo \ FieldInfo -```diff -namespace System.Reflection -{ - public abstract class PropertyInfo - { -+ [System.CLSCompliantAttribute(false)] - public virtual void GetValueDirect(TypedReference target, TypedReference result); -+ [System.CLSCompliantAttribute(false)] - public virtual void SetValueDirect(TypedReference target, TypedReference value); +# Future +Holding area of features discussed but not planned yet. - // 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 target, TypedReference result); - -+ [System.CLSCompliantAttribute(false)] - public virtual void SetValueDirect(TypedReference target, TypedReference value); +## Variable-length, safe collections +The API proposal below does have a variable-lenth 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. - // Possible adds to get max performance in System.Text.Json: -+ public virtual Func CreateGetterDelegate(); -+ public virtual Action CreateSetterDelegate(); - } -} +## `__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)); ``` From fdbd1b257b80a82740db9488a898aa6db1dd455c Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Tue, 25 Apr 2023 15:38:32 -0500 Subject: [PATCH 4/5] Change fixed-len case from separate struct to Invoke params; misc other --- accepted/2022/ReflectionInvoke.md | 278 +++++++++++++++++------------- 1 file changed, 160 insertions(+), 118 deletions(-) diff --git a/accepted/2022/ReflectionInvoke.md b/accepted/2022/ReflectionInvoke.md index 9da3be63b..b6873ac8d 100644 --- a/accepted/2022/ReflectionInvoke.md +++ b/accepted/2022/ReflectionInvoke.md @@ -1,11 +1,11 @@ -# Reflection Invoke for 8.0 (draft \ in progress) +# Reflection Invoke for 8.0 (draft / in progress) #### Steve Harter -#### April 13, 2023 +#### April 25, 2023 # Background For additional context, see: - [Developers using reflection invoke should be able to use ref struct](https://github.com/dotnet/runtime/issues/45152) -- [Originally library issue; needs refreshing\updating](https://github.com/dotnet/runtime/issues/10057) +- [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 @@ -29,8 +29,8 @@ The `Invoke()` APIs are easy to use and flexible: - 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`. + - `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. @@ -39,19 +39,19 @@ However the object-based Invoke() is not very performant: - 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. + - `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 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 `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. +- 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 a `try\catch` would help a bit with performance as well. +- 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 performance and usability issues, workarounds and alternatives are used including: +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`). +- [`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. @@ -61,28 +61,53 @@ The .NET 7 release had a 3-4x perf improvement for 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 performace faster than standard reflection, it will not necessarily be as fast as hand-coded IL Emit for specific scenarios that can optimize for any 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. + - 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. +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 does 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. +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 becreated 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# keyworkds (`__makeref`, `__reftype`, `__refvalue`). +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). Supports loose coupling scenarios are supported, like reflection today. +- `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`. Optional for now; pending language asks, it may make supporting byref-like types a safe operation. +- `TypedReference`. Not shown below for now; pending language asks, it may make supporting byref-like types a safe operation. ```cs namespace System.Reflection @@ -106,7 +131,6 @@ namespace System.Reflection public ref T GetTarget() public void SetTarget(object value) - public void SetTarget(TypedReference value) public unsafe void SetTarget(void* value, Type type) public void SetTarget(ref T value) @@ -115,7 +139,6 @@ namespace System.Reflection public ref T GetArgument(int index) public void SetArgument(int index, object? value) - public void SetArgument(int index, TypedReference value) public unsafe void SetArgument(int index, void* value, Type type) public void SetArgument(int index, ref T value) @@ -124,15 +147,25 @@ namespace System.Reflection public ref T GetReturn() public void SetReturn(object value) - public void SetReturn(TypedReference value) public unsafe void SetReturn(void* value, Type type) public void SetReturn(ref T value) - // Invoke direct (limited validation and defaulting) - public unsafe void InvokeDirect(MethodBase method) + // Unsafe direct invoke (no validation or conversions) + public unsafe void InvokeDirectUnsafe(MethodBase method) + // Faster for fixed parameter count (object-only) and no ref/out. Extra args ignored. + public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target) + 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) - // Invoke (same validation and defaulting as reflection today) + // Safe invoke (same validation and conversions as reflection today) public void Invoke(MethodBase method) + public static object? Invoke(MethodBase method, object? 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. @@ -144,59 +177,8 @@ namespace System.Reflection public struct ArgumentValue { } ``` -## ArgumentValuesFixed -This class is used for cases where the known arguments are small. - -```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) - - // Fastest 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) - } -} -``` - -## 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; }; - } -} -``` - -## 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`. +## 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 @@ -230,46 +212,19 @@ namespace System.Reflection ``` ## Examples -### Fixed-length arguments -```cs -MethodInfo method = ... // Some method to call -ArgumentValuesFixed values = new(4); // 4 parameters -InvokeContext context = new InvokeContext(ref values); -context.SetArgument(0, new MyClass()); -context.SetArgument(1, null); -context.SetArgument(2, 42); -context.SetArgument(3, "Hello"); - -// Can inspect before or after invoke: -object o0 = context.GetArgument(0); -object o1 = context.GetArgument(1); -object o2 = context.GetArgument(2); -object o3 = context.GetArgument(3); - -context.InvokeDirect(method); -int ret = (int)context.GetReturn(); -``` - -### Fixed-length object arguments (faster) -```cs -ArgumentValuesFixed args = new(new MyClass(), null, 42, "Hello"); -InvokeContext context = new InvokeContext(ref args); -context.InvokeDirect(method); -``` - ### Variable-length object arguments -Unsafe and slightly slower than fixed-length plus requires `using` or `try\finally\Dispose()`. +Unsafe and slightly slower than fixed-length plus requires `using` or `try-finally-Dispose()`. ```cs unsafe { ArgumentValue* args = stackalloc ArgumentValue[4]; - using (InvokeContext context = new InvokeContext(ref args)) + using (MethodInvoker context = new InvokeContext(ref args)) { context.SetArgument(0, new MyClass()); context.SetArgument(1, null); context.SetArgument(2, 42); context.SetArgument(3, "Hello"); - context.InvokeDirect(method); + context.InvokeDirectUnsafe(method); } } ``` @@ -281,13 +236,13 @@ Value types can be references to avoid boxing. int i = 42; int ret = 0; ArgumentValuesFixed args = new(4); -InvokeContext context = new InvokeContext(ref args); +MethodInvoker context = new InvokeContext(ref args); context.SetArgument(0, new MyClass()); context.SetArgument(1, null); context.SetArgument(2, ref i); // No boxing (argument not required to be byref) context.SetArgument(3, "Hello"); context.SetReturn(ref ret); // No boxing; 'ret' variable updated automatically -context.InvokeDirect(method); +context.InvokeDirectUnsafe(method); ``` ### Pass a `Span` to a method @@ -297,17 +252,17 @@ ArgumentValuesFixed args = new(1); unsafe { - InvokeContext context = new InvokeContext(ref args); + MethodInvoker context = new InvokeContext(ref args); #pragma warning disable CS8500 void* ptr = (void*)new IntPtr(&span); #pragma warning restore CS8500 // Ideally we can use __makeref(span) instead of the above. context.SetArgument(0, ptr, typeof(Span)); - context.InvokeDirect(method); + context.InvokeDirectUnsafe(method); } ``` -# Design ext +# 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. @@ -315,7 +270,7 @@ As a litmus test, STJ and DI will be changed (or prototyped) to use the new APIs 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. +- [`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). @@ -335,7 +290,7 @@ TypedReference tr2 = __makeref(span); // Error CS1601: Cannot make reference to 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: +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' @@ -427,11 +382,11 @@ internal class Program // Using a proposed invoke API; calling should be supported passing byvalue MethodInfo mi1 = typeof(Program).GetMethod(nameof(ChangeIt1)); - mi1.InvokeDirect(target: default, arg1: tr); + mi1.InvokeDirectUnsafe(target: default, arg1: tr); // and supported passing byref MethodInfo mi2 = typeof(Program).GetMethod(nameof(ChangeIt2)); - mi2.InvokeDirect(target: default, arg1: tr); + mi2.InvokeDirectUnsafe(target: default, arg1: tr); // Just like these methods can be called today: ChangeIt1(span); @@ -456,12 +411,11 @@ static unsafe void CallMe(__arglist) // 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-lenth 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. +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 @@ -472,3 +426,91 @@ 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) + + // Fastest 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 context = new MethodInvoker(ref values); +context.SetArgument(0, new MyClass()); +context.SetArgument(1, null); +context.SetArgument(2, 42); +context.SetArgument(3, "Hello"); + +// Can inspect before or after invoke: +object o0 = context.GetArgument(0); +object o1 = context.GetArgument(1); +object o2 = context.GetArgument(2); +object o3 = context.GetArgument(3); + +context.InvokeDirectUnsafe(method); +int ret = (int)context.GetReturn(); +``` + +### Fixed-length object arguments (sample) +```cs +ArgumentValuesFixed args = new(new MyClass(), null, 42, "Hello"); +MethodInvoker context = new MethodInvoker(ref args); +context.InvokeDirectUnsafe(method); +``` + From 68dac1568627332a6ff458717c2f27acd30f08ff Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Wed, 26 Apr 2023 09:10:15 -0500 Subject: [PATCH 5/5] Add fast invoke() with ReadOnlySpan --- accepted/2022/ReflectionInvoke.md | 104 ++++++++++++++---------------- 1 file changed, 50 insertions(+), 54 deletions(-) diff --git a/accepted/2022/ReflectionInvoke.md b/accepted/2022/ReflectionInvoke.md index b6873ac8d..fa425074d 100644 --- a/accepted/2022/ReflectionInvoke.md +++ b/accepted/2022/ReflectionInvoke.md @@ -1,6 +1,6 @@ # Reflection Invoke for 8.0 (draft / in progress) #### Steve Harter -#### April 25, 2023 +#### April 26, 2023 # Background For additional context, see: @@ -114,22 +114,15 @@ namespace System.Reflection { public ref struct MethodInvoker { - // Zero-arg case: - public MethodInvoker() - - // Variable-length number of arguments: + // Takes a variable-length number of arguments: public unsafe MethodInvoker(ArgumentValue* argumentStorage, int argCount) - // Fixed length (say up to 8) - public MethodInvoker(ref ArgumentValuesFixed values) - - // Dispose needs to be called with variable-length case + // 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) @@ -137,7 +130,6 @@ namespace System.Reflection // 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) @@ -145,23 +137,24 @@ namespace System.Reflection // 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 direct invoke (no validation or conversions) + // Unsafe versions; no conversions or validation public unsafe void InvokeDirectUnsafe(MethodBase method) - // Faster for fixed parameter count (object-only) and no ref/out. Extra args ignored. + // 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 invoke (same validation and conversions as reflection today) + // 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) @@ -213,18 +206,16 @@ namespace System.Reflection ## Examples ### Variable-length object arguments -Unsafe and slightly slower than fixed-length plus requires `using` or `try-finally-Dispose()`. ```cs unsafe { - ArgumentValue* args = stackalloc ArgumentValue[4]; - using (MethodInvoker context = new InvokeContext(ref args)) + using (MethodInvoker invoker = new MethodInvoker(argCount: 3)) { - context.SetArgument(0, new MyClass()); - context.SetArgument(1, null); - context.SetArgument(2, 42); - context.SetArgument(3, "Hello"); - context.InvokeDirectUnsafe(method); + invoker.SetArgument(0, new MyClass()); + invoker.SetArgument(1, null); + invoker.SetArgument(2, 42); + invoker.SetArgument(3, "Hello"); + invoker.InvokeDirectUnsafe(method); } } ``` @@ -235,14 +226,18 @@ Value types can be references to avoid boxing. ```cs int i = 42; int ret = 0; -ArgumentValuesFixed args = new(4); -MethodInvoker context = new InvokeContext(ref args); -context.SetArgument(0, new MyClass()); -context.SetArgument(1, null); -context.SetArgument(2, ref i); // No boxing (argument not required to be byref) -context.SetArgument(3, "Hello"); -context.SetReturn(ref ret); // No boxing; 'ret' variable updated automatically -context.InvokeDirectUnsafe(method); +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 @@ -252,14 +247,16 @@ ArgumentValuesFixed args = new(1); unsafe { - MethodInvoker context = new InvokeContext(ref args); -#pragma warning disable CS8500 - void* ptr = (void*)new IntPtr(&span); -#pragma warning restore CS8500 - // Ideally we can use __makeref(span) instead of the above. - - context.SetArgument(0, ptr, typeof(Span)); - context.InvokeDirectUnsafe(method); + 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 @@ -475,7 +472,7 @@ namespace System.Reflection // Used when non-object arguments are specified later. public ArgumentValuesFixed(int argCount) - // Fastest way to pass objects: + // 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) @@ -491,26 +488,25 @@ namespace System.Reflection ```cs MethodInfo method = ... // Some method to call ArgumentValuesFixed values = new(4); // 4 parameters -MethodInvoker context = new MethodInvoker(ref values); -context.SetArgument(0, new MyClass()); -context.SetArgument(1, null); -context.SetArgument(2, 42); -context.SetArgument(3, "Hello"); +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 = context.GetArgument(0); -object o1 = context.GetArgument(1); -object o2 = context.GetArgument(2); -object o3 = context.GetArgument(3); +object o0 = invoker.GetArgument(0); +object o1 = invoker.GetArgument(1); +object o2 = invoker.GetArgument(2); +object o3 = invoker.GetArgument(3); -context.InvokeDirectUnsafe(method); -int ret = (int)context.GetReturn(); +invoker.InvokeDirectUnsafe(method); +int ret = (int)invoker.GetReturn(); ``` ### Fixed-length object arguments (sample) ```cs ArgumentValuesFixed args = new(new MyClass(), null, 42, "Hello"); -MethodInvoker context = new MethodInvoker(ref args); -context.InvokeDirectUnsafe(method); +MethodInvoker invoker = new MethodInvoker(ref args); +invoker.InvokeDirectUnsafe(method); ``` -