Skip to content

DynamicProxy might no longer need to emit one dedicated invocation type per method having a target #737

@stakx

Description

@stakx

DynamicProxy currently emits one dedicated invocation type for each method that can proceed to a target method. Looking at C# 9's function pointers (delegate*) made me wonder if they could possibly replace those invocation types.

Case example

Say we have this type with a generic method that we want to proxy:

public class Foo
{
    public virtual void Method<T2>(string arg1, T2 arg2) { }
}

var fooProxy = generator.CreateClassProxy<Foo>(...);

Current code generation

Because Method<> is not abstract and can be proceeded to, DynamicProxy emits two additional things for it:

  1. A method FooProxy.Method_callback that calls the base method implementation:

    partial class FooProxy: Foo
    {
        public void Method_callback<T2>(string arg1, T2 arg2)
        {
            base.Method(arg1, arg2);
        }
    }
  2. A Foo_Method<> invocation type inheriting from InheritanceInvocation, whose InvokeMethodOnTarget method implementation delegates to FooProxy.Method_callback:

    public partial class Foo_Method<T2> : InheritanceInvocation
    {
        public override void InvokeMethodOnTarget()
        {
            ((FooProxy)this.proxyObject).Method_callback((string)this.Arguments[0], (T2)this.Arguments[1]);
        }
    }

Proposal: more efficient code generation using delegate*

What we could do instead is:

  1. Still emit FooProxy.Method_callback, as is.
  2. Instead of the FooMethod<> invocation type, emit another method FooProxy.Method_invokeOnTarget<> directly inside the proxy type.
  3. Add a new pre-defined invocation type Invocation (not its final name, obviously) to this library that will work for any proxied method – i. e. it might eventually even replace the existing CompositionInvocation and InheritanceInvocation.

Starting with (3), that general pre-defined invocation type might look as follows (uninteresting parts not shown):

public unsafe sealed class Invocation : IInvocation
{
    private readonly delegate*<object, Invocation, void> invokeMethodOnTarget;

    public Invocation(delegate*<object, Invocation, void> invokeMethodOnTarget, object proxy, object?[] arguments, ...)
    {
        this.invokeMethodOnTarget = invokeMethodOnTarget;

        Proxy = proxy;
        Arguments = arguments;
        ...
    }

    public object Proxy { get; }

    public object?[] Arguments { get; }

    private void InvokeMethodOnTarget()
    {
        if (invokeMethodOnTarget == null) throw new InvalidOperationException("There is no target to proceed to.");
        invokeMethodOnTarget(Proxy!, this);
    }
}

That is, it receives a function pointer invokeMethodOnTarget that the invocation can use to proceed to the target method (whatever that may be).

Regarding (1) and (2), the emitted code for the Foo proxy type might look roughly as follows:

public unsafe class FooProxy : Foo
{
    public override void Method<T2>(string arg1, T2 arg2)
    {
        new Invocation(&Method_invokeOnTarget<T2>, this, arguments: [arg1, arg2], ...).Proceed();
    }

    // DynamicProxy already emits this (albeit with `public` visibility, which would no longer be necessary):
    private void Method_callback<T2>(string arg1, T2 arg2)
    {
        base.Method(arg1, arg2);
    }

    // This is the new method that would replace the separate `Foo_Method` invocation type:
    private static void Method_invokeOnTarget<T>(object proxyObject, Invocation invocation)
    {
        ((FooProxy)proxyObject).Method_callback((string)invocation.Arguments[0], (T2)invocation.Arguments[1]);
    }
}

Possible advantages

  • Fewer types and method emitted would likely result in faster runtime performance during proxy type generation.
  • Same amount of allocations and less virtual method dispatch might result in marginally better runtime performance during proxy object calls – Invocation.InvokeMethodOnTarget does not need to be virtual, and could even be inlined.
  • Fewer pre-defined types needed in DynamicProxy – it's possible that the general-purpose Invocation type could replace all others (such as InheritanceInvocation and CompositionInvocation.
  • Possibly less complexity in generic type parameter handling. We don't need to transplant type parameters from a method to a class type; we'd only be emitting sibling methods next to the proxied method that have its exact same generic type parameters – this might possibly benefit Improve code generation performance/caching using open generic types #448.

This looks feasible to me, but I haven't done any deeper digging to see what problems might surface with this approach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions