Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to mix in delegate (types) to proxies #436

Merged
merged 6 commits into from
Apr 2, 2019

Conversation

stakx
Copy link
Member

@stakx stakx commented Mar 13, 2019

This is a follow-up to #403, which started out with the goal of adding a set of ProxyGenerator.CreateDelegateProxy methods, and ended with us thinking about achieving delegate proxies through DynamicProxy's mixin capabilities. I am closing the earlier PR in favour of this present one.

This PR adds the following capabilities to DynamicProxy:

  • void ProxyGenerationOptions.AddDelegateMixin(Delegate @delegate)
    to add a delegate mixin. This will produce an Invoke method on the proxy that has the same signature as the delegate. The delegate can be Proceed-ed to.

  • void ProxyGenerationOptions.AddDelegateTypeMixin(Type delegateType)
    adds an Invoke method on the proxy that has the same signature as a specified delegate type. When those Invoke methods are intercepted, they cannot be proceeded from as that mixin has no target.

  • Delegate ProxyUtil.CreateDelegateToMixin(object target, Type delegateType)
    TDelegate ProxyUtil.CreateDelegateToMixin<TDelegate>(object target)
    are helper methods to create a delegate of a given type to a suitable Invoke method on a proxy.

By combining the above methods, you can now create a delegate proxy:

var options = new ProxyGenerationOptions();
options.AddDelegateTypeMixin(typeof(Action));

var _ = generator.CreateClassProxy(typeof(object), options, ...);

var proxy = ProxyUtil.CreateDelegateToMixin<Action>(_);

Because you're creating a class (or interface) proxy, you have full control over the "container" type for the generated interceptable Invoke method. You're also free to add custom attributes to that type, as you normally would.

I'd still like to...

  • run some tests whether these new capabilities are actually useful in consuming libraries (Moq in my case)

    See feedback for Moq and NSubstitute below. Feedback is better than anticipated.

  • I am also still unsure whether there may be serialization issues when using lambda delegates as mixin targets... and whether that is relevant in practice.

    ⚠️ If you use a lambda that captures any variables as a delegate mixin, then you won't be able to save the generated dynamic assembly to disk because the captured variables are put in a non-serializable "display class" by the compiler.

    This can be worked around by rewriting such lambdas & captures as explicit types. Beyond that, I'm not yet sure this is even fixable; if so, then we'd likely have to let ProxyGenerationOptions implement ISerializable and serialize it differently (specifically the mixins field).

    For now, I'd like to treat this as a known current limitation of the delegate mixin story, as it's probably not an immediate roadblock for most downstream libs.

Closes #399 (where we said that noone except DP itself should emit into its dynamic assembly, and the only known roadblock to that is delegate proxying; so if we get that, we no longer need ModuleScope.DefineType).

/cc @blairconrad, @thomaslevesque, @zvirja

@zvirja
Copy link
Contributor

zvirja commented Mar 14, 2019

Hi @stakx!

The way you decided to sort it out looks promising and elegant!

I’ll definitely give it a more precise look. In the meanwhile, I want to clarify whether the following feature is supported (found the concern in my old comment):

I've just reviewed the NSubstitute lib and the usages we have and found that new API lucks a few features:

  • ...
  • ability to emit custom attributes to the generated method. We need this to quickly get the original delegate type from the methodInfo instance we internally store for the each recorded call. Basically, we use this attribute also to understand that methodInfo is a delegate proxy, rather than a method from a class proxy, so we render the client error messages more precisely.

Thanks! :)

@stakx
Copy link
Member Author

stakx commented Mar 14, 2019

@zvirja, nice to hear from you again!

ability to emit custom attributes to the generated method

I can't remember all the details from the previous issue, but basically this PR follows @jonorossi's proposal made in #403 (comment), where he also wrote:

It doesn't allow you to emit custom attributes on the proxy's methods (which sounded like a nice to have, not a requirement), but it does still allow custom attributes on the proxy type [...]

That pretty much sums it up. DynamicProxy didn't have the ability to add custom attributes to single methods (AFAIK) and that hasn't changed. If that's needed, perhaps we'd need to look at it in a separate PR.

@stakx stakx changed the title Add ability to mix in delegate (types) to proxies WIP: Add ability to mix in delegate (types) to proxies Mar 14, 2019
@stakx

This comment has been minimized.

@stakx stakx force-pushed the delegate-mixins branch 3 times, most recently from fbb46e4 to 3467d89 Compare March 14, 2019 08:05
@zvirja
Copy link
Contributor

zvirja commented Mar 14, 2019

@stakx Thanks for the replies. It started to ring a bell in my head now.

Tried to give it a go, but it fails with the following error:

Castle.DynamicProxy.InvalidMixinConfigurationException : There is a problem with the mixins added to this ProxyGenerationOptions: The list of mixins contains at least two delegate mixins for the same delegate signature.
Parameter name: mixinInstances
  ----> System.ArgumentException : The list of mixins contains at least two delegate mixins for the same delegate signature.
Parameter name: mixinInstances
   at Castle.DynamicProxy.ProxyGenerationOptions.Initialize()
...

In our code I also mix in an additional object. It might be related to the finding you already described. Please ping me when it's ready, so I can give it a try 😉

From the first glance I could say that API looks perfect from the client perspective and should cover all the needs we have 👍

One more question (warning, might be stupid): are you sure that after all the changes you applied the container generated type is still cached?

P.S. Here is my branch if you would like to take a look.

@stakx
Copy link
Member Author

stakx commented Mar 14, 2019

*snip* (outdated bit that you can safely ignore)

it fails with the following error

I would need to see your code, for the moment I'll make a blind guess: Did you perhaps try to reuse the same ProxyGenerationOptions for different delegate proxy types? I suspect that you might end up with the same usage pattern as I did: You need at least one distinct ProxyGenerationOptions instance per mocked delegate type.

are you sure that after all the changes you applied the container generated type is still cached?

I ran some quick tests this morning. Basically, caching should work as before if equality is correctly implemented on both ProxyGenerationOptions (and MixinData, which is used by the former), I've added some additional unit tests for that. You can test for yourself by attaching e.g. a ConsoleLogger to your ProxyGenerator. When you ask for a proxy, you should see a log message stating whether a cached type was found (and used) or not.

@stakx
Copy link
Member Author

stakx commented Mar 14, 2019

@zvirja, there was a glaringly obvious bug that I didn't notice anymore at 2am last night. 😁 Fixing it also fixed all NSubstitute unit tests except one (When_checking_across_multiple_subs_including_delegates, which appears to fail because of differently named types).

@stakx
Copy link
Member Author

stakx commented Mar 14, 2019

Speaking for Moq, I am very satisfied with how well this change would work out: stakx/moq@50fd9fa (14 insertions, 122 deletions). Almost all of the delegate special-casing code is gone, and gets replaced by the very short ProxyUtil.TryCreateDelegateToMixin bit. All of our unit tests are passing.

@stakx stakx requested a review from jonorossi March 14, 2019 17:07
@zvirja
Copy link
Contributor

zvirja commented Mar 14, 2019

@stakx After working a bit more on our code I managed to finally get it work. Because MethodInfo passed to the invocation is actually taken from the delegate type now, I don't even need to emit custom attributes - simply use methodInfo.DeclaringType to get original delegate type 😊 I really enjoy the way it goes! Thanks a lot! 🤝

The only issue I discovered is that delegate proxy activation degraded in terms of performance:

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i7-8850H CPU 2.60GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
Frequency=2531252 Hz, Resolution=395.0614 ns, Timer=TSC
.NET Core SDK=2.1.504
  [Host] : .NET Core 2.1.8 (CoreCLR 4.6.27317.03, CoreFX 4.6.27317.03), 64bit RyuJIT
  Clr    : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3324.0
  Core   : .NET Core 2.1.8 (CoreCLR 4.6.27317.03, CoreFX 4.6.27317.03), 64bit RyuJIT

BEFORE

                           Method |  Job | Runtime |      Mean |     Error |    StdDev |
--------------------------------- |----- |-------- |----------:|----------:|----------:|
         CreateDelegateSubstitute |  Clr |     Clr | 10.837 us | 0.1587 us | 0.1407 us |
         CreateDelegateSubstitute | Core |    Core |  7.557 us | 0.0533 us | 0.0352 us |
		 
AFTER

                           Method |  Job | Runtime |      Mean |     Error |    StdDev |
--------------------------------- |----- |-------- |----------:|----------:|----------:|
         CreateDelegateSubstitute |  Clr |     Clr | 13.452 us | 0.1607 us | 0.1424 us |
         CreateDelegateSubstitute | Core |    Core |  9.630 us | 0.1893 us | 0.2714 us |

Obviously, I don't want you to profile our code 😅 But if you could benchmark Moq instead, it would be interesting to see whether you notice the same degradation. So it might be easier for you to check whether degradation resides somewhere in the code you are adding (as previously we also used Castle.Proxy to activate proxies and Delegate.CreateDelegate() to make delegates)...🤔

@stakx
Copy link
Member Author

stakx commented Mar 15, 2019

@zvirja: I suspect that ProxyUtil.TryCreateDelegateToMixin might be more costly than it needs to be in some circumstances. It has to assume that there might be several Invoke methods, and it has to pick the right overload. However, if you know that there can only be one Invoke, you might save some execution time by simply doing proxy.GetMethod("Invoke").CreateDelegate(delegateType, proxy) instead. Also, creating an interface proxy might be a wee bit faster than creating a class proxy.

(Here are my quick benchmark results.)
BenchmarkDotNet=v0.11.4, OS=Windows 10.0.17134.407 (1803/April2018Update/Redstone4)
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
  [Host]     : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3221.0
  DefaultJob : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3221.0
Method Mean Error StdDev Median
1. Moq 4.10.1 unchanged 15.98 us 0.1320 us 0.1235 us
2. Reference latest Castle.Core 12.53 us 0.3527 us 1.023 us 11.89 us
3. Use new delegate proxying approach 16.74 us 0.1800 us 0.1503 us
4. Don't use ProxyUtil.TryCreateDelegateToMixin 14.14 us 0.0251 us 0.0235 us

(Not sure btw. why referencing Castle.Core as a project instead of its latest NuGet package makes such a big difference.)

P.S.: Those additional ~2.5 μs/proxy likely won't matter much in practice. You'd have to create an awful lot of delegate proxies (say, 100,000 to 1 million) before that accumulates into a noticeable time span. I think it's a good price to pay if it means we can free our mocking libs from doing their own Refection.Emit-ing, and freeing Core to no longer expose its internal APIs (ModuleScope).

@stakx stakx changed the title WIP: Add ability to mix in delegate (types) to proxies Add ability to mix in delegate (types) to proxies Mar 25, 2019
Copy link
Member

@jonorossi jonorossi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great @stakx 🎉. I had to really review the thread where I proposed this design (forgot all the details), it really did work out well. Many thanks for getting this done.

src/Castle.Core/DynamicProxy/ProxyUtil.cs Outdated Show resolved Hide resolved
src/Castle.Core/DynamicProxy/ProxyUtil.cs Show resolved Hide resolved
src/Castle.Core/DynamicProxy/ProxyUtil.cs Outdated Show resolved Hide resolved
src/Castle.Core/DynamicProxy/MixinData.cs Outdated Show resolved Hide resolved
@stakx stakx force-pushed the delegate-mixins branch 2 times, most recently from 68e8fef to 2e0d8b2 Compare March 29, 2019 14:06
@stakx

This comment has been minimized.

@stakx
Copy link
Member Author

stakx commented Mar 31, 2019

[...] whether this new way of creating delegate proxies will also work for Windsor?

If I'm not mistaken, the necessary changes for Windsor—excluding any possible optimizations—might look as follows (applied here):

 		private Type GetProxyType(IProxyBuilder builder, Type targetDelegateType)
 		{
-			var scope = builder.ModuleScope;
-			var logger = builder.Logger;
-			var generator = new DelegateProxyGenerator(scope, targetDelegateType)
-			{
-				Logger = logger
-			};
-			return generator.GetProxyType();
+			var options = new ProxyGenerationOptions();
+			options.AddDelegateTypeMixin(targetDelegateType);
+			return builder.CreateClassProxyType(typeof(object), Type.EmptyTypes, options);
 		}

@stakx stakx mentioned this pull request Mar 31, 2019
20 tasks
Copy link
Member Author

@stakx stakx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's just one last change I'd like to make (turning TryCreateMixinToDelegate into a throwing non-Try version), then I think this will be ready to merge.

src/Castle.Core/DynamicProxy/ProxyUtil.cs Outdated Show resolved Hide resolved
This adds the following capabilities to DynamicProxy:

 * `ProxyGenerationOptions.AddDelegateMixin` to add a delegate mixin.
   This will produce an `Invoke` method on the proxy that has the same
   signature as the delegate. The delegate can be `Proceed`-ed to.

 * `ProxyGenerationOptions.AddDelegateTypeMixin` adds an `Invoke`
   method on the proxy that has the same signature as a specified
   delegate type. When those `Invoke` methods are intercepted, they
   cannot be proceeded from as that mixin has no target.

 * `ProxyUtil.TryCreateDelegateToMixin` is a helper method to create a
   delegate of a given type to a suitable `Invoke` method on a proxy.

By combining the above methods, you can now create a delegate proxy:

    var options = new ProxyGenerationOptions();
    options.AddDelegateTypeMixin(typeof(Action));

    var _ = generator.CreateClassProxy(typeof(object), options, ...);

    ProxyUtil.TryCreateDelegateToMixin(_, out Action proxy);
                                          ^^^^^^^^^^^^^^^^
Simply comparing the count of mixin types against a filtered distinct
count triggers false positive conflict detection because the filter
was only applied to one set of mixin types.

Also, optimize the conflict detection logic & only run it when needed.
...and standardize on using `Delegate` instead of `MulticastDelegate`
in public method signatures. (This might seem contradictory, since the
delegate type check is based on the latter. But it will arguably cause
users less confusion to see `Delegate` than the less familiar `Multi-
castDelegate`. Also, BCL APIs such `Delegate.CreateDelegate` have a
declared return type of `Delegate`. Let's do the same.)
From the user's point of view, there's no reason to expect `TryCreate-
DelegateToMixin` to fail once a proxy with `options.AddDelegate[Type]-
Mixin` has been created. If the method *does* fail, there will be no
indication what might have gone wrong, and user code likely won't be
able to proceed; so why not just let DynamicProxy throw an exception.
@stakx stakx merged commit b066111 into castleproject:master Apr 2, 2019
@stakx stakx deleted the delegate-mixins branch April 2, 2019 22:03
@stakx stakx added this to the v4.4.0 milestone Apr 2, 2019
@jonorossi
Copy link
Member

Out of curiosity, @jonorossi, can you say whether this new way of creating delegate proxies will also work for Windsor? I haven't looked at its code closely, and am still not overly familiar with it.

IIRC, Windsor is currently using a lower-level component (DelegateProxyGenerator), which might no longer be feasible once we make more of DP's internals internal.
[...]
var options = new ProxyGenerationOptions();
options.AddDelegateTypeMixin(targetDelegateType);
return builder.CreateClassProxyType(typeof(object), Type.EmptyTypes, options);

Yep, looks about right. Maybe we should mark DelegateProxyGenerator obsolete now since we've got a better option.

Somehow, using the Try pattern for this method (and its generic counterpart) feels wrong. In the typical use case of having previously called options.AddDelegate[Type]Mixin, there is no reason why users should be expected to deal with failure at this point. If this method fails, users will generally have no idea what might have gone wrong, and they likely won't be able to proceed, so it would be better for DynamicProxy to throw an exception directly.

I agree, I'm glad you are changing it. I didn't comment on it but I did wonder what the use case of wanting to handle the failure case yourself was, just assumed you had a good reason 😉.

@stakx
Copy link
Member Author

stakx commented Apr 3, 2019

just assumed you had a good reason

Glad to hear you approve of that last-minute change, which didn't get reviewed. I was fairly confident that it was the right call, so I just went ahead.

I've been a fan of the Try pattern esp. since C# introduced the possibility to declare variables inline (out var, is T x), but there's such a thing as overuse. ;-)

That, and I recently came to suspect that the Try pattern will once again become somewhat less useful with C# 8's nullable reference types, where you can make it explicit in the return type that a method might not succeed.

stakx added a commit to stakx/Castle.Core that referenced this pull request Jun 6, 2020
 * Windsor needs `TypeUtil.GetAllInterfaces`, so make it public again.

 * Windsor is currently relying on `DelegateProxyGenerator`, but there
   is now another, more public API to build delegate proxies (see PR
   castleproject#436) so that class can
   now be removed. The unit tests for it are rewritten in terms of the
   newer API to demonstrate that it is indeed a suitable replacement.
stakx added a commit to stakx/Castle.Core that referenced this pull request Jun 7, 2020
 * Windsor needs `TypeUtil.GetAllInterfaces`, so make it public again.

 * Windsor is currently relying on `DelegateProxyGenerator`, but there
   is now another, more public API to build delegate proxies (see PR
   castleproject#436) so that class can
   now be removed. The unit tests for it are rewritten in terms of the
   newer API to demonstrate that it is indeed a suitable replacement.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Do we need a thread-safe alternative to ModuleScope.DefineType?
3 participants