Skip to content

Commit

Permalink
Use Castle to generate delegate proxies
Browse files Browse the repository at this point in the history
  • Loading branch information
zvirja committed Apr 8, 2019
1 parent b68c1d2 commit 3ce3a59
Show file tree
Hide file tree
Showing 14 changed files with 102 additions and 182 deletions.
6 changes: 0 additions & 6 deletions src/NSubstitute/AssemblyProperties.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using NSubstitute.Core.Arguments;
using NSubstitute.Proxies;
using NSubstitute.Proxies.CastleDynamicProxy;
using NSubstitute.Proxies.DelegateProxy;
using NSubstitute.Routing;
using NSubstitute.Routing.AutoValues;

Expand Down Expand Up @@ -39,10 +37,7 @@ private static INSubContainer CreateDefaultContainer()
.RegisterPerScope<ISubstituteFactory, SubstituteFactory>()
.RegisterPerScope<ICallRouterResolver, CallRouterResolver>()
.RegisterPerScope<ISubstitutionContext, SubstitutionContext>()
.RegisterPerScope<CastleDynamicProxyFactory, CastleDynamicProxyFactory>()
.RegisterPerScope<DelegateProxyFactory, DelegateProxyFactory>()
.RegisterPerScope<IProxyFactory>(r =>
new ProxyFactory(r.Resolve<DelegateProxyFactory>(), r.Resolve<CastleDynamicProxyFactory>()))
.RegisterPerScope<IProxyFactory, CastleDynamicProxyFactory>()
.RegisterPerScope<ICallFactory, CallFactory>()
.RegisterPerScope<IPropertyHelper, PropertyHelper>()
.RegisterSingleton<IReceivedCallsExceptionThrower, ReceivedCallsExceptionThrower>();
Expand Down
5 changes: 5 additions & 0 deletions src/NSubstitute/Core/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public static string Join(this IEnumerable<string> strings, string separator)
return string.Join(separator, strings);
}

public static bool IsDelegate(this Type type)
{
return type.GetTypeInfo().IsSubclassOf(typeof(MulticastDelegate));
}

private static bool TypeCanBeNull(Type type)
{
return !type.GetTypeInfo().IsValueType || Nullable.GetUnderlyingType(type) != null;
Expand Down
11 changes: 1 addition & 10 deletions src/NSubstitute/Core/MethodFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NSubstitute.Proxies.DelegateProxy;

namespace NSubstitute.Core
{
Expand All @@ -15,10 +14,7 @@ public bool CanFormat(MethodInfo methodInfo)
public string Format(MethodInfo methodInfo, IEnumerable<string> arguments)
{
var args = string.Join(", ", arguments);

return IsDelegateProxy(methodInfo)
? string.Format("Invoke({0})", args)
: string.Format("{0}{1}({2})", methodInfo.Name, FormatGenericType(methodInfo), args);
return $"{methodInfo.Name}{FormatGenericType(methodInfo)}({args})";
}

private string FormatGenericType(MethodInfo methodInfoOfCall)
Expand All @@ -27,10 +23,5 @@ private string FormatGenericType(MethodInfo methodInfoOfCall)
var genericArgs = methodInfoOfCall.GetGenericArguments();
return "<" + string.Join(", ", genericArgs.Select(x => x.GetNonMangledTypeName()).ToArray()) + ">";
}

private static bool IsDelegateProxy(MethodInfo methodInfo)
{
return methodInfo.GetCustomAttribute<ProxiedDelegateTypeAttribute>() != null;
}
}
}
6 changes: 1 addition & 5 deletions src/NSubstitute/Core/SequenceChecking/SequenceFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Linq;
using System.Reflection;
using NSubstitute.Core.Arguments;
using NSubstitute.Proxies.DelegateProxy;

namespace NSubstitute.Core.SequenceChecking
{
Expand Down Expand Up @@ -100,10 +99,7 @@ public string Format(bool multipleInstances, bool includeInstanceNumber)

var instanceIdentifier = includeInstanceNumber ? _instanceNumber + "@" : "";

var delegateType = MethodInfo.GetCustomAttribute<ProxiedDelegateTypeAttribute>()?.DelegateType;
var declaringTypeName = delegateType != null
? delegateType.GetNonMangledTypeName()
: MethodInfo.DeclaringType.Name;
var declaringTypeName = MethodInfo.DeclaringType.GetNonMangledTypeName();
return string.Format("{1}{0}.{2}", declaringTypeName, instanceIdentifier, call);
}

Expand Down
4 changes: 2 additions & 2 deletions src/NSubstitute/Core/SubstituteFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,14 @@ private object Create(Type[] typesToProxy, object[] constructorArguments, bool c

private static Type GetPrimaryProxyType(Type[] typesToProxy)
{
return typesToProxy.FirstOrDefault(t => t.GetTypeInfo().IsSubclassOf(typeof(Delegate)))
return typesToProxy.FirstOrDefault(t => t.IsDelegate())
?? typesToProxy.FirstOrDefault(t => t.GetTypeInfo().IsClass)
?? typesToProxy.First();
}

private static bool CanCallBaseImplementation(Type primaryProxyType)
{
var isDelegate = primaryProxyType.GetTypeInfo().IsSubclassOf(typeof(Delegate));
var isDelegate = primaryProxyType.IsDelegate();
var isClass = primaryProxyType.GetTypeInfo().IsClass;

return isClass && !isDelegate;
Expand Down
10 changes: 0 additions & 10 deletions src/NSubstitute/IsReadOnlyAttribute.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/NSubstitute/NSubstitute.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Castle.Core" Version="4.3.1-*" />
<PackageReference Include="Castle.Core" Version="4.4.0-*" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.3.0-*" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Castle.DynamicProxy;
using NSubstitute.Core;
using NSubstitute.Exceptions;
Expand All @@ -24,17 +23,22 @@ public CastleDynamicProxyFactory(ICallFactory callFactory, IArgumentSpecificatio
}

public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[] additionalInterfaces, object[] constructorArguments)
{
var isDelegate = typeToProxy.IsDelegate();
return isDelegate
? GenerateDelegateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments)
: GenerateTypeProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments);
}

private object GenerateTypeProxy(ICallRouter callRouter, Type typeToProxy, Type[] additionalInterfaces, object[] constructorArguments)
{
VerifyClassHasNotBeenPassedAsAnAdditionalInterface(additionalInterfaces);

var proxyIdInterceptor = new ProxyIdInterceptor(typeToProxy);
var forwardingInterceptor = new CastleForwardingInterceptor(
new CastleInvocationMapper(
_callFactory,
_argSpecificationDequeue),
callRouter);
var forwardingInterceptor = CreateForwardingInterceptor(callRouter);

var proxyGenerationOptions = GetOptionsToMixinCallRouterProvider(callRouter);

var proxy = CreateProxyUsingCastleProxyGenerator(
typeToProxy,
additionalInterfaces,
Expand All @@ -46,13 +50,35 @@ public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[] add
return proxy;
}

/// <summary>
/// Dynamically define the type with specified <paramref name="typeName"/>.
/// </summary>
public TypeBuilder DefineDynamicType(string typeName, TypeAttributes flags)
private object GenerateDelegateProxy(ICallRouter callRouter, Type delegateType, Type[] additionalInterfaces, object[] constructorArguments)
{
VerifyNoAdditionalInterfacesGivenForDelegate(additionalInterfaces);
VerifyNoConstructorArgumentsGivenForDelegate(constructorArguments);

var forwardingInterceptor = CreateForwardingInterceptor(callRouter);

var proxyGenerationOptions = GetOptionsToMixinCallRouterProvider(callRouter);
proxyGenerationOptions.AddDelegateTypeMixin(delegateType);

var proxy = CreateProxyUsingCastleProxyGenerator(
typeToProxy: typeof(object),
additionalInterfaces: null,
constructorArguments: null,
interceptors: new IInterceptor[] {forwardingInterceptor},
proxyGenerationOptions);

forwardingInterceptor.SwitchToFullDispatchMode();

return ProxyUtil.CreateDelegateToMixin(proxy, delegateType);
}

private CastleForwardingInterceptor CreateForwardingInterceptor(ICallRouter callRouter)
{
// It's important to use the signed module, as we exposed internals type to the signed module.
return _proxyGenerator.ProxyBuilder.ModuleScope.DefineType(inSignedModulePreferably: true, typeName, flags);
return new CastleForwardingInterceptor(
new CastleInvocationMapper(
_callFactory,
_argSpecificationDequeue),
callRouter);
}

private object CreateProxyUsingCastleProxyGenerator(Type typeToProxy, Type[] additionalInterfaces,
Expand Down Expand Up @@ -101,19 +127,44 @@ private ProxyGenerationOptions GetOptionsToMixinCallRouterProvider(ICallRouter c

private static void VerifyNoConstructorArgumentsGivenForInterface(object[] constructorArguments)
{
if (constructorArguments != null && constructorArguments.Length > 0)
if (HasItems(constructorArguments))
{
throw new SubstituteException("Can not provide constructor arguments when substituting for an interface.");
}
}

private static void VerifyNoConstructorArgumentsGivenForDelegate(object[] constructorArguments)
{
if (HasItems(constructorArguments))
{
throw new SubstituteException("Can not provide constructor arguments when substituting for a delegate.");
}
}

private static void VerifyNoAdditionalInterfacesGivenForDelegate(Type[] constructorArguments)
{
if (HasItems(constructorArguments))
{
throw new SubstituteException(
"Can not specify additional interfaces when substituting for a delegate. " +
"You must specify only a single delegate type if you need to substitute for a delegate.");
}
}

private static void VerifyClassHasNotBeenPassedAsAnAdditionalInterface(Type[] additionalInterfaces)
{
if (additionalInterfaces != null && additionalInterfaces.Any(x => x.GetTypeInfo().IsClass))
{
throw new SubstituteException("Can not substitute for multiple classes. To substitute for multiple types only one type can be a concrete class; other types can only be interfaces.");
throw new SubstituteException(
"Can not substitute for multiple classes. " +
"To substitute for multiple types only one type can be a concrete class; other types can only be interfaces.");
}
}

private static bool HasItems<T>(T[] array)
{
return array != null && array.Length > 0;
}

private class AllMethodsExceptCallRouterCallsHook : AllMethodsHook
{
Expand Down
113 changes: 4 additions & 109 deletions src/NSubstitute/Proxies/DelegateProxy/DelegateProxyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,125 +12,20 @@

namespace NSubstitute.Proxies.DelegateProxy
{
[Obsolete("This class is deprecated and will be removed in future versions of the product.")]
public class DelegateProxyFactory : IProxyFactory
{
private const string MethodNameInsideProxyContainer = "Invoke";
private const string IsReadOnlyAttributeFullTypeName = "System.Runtime.CompilerServices.IsReadOnlyAttribute";
private readonly CastleDynamicProxyFactory _castleObjectProxyFactory;
private readonly ConcurrentDictionary<Type, Type> _delegateContainerCache = new ConcurrentDictionary<Type, Type>();

public DelegateProxyFactory(CastleDynamicProxyFactory objectProxyFactory)
{
_castleObjectProxyFactory = objectProxyFactory ?? throw new ArgumentNullException(nameof(objectProxyFactory));
}

public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[] additionalInterfaces, object[] constructorArguments)
{
if (HasItems(additionalInterfaces))
{
throw new SubstituteException(
"Can not specify additional interfaces when substituting for a delegate. " +
"You must specify only a single delegate type if you need to substitute for a delegate.");
}
if (HasItems(constructorArguments))
{
throw new SubstituteException("Can not provide constructor arguments when substituting for a delegate.");
}

return DelegateProxy(typeToProxy, callRouter);
}

private static bool HasItems<T>(T[] array)
{
return array != null && array.Length > 0;
}

private object DelegateProxy(Type delegateType, ICallRouter callRouter)
{
var delegateContainer = _delegateContainerCache.GetOrAdd(delegateType, GenerateDelegateContainerInterface);
var invokeMethod = delegateContainer.GetMethod(MethodNameInsideProxyContainer);

var proxy = _castleObjectProxyFactory.GenerateProxy(callRouter, delegateContainer, Type.EmptyTypes, null);
return invokeMethod.CreateDelegate(delegateType, proxy);
}

private Type GenerateDelegateContainerInterface(Type delegateType)
{
var delegateSignature = delegateType.GetMethod("Invoke");
var delegateParameters = delegateSignature.GetParameters();

var typeName = $"NSubstituteDelegateProxy.DelegateContainer_{Guid.NewGuid():N}";

var typeBuilder = _castleObjectProxyFactory.DefineDynamicType(
typeName,
TypeAttributes.Abstract | TypeAttributes.Interface | TypeAttributes.Public);

// Notice, we don't copy the custom modifiers here.
// That's absolutely fine, as custom modifiers are ignored when delegate is constructed.
// See the related discussion here: https://github.com/dotnet/coreclr/issues/18401
var methodBuilder = typeBuilder
.DefineMethod(
MethodNameInsideProxyContainer,
MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.Public,
CallingConventions.Standard,
delegateSignature.ReturnType,
delegateSignature.GetParameters().Select(p => p.ParameterType).ToArray());

// Copy original method attributes, so "out" parameters are recognized later.
for (var i = 0; i < delegateParameters.Length; i++)
{
var parameter = delegateParameters[i];

// Increment position by 1 to skip the implicit "this" parameter.
var paramBuilder = methodBuilder.DefineParameter(i + 1, parameter.Attributes, parameter.Name);

// Read-only parameter ('in' keyword) is recognized by presence of the special attribute.
// If source parameter contained that attribute, ensure to copy it to the generated method.
// That helps Castle to understand that parameter is read-only and cannot be mutated.
DefineIsReadOnlyAttributeIfNeeded(parameter, paramBuilder);
}

// Preserve the original delegate type in attribute, so it can be retrieved later in code.
methodBuilder.SetCustomAttribute(
new CustomAttributeBuilder(
typeof(ProxiedDelegateTypeAttribute).GetConstructors().Single(),
new object[] {delegateType}));

return typeBuilder.CreateTypeInfo().AsType();
}

private static void DefineIsReadOnlyAttributeIfNeeded(ParameterInfo sourceParameter, ParameterBuilder paramBuilder)
public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[] additionalInterfaces, object[] constructorArguments)
{
// Read-only parameter can be by-ref only.
if (!sourceParameter.ParameterType.IsByRef)
{
return;
}

// Lookup for the attribute using full type name.
// That's required because compiler can embed that type directly to the client's assembly
// as type identity doesn't matter - only full type attribute name is checked.
var isReadOnlyAttrType = sourceParameter.CustomAttributes
.Select(ca => ca.AttributeType)
.FirstOrDefault(t => t.FullName.Equals(IsReadOnlyAttributeFullTypeName, StringComparison.Ordinal));

// Parameter doesn't contain the IsReadOnly attribute.
if (isReadOnlyAttrType == null)
{
return;
}

// If the compiler generated attribute is used (e.g. runtime doesn't contain the attribute),
// the generated attribute type might be internal, so we cannot referecnce it in the dynamic assembly.
// In this case use the attribute type from the current assembly, as we allow dynamic assembly
// to read our internal types.
if (!isReadOnlyAttrType.GetTypeInfo().IsVisible)
{
isReadOnlyAttrType = typeof(IsReadOnlyAttribute);
}

paramBuilder.SetCustomAttribute(
new CustomAttributeBuilder(isReadOnlyAttrType.GetConstructor(Type.EmptyTypes), new object[0]));
// Castle factory can now resolve delegate proxies as well.
return _castleObjectProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace NSubstitute.Proxies.DelegateProxy
{
[Obsolete("This class is deprecated and will be removed in future versions of the product.")]
[AttributeUsage(AttributeTargets.Method)]
public class ProxiedDelegateTypeAttribute : Attribute
{
Expand Down
Loading

0 comments on commit 3ce3a59

Please sign in to comment.