Skip to content

Commit

Permalink
Merge pull request #342 from AArnott/fix236
Browse files Browse the repository at this point in the history
Add support for dynamic proxies with non-public interfaces
  • Loading branch information
AArnott authored Sep 19, 2019
2 parents ba0b883 + 0d3015e commit de7e17c
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 39 deletions.
3 changes: 3 additions & 0 deletions src/StreamJsonRpc.Tests.ExternalAssembly/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("StreamJsonRpc.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace StreamJsonRpc.Tests.ExternalAssembly
{
using System.Threading.Tasks;

internal interface ISomeInternalProxyInterface
{
Task<int> SubtractAsync(int a, int b);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<CodeAnalysisRuleSet>..\tests.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>

</Project>
28 changes: 23 additions & 5 deletions src/StreamJsonRpc.Tests/JsonRpcProxyGenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ public interface IServerWithGenericMethod
Task AddAsync<T>(T a, T b);
}

internal interface IServerInternal
internal interface IServerInternal : StreamJsonRpc.Tests.ExternalAssembly.ISomeInternalProxyInterface
{
Task AddAsync(int a, int b);
Task<int> AddAsync(int a, int b);
}

[Fact]
Expand Down Expand Up @@ -325,11 +325,22 @@ public void InstanceProxiesImplementIJsonRpcClientProxy()
}

[Fact]
public void InternalInterface()
public async Task InternalInterface()
{
// When implementing internal interfaces work, fill out this test to actually invoke it.
var streams = FullDuplexStream.CreateStreams();
Assert.Throws<TypeLoadException>(() => JsonRpc.Attach<IServerInternal>(streams.Item1));
var server = new ServerOfInternalInterface();
var serverRpc = JsonRpc.Attach(streams.Item2, server);

var clientRpc = JsonRpc.Attach(streams.Item1);

// Try the first internal interface, which is external to this test assembly
var proxy1 = clientRpc.Attach<StreamJsonRpc.Tests.ExternalAssembly.ISomeInternalProxyInterface>();
Assert.Equal(-1, await proxy1.SubtractAsync(1, 2).WithCancellation(this.TimeoutToken));

// Now create a proxy for another interface that is internal within this assembly, but derives from the external assembly's internal interface.
// This verifies that we can handle multiple sets of assemblies which we need internal visibility into, as well as that it can track base type interfaces.
var proxy2 = clientRpc.Attach<IServerInternal>();
Assert.Equal(3, await proxy2.AddAsync(1, 2).WithCancellation(this.TimeoutToken));
}

[Fact]
Expand Down Expand Up @@ -675,4 +686,11 @@ public async Task<int> SumOfParameterObject(Newtonsoft.Json.Linq.JToken paramObj

internal void OnBoolEvent(bool args) => this.BoolEvent?.Invoke(this, args);
}

internal class ServerOfInternalInterface : IServerInternal
{
public Task<int> AddAsync(int a, int b) => Task.FromResult(a + b);

public Task<int> SubtractAsync(int a, int b) => Task.FromResult(a - b);
}
}
1 change: 1 addition & 0 deletions src/StreamJsonRpc.Tests/StreamJsonRpc.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\StreamJsonRpc.Tests.ExternalAssembly\StreamJsonRpc.Tests.ExternalAssembly.csproj" />
<ProjectReference Include="..\StreamJsonRpc\StreamJsonRpc.csproj" />
</ItemGroup>
<ItemGroup>
Expand Down
6 changes: 6 additions & 0 deletions src/StreamJsonRpc.sln
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "Benchmarks\Benchmarks.csproj", "{CEF0F77F-19EB-4C76-A050-854984BB0364}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StreamJsonRpc.Tests.ExternalAssembly", "StreamJsonRpc.Tests.ExternalAssembly\StreamJsonRpc.Tests.ExternalAssembly.csproj", "{5936CF62-A59D-4E5C-9EE4-5E8BAFA9F215}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -40,6 +42,10 @@ Global
{CEF0F77F-19EB-4C76-A050-854984BB0364}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEF0F77F-19EB-4C76-A050-854984BB0364}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEF0F77F-19EB-4C76-A050-854984BB0364}.Release|Any CPU.Build.0 = Release|Any CPU
{5936CF62-A59D-4E5C-9EE4-5E8BAFA9F215}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5936CF62-A59D-4E5C-9EE4-5E8BAFA9F215}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5936CF62-A59D-4E5C-9EE4-5E8BAFA9F215}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5936CF62-A59D-4E5C-9EE4-5E8BAFA9F215}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
109 changes: 75 additions & 34 deletions src/StreamJsonRpc/ProxyGeneration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,27 @@ namespace StreamJsonRpc
{
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
using Microsoft;

internal static class ProxyGeneration
{
private static readonly Dictionary<ImmutableHashSet<AssemblyName>, ModuleBuilder> TransparentProxyModuleBuilderByVisibilityCheck = new Dictionary<ImmutableHashSet<AssemblyName>, ModuleBuilder>(new ByContentEqualityComparer());
private static readonly object BuilderLock = new object();

private static readonly Type[] EmptyTypes = new Type[0];
private static readonly AssemblyName ProxyAssemblyName = new AssemblyName(string.Format(CultureInfo.InvariantCulture, "StreamJsonRpc_Proxies_{0}", Guid.NewGuid()));
private static readonly MethodInfo DelegateCombineMethod = typeof(Delegate).GetRuntimeMethod(nameof(Delegate.Combine), new Type[] { typeof(Delegate), typeof(Delegate) });
private static readonly MethodInfo DelegateRemoveMethod = typeof(Delegate).GetRuntimeMethod(nameof(Delegate.Remove), new Type[] { typeof(Delegate), typeof(Delegate) });
private static readonly MethodInfo CancellationTokenNonePropertyGetter = typeof(CancellationToken).GetRuntimeProperty(nameof(CancellationToken.None)).GetMethod;
private static readonly AssemblyBuilder AssemblyBuilder;
private static readonly ModuleBuilder ProxyModuleBuilder;
private static readonly ConstructorInfo ObjectCtor = typeof(object).GetTypeInfo().DeclaredConstructors.Single();
private static readonly Dictionary<TypeInfo, TypeInfo> GeneratedProxiesByInterface = new Dictionary<TypeInfo, TypeInfo>();
private static readonly MethodInfo CompareExchangeMethod = (from method in typeof(Interlocked).GetRuntimeMethods()
Expand All @@ -42,16 +45,6 @@ internal static class ProxyGeneration
private static readonly MethodInfo EventNameTransformInvoke = typeof(Func<string, string>).GetRuntimeMethod(nameof(JsonRpcProxyOptions.EventNameTransform.Invoke), new Type[] { typeof(string) });
private static readonly MethodInfo ServerRequiresNamedArgumentsPropertyGetter = typeof(JsonRpcProxyOptions).GetRuntimeProperty(nameof(JsonRpcProxyOptions.ServerRequiresNamedArguments)).GetMethod;

static ProxyGeneration()
{
#if SaveAssembly
AssemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"rpcProxies_{Guid.NewGuid()}"), AssemblyBuilderAccess.RunAndSave);
#else
AssemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"rpcProxies_{Guid.NewGuid()}"), AssemblyBuilderAccess.RunAndCollect);
#endif
ProxyModuleBuilder = AssemblyBuilder.DefineDynamicModule("rpcProxies");
}

/// <summary>
/// Gets a dynamically generated type that implements a given interface in terms of a <see cref="JsonRpc"/> instance.
/// </summary>
Expand All @@ -64,26 +57,25 @@ internal static TypeInfo Get(TypeInfo serviceInterface)

TypeInfo generatedType;

lock (GeneratedProxiesByInterface)
lock (BuilderLock)
{
if (GeneratedProxiesByInterface.TryGetValue(serviceInterface, out generatedType))
{
return generatedType;
}
}

var methodNameMap = new JsonRpc.MethodNameMap(serviceInterface);
ModuleBuilder proxyModuleBuilder = GetProxyModuleBuilder(serviceInterface);

var methodNameMap = new JsonRpc.MethodNameMap(serviceInterface);

lock (ProxyModuleBuilder)
{
var interfaces = new List<Type>
{
serviceInterface.AsType(),
};

interfaces.Add(typeof(IJsonRpcClientProxy));

TypeBuilder proxyTypeBuilder = ProxyModuleBuilder.DefineType(
TypeBuilder proxyTypeBuilder = proxyModuleBuilder.DefineType(
string.Format(CultureInfo.InvariantCulture, "_proxy_{0}_{1}", serviceInterface.FullName, Guid.NewGuid()),
TypeAttributes.Public,
typeof(object),
Expand Down Expand Up @@ -280,7 +272,7 @@ internal static TypeInfo Get(TypeInfo serviceInterface)
{
if (argumentCountExcludingCancellationToken > 0)
{
ConstructorInfo paramObjectCtor = CreateParameterObjectType(methodParameters.Take(argumentCountExcludingCancellationToken).ToArray(), proxyType);
ConstructorInfo paramObjectCtor = CreateParameterObjectType(proxyModuleBuilder, methodParameters.Take(argumentCountExcludingCancellationToken).ToArray(), proxyType);
for (int i = 0; i < argumentCountExcludingCancellationToken; i++)
{
il.Emit(OpCodes.Ldarg, i + 1);
Expand Down Expand Up @@ -367,19 +359,7 @@ internal static TypeInfo Get(TypeInfo serviceInterface)
}

generatedType = proxyTypeBuilder.CreateTypeInfo();
}

lock (GeneratedProxiesByInterface)
{
if (!GeneratedProxiesByInterface.TryGetValue(serviceInterface, out TypeInfo raceGeneratedType))
{
GeneratedProxiesByInterface.Add(serviceInterface, generatedType);
}
else
{
// Ensure we only expose the same generated type externally.
generatedType = raceGeneratedType;
}
GeneratedProxiesByInterface.Add(serviceInterface, generatedType);
}

#if SaveAssembly
Expand All @@ -390,15 +370,52 @@ internal static TypeInfo Get(TypeInfo serviceInterface)
return generatedType;
}

private static ConstructorInfo CreateParameterObjectType(ParameterInfo[] parameters, Type parentType)
/// <summary>
/// Gets the <see cref="ModuleBuilder"/> to use for generating a proxy for the given type.
/// </summary>
/// <param name="interfaceType">The type of the interface to generate a proxy for.</param>
/// <returns>The <see cref="ModuleBuilder"/> to use.</returns>
private static ModuleBuilder GetProxyModuleBuilder(TypeInfo interfaceType)
{
Requires.NotNull(interfaceType, nameof(interfaceType));
Assumes.True(Monitor.IsEntered(BuilderLock));

// Dynamic assemblies are relatively expensive. We want to create as few as possible.
// For each unique set of skip visibility check assemblies, we need a new dynamic assembly
// because the CLR will not honor any additions to that set once the first generated type is closed.
// So maintain a dictionary to point at dynamic modules based on the set of skip visiblity check assemblies they were generated with.
ImmutableHashSet<AssemblyName> skipVisibilityCheckAssemblies = SkipClrVisibilityChecks.GetSkipVisibilityChecksRequirements(interfaceType);
if (!TransparentProxyModuleBuilderByVisibilityCheck.TryGetValue(skipVisibilityCheckAssemblies, out ModuleBuilder moduleBuilder))
{
AssemblyBuilder assemblyBuilder = CreateProxyAssemblyBuilder(typeof(SecurityTransparentAttribute).GetTypeInfo().GetConstructor(Type.EmptyTypes));
moduleBuilder = assemblyBuilder.DefineDynamicModule("rpcProxies");
var skipClrVisibilityChecks = new SkipClrVisibilityChecks(assemblyBuilder, moduleBuilder);
skipClrVisibilityChecks.SkipVisibilityChecksFor(skipVisibilityCheckAssemblies);
TransparentProxyModuleBuilderByVisibilityCheck.Add(skipVisibilityCheckAssemblies, moduleBuilder);
}

return moduleBuilder;
}

private static AssemblyBuilder CreateProxyAssemblyBuilder(ConstructorInfo constructorInfo)
{
var proxyAssemblyName = new AssemblyName(string.Format(CultureInfo.InvariantCulture, "rpcProxies_{0}", Guid.NewGuid()));
#if SaveAssembly
return AssemblyBuilder.DefineDynamicAssembly(proxyAssemblyName, AssemblyBuilderAccess.RunAndSave);
#else
return AssemblyBuilder.DefineDynamicAssembly(proxyAssemblyName, AssemblyBuilderAccess.RunAndCollect);
#endif
}

private static ConstructorInfo CreateParameterObjectType(ModuleBuilder moduleBuilder, ParameterInfo[] parameters, Type parentType)
{
Requires.NotNull(parameters, nameof(parameters));
if (parameters.Length == 0)
{
return ObjectCtor;
}

TypeBuilder proxyTypeBuilder = ProxyModuleBuilder.DefineType(
TypeBuilder proxyTypeBuilder = moduleBuilder.DefineType(
string.Format(CultureInfo.InvariantCulture, "_param_{0}", Guid.NewGuid()),
TypeAttributes.NotPublic);

Expand Down Expand Up @@ -540,5 +557,29 @@ private static IEnumerable<T> FindAllOnThisAndOtherInterfaces<T>(TypeInfo interf
IEnumerable<T> result = oneInterfaceQuery(interfaceType);
return result.Concat(interfaceType.ImplementedInterfaces.SelectMany(i => oneInterfaceQuery(i.GetTypeInfo())));
}

private class ByContentEqualityComparer : IEqualityComparer<ImmutableHashSet<AssemblyName>>
{
public bool Equals(ImmutableHashSet<AssemblyName> x, ImmutableHashSet<AssemblyName> y)
{
if (x.Count != y.Count)
{
return false;
}

return !x.Except(y).Any();
}

public int GetHashCode(ImmutableHashSet<AssemblyName> obj)
{
int hashCode = 0;
foreach (AssemblyName item in obj)
{
hashCode += item.GetHashCode();
}

return hashCode;
}
}
}
}
Loading

0 comments on commit de7e17c

Please sign in to comment.