Skip to content

Commit

Permalink
Merge pull request #1130 from stakx/default-interface-implementations
Browse files Browse the repository at this point in the history
`.CallBase` for default interface implementations
  • Loading branch information
stakx authored Jan 17, 2021
2 parents da5ee73 + 3f54703 commit cdb32ec
Show file tree
Hide file tree
Showing 8 changed files with 498 additions and 7 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1

## Unreleased

#### Added

* `CallBase` can now be used with interface methods that have a default interface implementation. It will call [the most specific override](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods#the-most-specific-override-rule). (@stakx, #1130)

#### Fixed
* Newly introduced: `AmbiguousMatchException` raised when interface has property indexer besides property in VB. (@mujdatdinc, #1129)

* `AmbiguousMatchException` raised when interface has property indexer besides property in VB. (@mujdatdinc, #1129)
* Interface default methods are ignored (@hahn-kev, #972)

## 4.16.0 (2021-01-16)

Expand Down
27 changes: 27 additions & 0 deletions src/Moq/Behaviors/ReturnBaseOrDefaultValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,24 @@ public override void Execute(Invocation invocation)

if (this.mock.CallBase)
{

#if FEATURE_DEFAULT_INTERFACE_IMPLEMENTATIONS
var tryCallDefaultInterfaceImplementation = false;
#endif

var declaringType = method.DeclaringType;
if (declaringType.IsInterface)
{
if (this.mock.MockedType.IsInterface)
{
// Case 1: Interface method of an interface proxy.

#if FEATURE_DEFAULT_INTERFACE_IMPLEMENTATIONS
// Fall through to invoke default implementation (if one exists).
tryCallDefaultInterfaceImplementation = true;
#else
// There is no base method to call, so fall through.
#endif
}
else
{
Expand All @@ -56,7 +67,13 @@ public override void Execute(Invocation invocation)
Debug.Assert(this.mock.AdditionalInterfaces.Contains(declaringType));

// Case 2b: Additional interface.

#if FEATURE_DEFAULT_INTERFACE_IMPLEMENTATIONS
// Fall through to invoke default implementation (if one exists).
tryCallDefaultInterfaceImplementation = true;
#else
// There is no base method to call, so fall through.
#endif
}
}
}
Expand All @@ -72,6 +89,16 @@ public override void Execute(Invocation invocation)
return;
}
}

#if FEATURE_DEFAULT_INTERFACE_IMPLEMENTATIONS
if (tryCallDefaultInterfaceImplementation && !method.IsAbstract)
{
// Invoke default implementation.
invocation.ReturnValue = invocation.CallBase();
return;
}
#endif

}

if (method.ReturnType != typeof(void))
Expand Down
6 changes: 5 additions & 1 deletion src/Moq/Moq.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
<RootNamespace>Moq</RootNamespace>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningLevel>4</WarningLevel>
<LangVersion>8.0</LangVersion>
<LangVersion>9.0</LangVersion>
</PropertyGroup>

<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.1' ">
<DefineConstants>$(DefineConstants);FEATURE_DEFAULT_INTERFACE_IMPLEMENTATIONS</DefineConstants>
</PropertyGroup>

<PropertyGroup>
Expand Down
20 changes: 19 additions & 1 deletion src/Moq/Pair.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors.
// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt.

using System;

namespace Moq
{
internal readonly struct Pair<T1, T2>
internal readonly struct Pair<T1, T2> : IEquatable<Pair<T1, T2>>
{
public readonly T1 Item1;
public readonly T2 Item2;
Expand All @@ -19,5 +21,21 @@ public void Deconstruct(out T1 item1, out T2 item2)
item1 = this.Item1;
item2 = this.Item2;
}

public bool Equals(Pair<T1, T2> other)
{
return object.Equals(this.Item1, other.Item1)
&& object.Equals(this.Item2, other.Item2);
}

public override bool Equals(object obj)
{
return obj is Pair<T1, T2> other && this.Equals(other);
}

public override int GetHashCode()
{
return unchecked(1001 * this.Item1?.GetHashCode() ?? 101 + this.Item2?.GetHashCode() ?? 11);
}
}
}
224 changes: 221 additions & 3 deletions src/Moq/ProxyFactories/CastleProxyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;

#if FEATURE_DEFAULT_INTERFACE_IMPLEMENTATIONS
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using System.Runtime;
using System.Text;
#endif

using Castle.DynamicProxy;

using Moq.Internals;
Expand Down Expand Up @@ -126,6 +132,17 @@ protected internal override object CallBase()
{
Debug.Assert(this.underlying != null);

#if FEATURE_DEFAULT_INTERFACE_IMPLEMENTATIONS
var method = this.Method;
if (method.DeclaringType.IsInterface && !method.IsAbstract)
{
// As of version 4.4.0, DynamicProxy cannot proceed to default method implementations of interfaces.
// we need to find and call those manually.
var mostSpecificOverride = FindMostSpecificOverride(method, this.underlying.Proxy.GetType());
return DynamicInvokeNonVirtually(mostSpecificOverride, this.underlying.Proxy, this.Arguments);
}
#endif

this.underlying.Proceed();
return this.underlying.ReturnValue;
}
Expand All @@ -136,6 +153,207 @@ public void DetachFromUnderlying()
}
}

#if FEATURE_DEFAULT_INTERFACE_IMPLEMENTATIONS
// Finding and calling default interface implementations currently involves a lot of reflection,
// we are using two caches to speed up these operations for repeated calls.
private static ConcurrentDictionary<Pair<MethodInfo, Type>, MethodInfo> mostSpecificOverrides;
private static ConcurrentDictionary<MethodInfo, Func<object, object[], object>> nonVirtualInvocationThunks;

static CastleProxyFactory()
{
mostSpecificOverrides = new ConcurrentDictionary<Pair<MethodInfo, Type>, MethodInfo>();
nonVirtualInvocationThunks = new ConcurrentDictionary<MethodInfo, Func<object, object[], object>>();
}

/// <summary>
/// Attempts to find the most specific override for the given method <paramref name="declaration"/>
/// in the type chains (base class, interfaces) of the given <paramref name="proxyType"/>.
/// </summary>
public static MethodInfo FindMostSpecificOverride(MethodInfo declaration, Type proxyType)
{
return mostSpecificOverrides.GetOrAdd(new Pair<MethodInfo, Type>(declaration, proxyType), static key =>
{
// This follows the rule specified in:
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods#the-most-specific-override-rule.

var (declaration, proxyType) = key;

var genericParameterCount = declaration.IsGenericMethod ? declaration.GetGenericArguments().Length : 0;
var returnType = declaration.ReturnType;
var parameterTypes = declaration.GetParameterTypes().ToArray();
var declaringType = declaration.DeclaringType;

// If the base class has a method implementation, then by rule (2) it will be more specific
// than any candidate method from an implemented interface:
var baseClass = proxyType.BaseType;
if (baseClass != null && declaringType.IsAssignableFrom(baseClass))
{
var map = baseClass.GetInterfaceMap(declaringType);
var index = Array.IndexOf(map.InterfaceMethods, declaration);
return map.TargetMethods[index];
}

// Otherwise, we need to look for candidates in all directly or indirectly implemented interfaces:
var implementedInterfaces = proxyType.GetInterfaces();
var candidateMethods = new HashSet<MethodInfo>();
foreach (var implementedInterface in implementedInterfaces.Where(i => declaringType.IsAssignableFrom(i)))
{
// Search for an implicit override:
var candidateMethod = implementedInterface.GetMethod(declaration.Name, genericParameterCount, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, parameterTypes, null);

// Search for an explicit override:
if (candidateMethod?.GetBaseDefinition() != declaration)
{
// Unfortunately, we cannot use `.GetInterfaceMap` to find out whether an interface method
// overrides another base interface method, i.e. whether they share the same vtbl slot.
// It appears that the best thing we can do is to look for a non-public method having
// the right name and parameter types, and hope for the best:
var name = new StringBuilder();
name.Append(declaringType.FullName);
name.Replace('+', '.');
name.Append('.');
name.Append(declaration.Name);
candidateMethod = implementedInterface.GetMethod(name.ToString(), genericParameterCount, BindingFlags.NonPublic | BindingFlags.Instance, null, parameterTypes, null);
}

if (candidateMethod == null) continue;

// Now we have a candidate override. We need to check if it is less specific than any others
// that we have already found earlier:
if (candidateMethods.Any(cm => implementedInterface.IsAssignableFrom(cm.DeclaringType))) continue;

// No, it is the most specific override so far. Add it to the list, but before doing so,
// remove all less specific overrides from it:
candidateMethods.ExceptWith(candidateMethods.Where(cm => cm.DeclaringType.IsAssignableFrom(implementedInterface)).ToArray());
candidateMethods.Add(candidateMethod);
}

var candidateCount = candidateMethods.Count();
if (candidateCount > 1)
{
throw new AmbiguousImplementationException();
}
else if (candidateCount == 1)
{
return candidateMethods.First();
}
else
{
return declaration;
}
});
}

/// <summary>
/// Performs a non-virtual (non-polymorphic) call to the given <paramref name="method"/>
/// using the specified object <paramref name="instance"/> and <paramref name="arguments"/>.
/// </summary>
public static object DynamicInvokeNonVirtually(MethodInfo method, object instance, object[] arguments)
{
// There are a couple of probable alternatives to the following implementation that
// unfortunately don't work in practice:
//
// * We could try `method.Invoke(instance, InvokeMethod | DeclaredOnly, arguments)`,
// unfortunately that doesn't work. `DeclaredOnly` does not have the desired effect.
//
// * We could get a function pointer via `method.MethodHandle.GetFunctionPointer()`,
// then construct a delegate for it (see ECMA-335 §II.14.4). This does not work
// because the delegate signature would have to have a matching parameter list,
// not just an untyped `object[]`. It also doesn't work because we don't always have
// a suitable delegate type ready (e.g. when a method has by-ref parameters).
//
// So we end up having to create a dynamic method that transforms the `object[]`array
// to a properly typed argument list and then invokes the method using the IL `call`
// instruction.

var thunk = nonVirtualInvocationThunks.GetOrAdd(method, static method =>
{
var originalParameterTypes = method.GetParameterTypes();
var n = originalParameterTypes.Count;

var dynamicMethod = new DynamicMethod(string.Empty, returnType: typeof(object), parameterTypes: new[] { typeof(object), typeof(object[]) });
dynamicMethod.InitLocals = true;
var il = dynamicMethod.GetILGenerator();

var arguments = new LocalBuilder[n];
var returnValue = il.DeclareLocal(typeof(object));

// Erase by-ref-ness of parameter types to get at the actual type of value.
// We need this because we are handed `invocation.Arguments` as an `object[]` array.
var parameterTypes = originalParameterTypes.ToArray();
for (var i = 0; i < n; ++i)
{
if (parameterTypes[i].IsByRef)
{
parameterTypes[i] = parameterTypes[i].GetElementType();
}
}

// Transfer `invocation.Arguments` into appropriately typed local variables.
// This involves unboxing value-typed arguments, and possibly down-casting others from `object`.
// The `unbox.any` instruction will do the right thing in both cases.
for (var i = 0; i < n; ++i)
{
arguments[i] = il.DeclareLocal(parameterTypes[i]);

il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldc_I4, i);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Unbox_Any, parameterTypes[i]);
il.Emit(OpCodes.Stloc, arguments[i]);
}

// Now we're going to call the actual default implementation.

// We do this inside a `try` block because we need to write back possibly modified
// arguments to `invocation.Arguments` even if the called method throws.
var returnLabel = il.DefineLabel();
il.BeginExceptionBlock();

// Perform the actual call.
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass, method.DeclaringType);
for (var i = 0; i < n; ++i)
{
il.Emit(originalParameterTypes[i].IsByRef ? OpCodes.Ldloca : OpCodes.Ldloc, arguments[i]);
}
il.Emit(OpCodes.Call, method);

// Put the return value in a local variable for later retrieval.
if (method.ReturnType != typeof(void))
{
il.Emit(OpCodes.Box, method.ReturnType);
il.Emit(OpCodes.Castclass, typeof(object));
il.Emit(OpCodes.Stloc, returnValue);
}
il.Emit(OpCodes.Leave, returnLabel);

il.BeginFinallyBlock();

// Write back possibly modified arguments to `invocation.Arguments`.
for (var i = 0; i < n; ++i)
{
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldc_I4, i);
il.Emit(OpCodes.Ldloc, arguments[i]);
il.Emit(OpCodes.Box, arguments[i].LocalType);
il.Emit(OpCodes.Stelem_Ref);
}
il.Emit(OpCodes.Endfinally);

il.EndExceptionBlock();
il.MarkLabel(returnLabel);

il.Emit(OpCodes.Ldloc, returnValue);
il.Emit(OpCodes.Ret);

return (Func<object, object[], object>)dynamicMethod.CreateDelegate(typeof(Func<object, object[], object>));
});

return thunk.Invoke(instance, arguments);
}
#endif

/// <summary>
/// This hook tells Castle DynamicProxy to proxy the default methods it suggests,
/// plus some of the methods defined by <see cref="object"/>, e.g. so we can intercept
Expand Down
Loading

0 comments on commit cdb32ec

Please sign in to comment.