Skip to content

Proposal for high-performance codegen-less Reflection factory APIs #23716

@GrabYourPitchforks

Description

@GrabYourPitchforks

/cc @jkotas

Background

There are certain scenarios today - largely involving activation, serialization, and DI - where library authors perform codegen in order to perform operations on arbitrary types. The primary reason for this is performance. The standard Reflection APIs are too slow to be used in the code paths targeted by these library authors, and though codegen has a large upfront cost it performs considerably better when amortized over the lifetime of the application.

This approach generally works well, but the .NET Framework is considering scenarios where it must operate in environments which do not allow codegen. This renders ineffective the existing performance improvement techniques used by these library authors.

We are uniquely positioned to provide a set of APIs which can cover the majority of scenarios traditionally involving reflection-based codegen. The general idea is that library authors can rely on the APIs we provide to work correctly both in codegen-enabled and in codegen-disallowed environments. Alternatively, the library authors can detect at runtime whether codegen is enabled, and if so they can use their existing highly-optimized codegen logic, falling back to the new API surface if codegen is disallowed.

Sample API surface

namespace System.Reflection {
    public delegate ref TField FieldAccessor<TField>(object target);

    public delegate ref TField FieldAccessor<TTarget, TField>(ref TTarget target);

    /// <summary>
    /// Provides factories that can be used by serializers, formatters, and DI systems
    /// to perform reflection-like activities in performance-critical code paths.
    /// </summary>
    [SecurityCritical]
    public static class ReflectionServices
    {
        public static bool IsCodegenAllowed { get; }

        /*
         * FIELD ACCESSORS, GETTERS, AND SETTERS
         */

        public static FieldAccessor<TField> CreateFieldAccessor<TField>(FieldInfo fieldInfo);
        public static FieldAccessor<TTarget, TField> CreateFieldAccessor<TTarget, TField>(FieldInfo fieldInfo);
        public static Func<object, object> CreateFieldGetter(FieldInfo fieldInfo);
        public static Func<object, TField> CreateFieldGetter<TField>(FieldInfo fieldInfo);
        public static Func<TTarget, TField> CreateFieldGetter<TTarget, TField>(FieldInfo fieldInfo);
        public static Action<object, object> CreateFieldSetter(FieldInfo fieldInfo);
        public static Action<object, TField> CreateFieldSetter<TField>(FieldInfo fieldInfo);
        // TTarget must not be value type; field must be an instance field.
        public static Action<TTarget, TField> CreateFieldSetter<TTarget, TField>(FieldInfo fieldInfo);
        
        /*
         * PARAMETERLESS OBJECT CREATION
         */

        public static Func<object> CreateInstanceFactory(Type type);
        public static Func<T> CreateInstanceFactory<T>();

        /*
         * PARAMETERFUL OBJECT CREATION
         */

        public static Func<object[], object> CreateInstanceFactory(ConstructorInfo constructorInfo);
        public static Func<object[], T> CreateInstanceFactory<T>(ConstructorInfo constructorInfo);
        public static Delegate CreateInstanceFactoryTyped(ConstructorInfo constructorInfo, Type delegateType);

        /*
         * PROPERTY GETTERS AND SETTERS
         * TODO: How would indexed properties be represented? Using the normal method invocation routines?
         */

        public static Func<object, object> CreatePropertyGetter(PropertyInfo propertyInfo);
        public static Func<object, TProperty> CreatePropertyGetter<TProperty>(PropertyInfo propertyInfo);
        public static Func<TTarget, TProperty> CreatePropertyGetter<TTarget, TProperty>(PropertyInfo propertyInfo);
        public static Action<object, object> CreatePropertySetter(PropertyInfo propertyInfo);
        public static Action<object, TProperty> CreatePropertySetter<TProperty>(PropertyInfo propertyInfo);
        // TTarget must not be value type; property must be an instance property.
        public static Action<TTarget, TProperty> CreatePropertySetter<TTarget, TProperty>(PropertyInfo propertyInfo);

        /*
         * METHODS
         */

        public static Func<object, object[], object> CreateMethodInvoker(MethodInfo methodInfo);
        // If instance method, 'delegateType' must be open over 'this' parameter.
        public static Delegate CreateMethodInvoker(MethodInfo methodInfo, Type delegateType);

        /*
         * EVENTS
         */
        
        public static Action<object, object> CreateEventSubscriber(EventInfo eventInfo);
        // Event must be an instance event.
        public static Action<TTarget, TDelegate> CreateEventSubscriber<TTarget, TDelegate>(EventInfo eventInfo);
        public static Action<object, object> CreateEventUnsubscriber(EventInfo eventInfo);
        // Event must be an instance event.
        public static Action<TTarget, TDelegate> CreateEventUnsubscriber<TTarget, TDelegate>(EventInfo eventInfo);
    }
}

Goals and non-goals

  • These APIs are not geared toward standard application developers who are already comfortable using the existing Reflection API surface. They are instead geared toward advanced library developers who need to perform Reflection operations in performance-sensitive code paths.

  • These APIs must work in a codegen-disallowed execution environment. (Are there exceptions?)

  • These APIs do not need to cover all scenarios currently allowed by the existing methods on MethodInfo and related types. For example, constructors that take ref or out parameters are sufficiently rare that we don't need to account for them. They can be invoked via the standard Reflection APIs.

  • These APIs do not need to have the same observable behavior as using the Reflection APIs; e.g., we may determine that these APIs should not throw TargetInvocationException on failure. But these APIs must provide consistent behavior regardless of whether they're running within a codegen-enabled or a codegen-disallowed environment.

  • Delegate creation does not need to be particularly optimized since there will be many checks performed upfront and we will ask callers to cache the returned delegate instances. However, once the delegates are created their invocation must be faster than calling the existing Reflection APIs. (Exception: if codegen is disallowed, then delegate invocation should be faster than calling the existing Reflection APIs wherever possible, and it must not be slower.)

  • It is an explicit goal to get serialization library authors to prefer this system over hand-rolling codegen for most member access scenarios. The selling points of this API would be ease of use (compared to hand-rolling codegen), performance, and the ability to work in a wide variety of execution environments.

  • It is an explicit non-goal to have performance characteristics equal to or better than a library's own custom codegen. For example, a DI system might choose to codegen a single method that both queries a service provider to get dependency instances and calls newobj on the target constructor. Such a system will always outperform these generalized APIs, but the API performance should be good enough that library authors would be generally satisfied using them over Reflection as a fallback in these scenarios.

  • These APIs do not need to support custom implementations of MemberInfo. Only support for CLR-backed members is required.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-needs-workAPI needs work before it is approved, it is NOT ready for implementationarea-System.Reflectionneeds-further-triageIssue has been initially triaged, but needs deeper consideration or reconsiderationtenet-performancePerformance related issue

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions