Merge pull request #1066 from AArnott/multiIfaceProxies

Enable generation of proxies that implement multiple interfaces
This commit is contained in:
Andrew Arnott 2024-07-26 11:09:51 -06:00 коммит произвёл GitHub
Родитель dc105fd1a1 ca1b9163dc
Коммит 53dd557306
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
9 изменённых файлов: 270 добавлений и 116 удалений

Просмотреть файл

@ -655,7 +655,9 @@ public class JsonRpc : IDisposableObservable, IJsonRpcFormatterCallbacks, IJsonR
/// <param name="stream">A bidirectional stream to send and receive RPC messages on.</param>
/// <param name="target">An optional target object to invoke when incoming RPC requests arrive.</param>
/// <returns>The initialized and listening <see cref="JsonRpc"/> object.</returns>
#pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads
public static JsonRpc Attach(Stream stream, object? target = null)
#pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads
{
Requires.NotNull(stream, nameof(stream));
@ -726,7 +728,7 @@ public class JsonRpc : IDisposableObservable, IJsonRpcFormatterCallbacks, IJsonR
where T : class
{
var rpc = new JsonRpc(sendingStream, receivingStream);
T proxy = rpc.CreateProxy<T>(null, JsonRpcProxyOptions.Default, null);
T proxy = rpc.CreateProxy<T>(default, null, JsonRpcProxyOptions.Default, null);
rpc.StartListening();
return proxy;
}
@ -762,7 +764,7 @@ public class JsonRpc : IDisposableObservable, IJsonRpcFormatterCallbacks, IJsonR
where T : class
{
var rpc = new JsonRpc(handler);
T proxy = rpc.CreateProxy<T>(null, options, null)!;
T proxy = rpc.CreateProxy<T>(default, null, options, null)!;
rpc.StartListening();
return proxy;
}
@ -787,7 +789,7 @@ public class JsonRpc : IDisposableObservable, IJsonRpcFormatterCallbacks, IJsonR
public T Attach<T>(JsonRpcProxyOptions? options)
where T : class
{
return this.CreateProxy<T>(null, options, null);
return this.CreateProxy<T>(default, null, options, null);
}
/// <summary>
@ -806,7 +808,19 @@ public class JsonRpc : IDisposableObservable, IJsonRpcFormatterCallbacks, IJsonR
public object Attach(Type interfaceType, JsonRpcProxyOptions? options)
{
Requires.NotNull(interfaceType, nameof(interfaceType));
return this.CreateProxy(interfaceType.GetTypeInfo(), null, options ?? JsonRpcProxyOptions.Default, null)!;
return this.CreateProxy(interfaceType.GetTypeInfo(), default, null, options ?? JsonRpcProxyOptions.Default, null)!;
}
/// <summary>
/// Creates a JSON-RPC client proxy that conforms to the specified server interfaces.
/// </summary>
/// <param name="interfaceTypes">The interfaces that describes the functions available on the remote end.</param>
/// <param name="options">A set of customizations for how the client proxy is wired up. If <see langword="null"/>, default options will be used.</param>
/// <returns>An instance of the generated proxy.</returns>
public object Attach(ReadOnlySpan<Type> interfaceTypes, JsonRpcProxyOptions? options)
{
Requires.Argument(interfaceTypes.Length > 0, nameof(interfaceTypes), Resources.RequiredArgumentMissing);
return this.CreateProxy(interfaceTypes[0], interfaceTypes.Slice(1), null, options ?? JsonRpcProxyOptions.Default, null)!;
}
/// <inheritdoc cref="AddLocalRpcTarget(object, JsonRpcTargetOptions?)"/>
@ -1201,10 +1215,10 @@ public class JsonRpc : IDisposableObservable, IJsonRpcFormatterCallbacks, IJsonR
throw new NotImplementedException();
}
/// <inheritdoc cref="CreateProxy(TypeInfo, ValueTuple{TypeInfo, int}[], JsonRpcProxyOptions?, long?)"/>
internal object Attach(Type contractInterface, (TypeInfo Type, int Code)[]? implementedOptionalInterfaces, JsonRpcProxyOptions? options, long? marshaledObjectHandle)
/// <inheritdoc cref="CreateProxy(Type, ReadOnlySpan{Type}, ReadOnlySpan{ValueTuple{Type, int}}, JsonRpcProxyOptions?, long?)"/>
internal object Attach(Type contractInterface, (Type Type, int Code)[]? implementedOptionalInterfaces, JsonRpcProxyOptions? options, long? marshaledObjectHandle)
{
return this.CreateProxy(contractInterface.GetTypeInfo(), implementedOptionalInterfaces, options, marshaledObjectHandle);
return this.CreateProxy(contractInterface.GetTypeInfo(), default, implementedOptionalInterfaces, options, marshaledObjectHandle);
}
/// <inheritdoc cref="RpcTargetInfo.AddLocalRpcMethod(MethodInfo, object?, JsonRpcMethodAttribute?, SynchronizationContext?)"/>
@ -2680,23 +2694,24 @@ public class JsonRpc : IDisposableObservable, IJsonRpcFormatterCallbacks, IJsonR
}
}
private T CreateProxy<T>((TypeInfo Type, int Code)[]? implementedOptionalInterfaces, JsonRpcProxyOptions? options, long? marshaledObjectHandle)
private T CreateProxy<T>(ReadOnlySpan<Type> additionalContractInterfaces, ReadOnlySpan<(Type Type, int Code)> implementedOptionalInterfaces, JsonRpcProxyOptions? options, long? marshaledObjectHandle)
where T : class
{
return (T)this.CreateProxy(typeof(T).GetTypeInfo(), implementedOptionalInterfaces, options, marshaledObjectHandle);
return (T)this.CreateProxy(typeof(T).GetTypeInfo(), additionalContractInterfaces, implementedOptionalInterfaces, options, marshaledObjectHandle);
}
/// <summary>
/// Creates a JSON-RPC client proxy that implements a given set of interfaces.
/// </summary>
/// <param name="contractInterface">The interface that describes the functions available on the remote end.</param>
/// <param name="additionalContractInterfaces"><inheritdoc cref="ProxyGeneration.Get" path="/param[@name='additionalContractInterfaces']"/></param>
/// <param name="implementedOptionalInterfaces">Additional marshalable interfaces that the client proxy should implement.</param>
/// <param name="options">A set of customizations for how the client proxy is wired up. If <see langword="null" />, default options will be used.</param>
/// <param name="marshaledObjectHandle">The handle to the remote object that is being marshaled via this proxy.</param>
/// <returns>An instance of the generated proxy.</returns>
private IJsonRpcClientProxyInternal CreateProxy(TypeInfo contractInterface, (TypeInfo Type, int Code)[]? implementedOptionalInterfaces, JsonRpcProxyOptions? options, long? marshaledObjectHandle)
private IJsonRpcClientProxyInternal CreateProxy(Type contractInterface, ReadOnlySpan<Type> additionalContractInterfaces, ReadOnlySpan<(Type Type, int Code)> implementedOptionalInterfaces, JsonRpcProxyOptions? options, long? marshaledObjectHandle)
{
TypeInfo proxyType = ProxyGeneration.Get(contractInterface, implementedOptionalInterfaces);
TypeInfo proxyType = ProxyGeneration.Get(contractInterface, additionalContractInterfaces, implementedOptionalInterfaces);
return (IJsonRpcClientProxyInternal)Activator.CreateInstance(
proxyType.AsType(),
this,

Просмотреть файл

@ -51,54 +51,77 @@ internal static class ProxyGeneration
/// <summary>
/// Gets a dynamically generated type that implements a given interface in terms of a <see cref="JsonRpc"/> instance.
/// </summary>
/// <param name="contractInterface">The interface that describes the RPC contract, and that the client proxy should implement.</param>
/// <param name="implementedOptionalInterfaces">Additional marshalable interfaces that the client proxy should implement.</param>
/// <param name="contractInterface">
/// The interface that describes the RPC contract, and that the client proxy should implement.
/// </param>
/// <param name="additionalContractInterfaces">
/// An optional list of additional interfaces that the client proxy should implement <em>without</em> the name transformation or event limitations
/// involved with <paramref name="implementedOptionalInterfaces"/>.
/// This set should have an empty intersection with <paramref name="implementedOptionalInterfaces"/>.
/// </param>
/// <param name="implementedOptionalInterfaces">
/// Additional marshalable interfaces that the client proxy should implement.
/// Methods on these interfaces are invoke using a special name transformation that includes an integer code,
/// ensuring that methods do not suffer from name collisions across interfaces.
/// </param>
/// <returns>The generated type.</returns>
internal static TypeInfo Get(TypeInfo contractInterface, (TypeInfo Type, int Code)[]? implementedOptionalInterfaces = null)
internal static TypeInfo Get(Type contractInterface, ReadOnlySpan<Type> additionalContractInterfaces, ReadOnlySpan<(Type Type, int Code)> implementedOptionalInterfaces)
{
Requires.NotNull(contractInterface, nameof(contractInterface));
VerifySupported(contractInterface.IsInterface, Resources.ClientProxyTypeArgumentMustBeAnInterface, contractInterface);
if (implementedOptionalInterfaces is not null)
foreach (TypeInfo additionalContract in additionalContractInterfaces)
{
foreach ((TypeInfo type, _) in implementedOptionalInterfaces)
{
VerifySupported(type.IsInterface, Resources.ClientProxyTypeArgumentMustBeAnInterface, type);
}
VerifySupported(additionalContract.IsInterface, Resources.ClientProxyTypeArgumentMustBeAnInterface, additionalContract);
}
var rpcInterfaces = new List<(TypeInfo Type, int? Code)>
{
(contractInterface, (int?)null),
};
if (implementedOptionalInterfaces is not null)
foreach ((Type type, _) in implementedOptionalInterfaces)
{
rpcInterfaces.AddRange(implementedOptionalInterfaces.Select(i => (i.Type, (int?)i.Code)));
VerifySupported(type.IsInterface, Resources.ClientProxyTypeArgumentMustBeAnInterface, type);
}
// Rpc interfaces must be sorted so that we implement methods from base interfaces before those from their derivations.
SortRpcInterfaces(rpcInterfaces);
TypeInfo? generatedType;
GeneratedProxiesByInterfaceKey generatedProxyKey = new(contractInterface, implementedOptionalInterfaces?.Select(i => i.Code));
lock (BuilderLock)
{
GeneratedProxiesByInterfaceKey generatedProxyKey = new(contractInterface.GetTypeInfo(), additionalContractInterfaces, implementedOptionalInterfaces);
if (GeneratedProxiesByInterface.TryGetValue(generatedProxyKey, out generatedType))
{
return generatedType;
}
ModuleBuilder proxyModuleBuilder = GetProxyModuleBuilder(contractInterface);
Type[] contractInterfaces = [contractInterface, .. additionalContractInterfaces];
List<(TypeInfo Type, int? Code)> rpcInterfaces = new(1 + additionalContractInterfaces.Length + implementedOptionalInterfaces.Length);
rpcInterfaces.Add((contractInterface.GetTypeInfo(), null));
foreach (Type addl in additionalContractInterfaces)
{
rpcInterfaces.Add((addl.GetTypeInfo(), null));
}
var proxyInterfaces = rpcInterfaces.Select(i => i.Type.AsType()).ToList();
proxyInterfaces.Add(typeof(IJsonRpcClientProxy));
proxyInterfaces.Add(typeof(IJsonRpcClientProxyInternal));
foreach ((Type type, int code) in implementedOptionalInterfaces)
{
rpcInterfaces.Add((type.GetTypeInfo(), code));
}
// Ensure types are not specified multiple times anywhere.
HashSet<TypeInfo> seenTypes = new();
foreach ((TypeInfo type, _) in rpcInterfaces)
{
if (!seenTypes.Add(type))
{
throw new ArgumentException(Resources.InterfacesMustBeUnique);
}
}
// Rpc interfaces must be sorted so that we implement methods from base interfaces before those from their derivations.
SortRpcInterfaces(rpcInterfaces);
Type[] proxyInterfaces = [.. rpcInterfaces.Select(i => i.Type), typeof(IJsonRpcClientProxy), typeof(IJsonRpcClientProxyInternal)];
ModuleBuilder proxyModuleBuilder = GetProxyModuleBuilder(proxyInterfaces);
TypeBuilder proxyTypeBuilder = proxyModuleBuilder.DefineType(
string.Format(CultureInfo.InvariantCulture, "_proxy_{0}_{1}", contractInterface.FullName, Guid.NewGuid()),
TypeAttributes.Public,
typeof(object),
proxyInterfaces.ToArray());
proxyInterfaces);
Type proxyType = proxyTypeBuilder;
const FieldAttributes fieldAttributes = FieldAttributes.Private | FieldAttributes.InitOnly;
@ -110,63 +133,66 @@ internal static class ProxyGeneration
FieldBuilder calledMethodField = proxyTypeBuilder.DefineField("calledMethod", typeof(EventHandler<string>), FieldAttributes.Private);
FieldBuilder marshaledObjectHandleField = proxyTypeBuilder.DefineField("marshaledObjectHandle", typeof(long?), fieldAttributes);
VerifySupported(!FindAllOnThisAndOtherInterfaces(contractInterface, i => i.DeclaredProperties).Any(), Resources.UnsupportedPropertiesOnClientProxyInterface, contractInterface);
// Implement events only on the main interface.
// We don't implement events for the additional interfaces because we don't support events in marshaled interfaces.
var ctorActions = new List<Action<ILGenerator>>();
foreach (EventInfo evt in FindAllOnThisAndOtherInterfaces(contractInterface, i => i.DeclaredEvents))
foreach (TypeInfo contract in contractInterfaces)
{
VerifySupported(evt.EventHandlerType!.Equals(typeof(EventHandler)) || (evt.EventHandlerType.GetTypeInfo().IsGenericType && evt.EventHandlerType.GetGenericTypeDefinition().Equals(typeof(EventHandler<>))), Resources.UnsupportedEventHandlerTypeOnClientProxyInterface, evt);
VerifySupported(!FindAllOnThisAndOtherInterfaces(contract, i => i.DeclaredProperties).Any(), Resources.UnsupportedPropertiesOnClientProxyInterface, contract);
// public event EventHandler EventName;
EventBuilder evtBuilder = proxyTypeBuilder.DefineEvent(evt.Name, evt.Attributes, evt.EventHandlerType);
// private EventHandler eventName;
FieldBuilder evtField = proxyTypeBuilder.DefineField(evt.Name, evt.EventHandlerType, FieldAttributes.Private);
// add_EventName
var addRemoveHandlerParams = new Type[] { evt.EventHandlerType };
const MethodAttributes methodAttributes = MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.NewSlot | MethodAttributes.Virtual;
MethodBuilder addMethod = proxyTypeBuilder.DefineMethod($"add_{evt.Name}", methodAttributes, null, addRemoveHandlerParams);
ImplementEventAccessor(addMethod.GetILGenerator(), evtField, DelegateCombineMethod);
evtBuilder.SetAddOnMethod(addMethod);
// remove_EventName
MethodBuilder removeMethod = proxyTypeBuilder.DefineMethod($"remove_{evt.Name}", methodAttributes, null, addRemoveHandlerParams);
ImplementEventAccessor(removeMethod.GetILGenerator(), evtField, DelegateRemoveMethod);
evtBuilder.SetRemoveOnMethod(removeMethod);
// void OnEventName(EventArgs args)
Type eventArgsType = evt.EventHandlerType.GetTypeInfo().GetDeclaredMethod(nameof(EventHandler.Invoke))!.GetParameters()[1].ParameterType;
MethodBuilder raiseEventMethod = proxyTypeBuilder.DefineMethod(
$"On{evt.Name}",
MethodAttributes.HideBySig | MethodAttributes.Private,
null,
new Type[] { eventArgsType });
ImplementRaiseEventMethod(raiseEventMethod.GetILGenerator(), evtField, jsonRpcField);
ctorActions.Add(new Action<ILGenerator>(il =>
// Implement events only on the main interface.
// We don't implement events for the additional interfaces because we don't support events in marshaled interfaces.
foreach (EventInfo evt in FindAllOnThisAndOtherInterfaces(contract, i => i.DeclaredEvents))
{
ConstructorInfo delegateCtor = typeof(Action<>).MakeGenericType(eventArgsType).GetTypeInfo().DeclaredConstructors.Single();
VerifySupported(evt.EventHandlerType!.Equals(typeof(EventHandler)) || (evt.EventHandlerType.GetTypeInfo().IsGenericType && evt.EventHandlerType.GetGenericTypeDefinition().Equals(typeof(EventHandler<>))), Resources.UnsupportedEventHandlerTypeOnClientProxyInterface, evt);
// rpc.AddLocalRpcMethod("EventName", new Action<EventArgs>(this.OnEventName));
il.Emit(OpCodes.Ldarg_1); // .ctor's rpc parameter
// public event EventHandler EventName;
EventBuilder evtBuilder = proxyTypeBuilder.DefineEvent(evt.Name, evt.Attributes, evt.EventHandlerType);
// First argument to AddLocalRpcMethod is the method name.
// Run it through the method name transform.
// this.options.EventNameTransform.Invoke("clrOrAttributedMethodName")
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, optionsField);
il.EmitCall(OpCodes.Callvirt, EventNameTransformPropertyGetter, null);
il.Emit(OpCodes.Ldstr, evt.Name);
il.EmitCall(OpCodes.Callvirt, EventNameTransformInvoke, null);
// private EventHandler eventName;
FieldBuilder evtField = proxyTypeBuilder.DefineField(evt.Name, evt.EventHandlerType, FieldAttributes.Private);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldftn, raiseEventMethod);
il.Emit(OpCodes.Newobj, delegateCtor);
il.Emit(OpCodes.Callvirt, AddLocalRpcMethodMethodInfo);
}));
// add_EventName
var addRemoveHandlerParams = new Type[] { evt.EventHandlerType };
const MethodAttributes methodAttributes = MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.NewSlot | MethodAttributes.Virtual;
MethodBuilder addMethod = proxyTypeBuilder.DefineMethod($"add_{evt.Name}", methodAttributes, null, addRemoveHandlerParams);
ImplementEventAccessor(addMethod.GetILGenerator(), evtField, DelegateCombineMethod);
evtBuilder.SetAddOnMethod(addMethod);
// remove_EventName
MethodBuilder removeMethod = proxyTypeBuilder.DefineMethod($"remove_{evt.Name}", methodAttributes, null, addRemoveHandlerParams);
ImplementEventAccessor(removeMethod.GetILGenerator(), evtField, DelegateRemoveMethod);
evtBuilder.SetRemoveOnMethod(removeMethod);
// void OnEventName(EventArgs args)
Type eventArgsType = evt.EventHandlerType.GetTypeInfo().GetDeclaredMethod(nameof(EventHandler.Invoke))!.GetParameters()[1].ParameterType;
MethodBuilder raiseEventMethod = proxyTypeBuilder.DefineMethod(
$"On{evt.Name}",
MethodAttributes.HideBySig | MethodAttributes.Private,
null,
new Type[] { eventArgsType });
ImplementRaiseEventMethod(raiseEventMethod.GetILGenerator(), evtField, jsonRpcField);
ctorActions.Add(new Action<ILGenerator>(il =>
{
ConstructorInfo delegateCtor = typeof(Action<>).MakeGenericType(eventArgsType).GetTypeInfo().DeclaredConstructors.Single();
// rpc.AddLocalRpcMethod("EventName", new Action<EventArgs>(this.OnEventName));
il.Emit(OpCodes.Ldarg_1); // .ctor's rpc parameter
// First argument to AddLocalRpcMethod is the method name.
// Run it through the method name transform.
// this.options.EventNameTransform.Invoke("clrOrAttributedMethodName")
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, optionsField);
il.EmitCall(OpCodes.Callvirt, EventNameTransformPropertyGetter, null);
il.Emit(OpCodes.Ldstr, evt.Name);
il.EmitCall(OpCodes.Callvirt, EventNameTransformInvoke, null);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldftn, raiseEventMethod);
il.Emit(OpCodes.Newobj, delegateCtor);
il.Emit(OpCodes.Callvirt, AddLocalRpcMethodMethodInfo);
}));
}
}
// .ctor(JsonRpc, JsonRpcProxyOptions, long? marshaledObjectHandle, Action onDispose)
@ -251,10 +277,10 @@ internal static class ProxyGeneration
MethodInfo notifyWithParameterObjectAsyncOfTaskMethodInfo = notifyWithParameterObjectAsyncMethodInfos.Single(m => !m.IsGenericMethod && m.GetParameters().Length == 2);
HashSet<MethodInfo> implementedMethods = new() { DisposeMethod };
foreach ((TypeInfo rpcInterface, int? rpcInterfaceCode) in rpcInterfaces)
foreach ((Type rpcInterface, int? rpcInterfaceCode) in rpcInterfaces)
{
RpcTargetInfo.MethodNameMap methodNameMap = RpcTargetInfo.GetMethodNameMap(rpcInterface);
foreach (MethodInfo method in FindAllOnThisAndOtherInterfaces(rpcInterface, i => i.DeclaredMethods).Where(m => !m.IsSpecialName))
RpcTargetInfo.MethodNameMap methodNameMap = RpcTargetInfo.GetMethodNameMap(rpcInterface.GetTypeInfo());
foreach (MethodInfo method in FindAllOnThisAndOtherInterfaces(rpcInterface.GetTypeInfo(), i => i.DeclaredMethods).Where(m => !m.IsSpecialName))
{
if (!implementedMethods.Add(method))
{
@ -423,7 +449,7 @@ internal static class ProxyGeneration
}
generatedType = proxyTypeBuilder.CreateTypeInfo()!;
GeneratedProxiesByInterface.Add(generatedProxyKey, generatedType);
GeneratedProxiesByInterface.Add(generatedProxyKey.Clone(), generatedType);
#if SaveAssembly
((AssemblyBuilder)proxyModuleBuilder.Assembly).Save(proxyModuleBuilder.ScopeName);
@ -724,18 +750,18 @@ internal static class ProxyGeneration
/// <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>
/// <param name="interfaceTypes">The interface types to generate a proxy for.</param>
/// <returns>The <see cref="ModuleBuilder"/> to use.</returns>
private static ModuleBuilder GetProxyModuleBuilder(TypeInfo interfaceType)
private static ModuleBuilder GetProxyModuleBuilder(Type[] interfaceTypes)
{
Requires.NotNull(interfaceType, nameof(interfaceType));
Requires.NotNull(interfaceTypes, nameof(interfaceTypes));
Assumes.True(Monitor.IsEntered(BuilderLock));
// Dynamic assemblies are relatively expensive. We want to create as few as possible.
// For each set of skip visibility check assemblies, we need a dynamic assembly that skips at *least* that set.
// The CLR will not honor any additions to that set once the first generated type is closed.
// We 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)
// We maintain a dictionary to point at dynamic modules based on the set of skip visibility check assemblies they were generated with.
ImmutableHashSet<AssemblyName> skipVisibilityCheckAssemblies = ImmutableHashSet.CreateRange(interfaceTypes.SelectMany(t => SkipClrVisibilityChecks.GetSkipVisibilityChecksRequirements(t.GetTypeInfo())))
.Add(typeof(ProxyGeneration).Assembly.GetName());
foreach ((ImmutableHashSet<AssemblyName> SkipVisibilitySet, ModuleBuilder Builder) existingSet in TransparentProxyModuleBuilderByVisibilityCheck)
{
@ -926,40 +952,78 @@ internal static class ProxyGeneration
private struct GeneratedProxiesByInterfaceKey : IEquatable<GeneratedProxiesByInterfaceKey>
{
private readonly TypeInfo baseInterfaceType;
private readonly ReadOnlyMemory<Type> additionalContractTypes;
private readonly ReadOnlyMemory<int> implementedOptionalInterfaces;
private readonly int[]? implementedOptionalInterfaces;
public GeneratedProxiesByInterfaceKey(TypeInfo baseInterfaceType, IEnumerable<int>? implementedOptionalInterfaces)
public GeneratedProxiesByInterfaceKey(TypeInfo baseInterfaceType, ReadOnlySpan<Type> additionalContractTypes, ReadOnlySpan<(Type Type, int Code)> implementedOptionalInterfaces)
{
this.baseInterfaceType = baseInterfaceType;
this.implementedOptionalInterfaces = implementedOptionalInterfaces?.OrderBy(n => n).ToArray();
this.additionalContractTypes = additionalContractTypes.ToArray();
if (implementedOptionalInterfaces.Length > 0)
{
int[] optionals = new int[implementedOptionalInterfaces.Length];
for (int i = 0; i < implementedOptionalInterfaces.Length; i++)
{
optionals[i] = implementedOptionalInterfaces[i].Code;
}
Array.Sort(optionals);
this.implementedOptionalInterfaces = optionals;
}
}
private GeneratedProxiesByInterfaceKey(GeneratedProxiesByInterfaceKey cloneFrom)
{
this.baseInterfaceType = cloneFrom.baseInterfaceType;
this.additionalContractTypes = cloneFrom.additionalContractTypes.ToArray(); // deep clone the object we originally got from the user so it can't change.
this.implementedOptionalInterfaces = cloneFrom.implementedOptionalInterfaces;
}
public bool Equals(GeneratedProxiesByInterfaceKey other)
{
return this.baseInterfaceType == other.baseInterfaceType &&
((this.implementedOptionalInterfaces is null && other.implementedOptionalInterfaces is null) ||
(this.implementedOptionalInterfaces is not null && other.implementedOptionalInterfaces is not null && this.implementedOptionalInterfaces.SequenceEqual(other.implementedOptionalInterfaces)));
if (this.baseInterfaceType != other.baseInterfaceType ||
this.additionalContractTypes.Length != other.additionalContractTypes.Length ||
this.implementedOptionalInterfaces.Length != other.implementedOptionalInterfaces.Length)
{
return false;
}
if (!this.implementedOptionalInterfaces.Span.SequenceEqual(other.implementedOptionalInterfaces.Span))
{
return false;
}
for (int i = 0; i < this.additionalContractTypes.Length; i++)
{
if (!this.additionalContractTypes.Span[i].IsEquivalentTo(other.additionalContractTypes.Span[i]))
{
return false;
}
}
return true;
}
public override bool Equals(object? obj)
{
return obj is GeneratedProxiesByInterfaceKey other && this.Equals(other);
}
public override bool Equals(object? obj) => obj is GeneratedProxiesByInterfaceKey other && this.Equals(other);
public override int GetHashCode()
{
int hashCode = this.baseInterfaceType.GetHashCode();
if (this.implementedOptionalInterfaces is not null)
foreach (Type subType in this.additionalContractTypes.Span)
{
foreach (int subType in this.implementedOptionalInterfaces)
{
hashCode = (hashCode * 31) + subType.GetHashCode();
}
hashCode = (hashCode * 31) + subType.GetHashCode();
}
foreach (int subType in this.implementedOptionalInterfaces.Span)
{
hashCode = (hashCode * 31) + subType.GetHashCode();
}
return hashCode;
}
internal GeneratedProxiesByInterfaceKey Clone() => new(this);
}
/// <summary>

Просмотреть файл

@ -309,7 +309,7 @@ internal class MessageFormatterRpcMarshaledContextTracker
{
CallScopedLifetime = token.Value.Lifetime == MarshalLifetime.Call,
};
List<(TypeInfo Type, int Code)>? optionalInterfaces = null;
List<(Type Type, int Code)>? optionalInterfaces = null;
if (token.Value.OptionalInterfacesCodes?.Length > 0)
{
// We ignore unknown optional interface codes

Просмотреть файл

@ -384,4 +384,7 @@
<data name="UsableOnceOnly" xml:space="preserve">
<value>This operation can only be performed once on this object.</value>
</data>
<data name="InterfacesMustBeUnique" xml:space="preserve">
<value>Proxy requested with non-unique set of interfaces.</value>
</data>
</root>

Просмотреть файл

@ -0,0 +1 @@
StreamJsonRpc.JsonRpc.Attach(System.ReadOnlySpan<System.Type!> interfaceTypes, StreamJsonRpc.JsonRpcProxyOptions? options) -> object!

Просмотреть файл

@ -0,0 +1 @@
StreamJsonRpc.JsonRpc.Attach(System.ReadOnlySpan<System.Type!> interfaceTypes, StreamJsonRpc.JsonRpcProxyOptions? options) -> object!

Просмотреть файл

@ -0,0 +1 @@
StreamJsonRpc.JsonRpc.Attach(System.ReadOnlySpan<System.Type!> interfaceTypes, StreamJsonRpc.JsonRpcProxyOptions? options) -> object!

Просмотреть файл

@ -0,0 +1 @@
StreamJsonRpc.JsonRpc.Attach(System.ReadOnlySpan<System.Type!> interfaceTypes, StreamJsonRpc.JsonRpcProxyOptions? options) -> object!

Просмотреть файл

@ -1,12 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using Microsoft;
using Microsoft.VisualStudio.Threading;
using Nerdbank;
using StreamJsonRpc;
using Xunit;
using Xunit.Abstractions;
using ExAssembly = StreamJsonRpc.Tests.ExternalAssembly;
public class JsonRpcProxyGenerationTests : TestBase
@ -54,6 +50,11 @@ public class JsonRpcProxyGenerationTests : TestBase
Task Dispose();
}
public interface IServerWithMoreEvents
{
event EventHandler AnotherEvent;
}
public interface IServerDerived : IServer
{
Task HeavyWorkAsync(CancellationToken cancellationToken);
@ -166,6 +167,69 @@ public class JsonRpcProxyGenerationTests : TestBase
Assert.IsType(this.clientRpc.GetType(), clientRpc);
}
[Fact]
public async Task Attach_MultipleInterfaces()
{
var streams = FullDuplexStream.CreateStreams();
JsonRpc serverRpc = JsonRpc.Attach(streams.Item2, this.server);
JsonRpc clientRpc = new(streams.Item1);
object clientProxy = clientRpc.Attach([typeof(IServer), typeof(IServer2), typeof(IServer3), typeof(IServerWithMoreEvents)], null);
IServer client1 = Assert.IsAssignableFrom<IServer>(clientProxy);
IServer2 client2 = Assert.IsAssignableFrom<IServer2>(clientProxy);
IServer3 client3 = Assert.IsAssignableFrom<IServer3>(clientProxy);
IServerWithMoreEvents client4 = Assert.IsAssignableFrom<IServerWithMoreEvents>(clientProxy);
Assert.IsNotAssignableFrom<IServerDerived>(clientProxy);
clientRpc.StartListening();
Assert.Equal("Hi!", await client1.SayHiAsync().WithCancellation(this.TimeoutToken));
Assert.Equal(6, await client2.MultiplyAsync(2, 3).WithCancellation(this.TimeoutToken));
Assert.Equal("TEST", await client3.ARoseByAsync("test").WithCancellation(this.TimeoutToken));
// Test events across multiple interfaces.
AsyncManualResetEvent itHappenedCompletion = new(), anotherEventCompletion = new();
client1.ItHappened += (s, e) => itHappenedCompletion.Set();
client4.AnotherEvent += (s, e) => anotherEventCompletion.Set();
this.server.OnItHappened(EventArgs.Empty);
this.server.OnAnotherEvent(EventArgs.Empty);
await itHappenedCompletion.WaitAsync().WithCancellation(this.TimeoutToken);
await anotherEventCompletion.WaitAsync().WithCancellation(this.TimeoutToken);
}
[Fact]
public void Attach_MultipleInterfaces_TypeReuse()
{
var streams = FullDuplexStream.CreateStreams();
var rpc = new JsonRpc(streams.Item1);
object clientRpc12a = rpc.Attach([typeof(IServer), typeof(IServer2)], null);
streams = FullDuplexStream.CreateStreams();
rpc = new JsonRpc(streams.Item1);
object clientRpc12b = rpc.Attach([typeof(IServer), typeof(IServer2)], null);
Assert.Same(clientRpc12a.GetType(), clientRpc12b.GetType());
Assert.IsAssignableFrom<IServer>(clientRpc12a);
Assert.IsAssignableFrom<IServer2>(clientRpc12a);
streams = FullDuplexStream.CreateStreams();
rpc = new JsonRpc(streams.Item1);
object clientRpc13 = rpc.Attach([typeof(IServer), typeof(IServer3)], null);
Assert.NotSame(clientRpc12a.GetType(), clientRpc13.GetType());
Assert.IsAssignableFrom<IServer>(clientRpc13);
Assert.IsAssignableFrom<IServer3>(clientRpc13);
Assert.IsNotAssignableFrom<IServer2>(clientRpc13);
}
[Fact]
public void Attach_NonUniqueList()
{
var streams = FullDuplexStream.CreateStreams();
var rpc = new JsonRpc(streams.Item1);
var ex = Assert.Throws<ArgumentException>(() => rpc.Attach([typeof(IServer), typeof(IServer)], null));
this.Logger.WriteLine($"{ex.ParamName}: {ex.Message}");
}
[Fact]
public void ProxyTypeIsReused()
{
@ -726,7 +790,7 @@ public class JsonRpcProxyGenerationTests : TestBase
public string? Color { get; set; }
}
internal class Server : IServerDerived, IServer2, IServer3, IServerWithValueTasks, IServerWithVoidReturnType
internal class Server : IServerDerived, IServer2, IServer3, IServerWithValueTasks, IServerWithVoidReturnType, IServerWithMoreEvents
{
public event EventHandler? ItHappened;
@ -736,6 +800,8 @@ public class JsonRpcProxyGenerationTests : TestBase
public event EventHandler<bool>? BoolEvent;
public event EventHandler? AnotherEvent;
public AsyncManualResetEvent MethodEntered { get; } = new AsyncManualResetEvent();
public AsyncManualResetEvent ResumeMethod { get; } = new AsyncManualResetEvent(initialState: true);
@ -814,6 +880,8 @@ public class JsonRpcProxyGenerationTests : TestBase
internal void OnAppleGrown(CustomNonDerivingEventArgs args) => this.AppleGrown?.Invoke(this, args);
internal void OnBoolEvent(bool args) => this.BoolEvent?.Invoke(this, args);
internal void OnAnotherEvent(EventArgs args) => this.AnotherEvent?.Invoke(this, args);
}
internal class Server2 : IServer2