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 support for dynamic proxies with non-public interfaces #342

Merged
merged 1 commit into from
Sep 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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