Skip to content

Commit

Permalink
Adding Rpc marshaling support for custom interfaces.
Browse files Browse the repository at this point in the history
  • Loading branch information
Matteo Prosperi committed Mar 10, 2022
1 parent 114ee4f commit 3570d33
Show file tree
Hide file tree
Showing 12 changed files with 837 additions and 81 deletions.
102 changes: 52 additions & 50 deletions src/StreamJsonRpc/JsonMessageFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ public class JsonMessageFormatter : IJsonRpcAsyncMessageTextFormatter, IJsonRpcF
/// </remarks>
private static readonly JsonSerializer DefaultSerializer = JsonSerializer.Create();

private readonly IReadOnlyDictionary<Type, RpcMarshalableImplicitConverter> implicitlyMarshaledTypes;

/// <summary>
/// The reusable <see cref="TextWriter"/> to use with newtonsoft.json's serializer.
/// </summary>
Expand Down Expand Up @@ -199,19 +197,9 @@ public JsonMessageFormatter(Encoding encoding)
new PipeWriterConverter(this),
new StreamConverter(this),
new ExceptionConverter(this),
new RpcMarshalableReadConverter(this),
},
};

var camelCaseProxyOptions = new JsonRpcProxyOptions { MethodNameTransform = CommonMethodNameTransforms.CamelCase };
var camelCaseTargetOptions = new JsonRpcTargetOptions { MethodNameTransform = CommonMethodNameTransforms.CamelCase };
this.implicitlyMarshaledTypes = MessageFormatterRpcMarshaledContextTracker.ImplicitlyMarshaledTypes.ToDictionary(
t => t.ImplicitlyMarshaledType,
t => new RpcMarshalableImplicitConverter(t.ImplicitlyMarshaledType, this, t.ProxyOptions, t.TargetOptions));

foreach (KeyValuePair<Type, RpcMarshalableImplicitConverter> implicitlyMarshaledType in this.implicitlyMarshaledTypes)
{
this.JsonSerializer.Converters.Add(implicitlyMarshaledType.Value);
}
}

private interface IMessageWithTopLevelPropertyBag
Expand Down Expand Up @@ -685,7 +673,7 @@ private JToken TokenizeUserData(Type? declaredType, object? value)
return JValue.CreateNull();
}

if (declaredType is object && this.TryGetImplicitlyMarshaledJsonConverter(declaredType, out RpcMarshalableImplicitConverter? converter))
if (declaredType is not null && this.TryGetImplicitlyMarshaledJsonConverter(declaredType, out RpcMarshalableConverter? converter))
{
using JTokenWriter jsonWriter = this.CreateJTokenWriter();
converter.WriteJson(jsonWriter, value, this.JsonSerializer);
Expand Down Expand Up @@ -715,19 +703,15 @@ private JTokenWriter CreateJTokenWriter()
};
}

private bool TryGetImplicitlyMarshaledJsonConverter(Type type, [NotNullWhen(true)] out RpcMarshalableImplicitConverter? converter)
private bool TryGetImplicitlyMarshaledJsonConverter(Type type, [NotNullWhen(true)] out RpcMarshalableConverter? converter)
{
if (this.implicitlyMarshaledTypes.TryGetValue(type, out converter))
{
return true;
}

if (type.IsConstructedGenericType && this.implicitlyMarshaledTypes.TryGetValue(type.GetGenericTypeDefinition(), out converter))
if (MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(type, out JsonRpcProxyOptions? proxyOptions, out JsonRpcTargetOptions? targetOptions))
{
converter = converter.WithClosedType(type);
converter = new RpcMarshalableConverter(type, this, proxyOptions, targetOptions);
return true;
}

converter = null;
return false;
}

Expand Down Expand Up @@ -1477,31 +1461,58 @@ public override void WriteJson(JsonWriter writer, Stream? value, JsonSerializer
}

[DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")]
private class RpcMarshalableImplicitConverter : JsonConverter
private class RpcMarshalableReadConverter : JsonConverter
{
private readonly Type implicitlyConvertedType;
private readonly JsonMessageFormatter jsonMessageFormatter;

public RpcMarshalableReadConverter(JsonMessageFormatter jsonMessageFormatter)
{
this.jsonMessageFormatter = jsonMessageFormatter;
}

public override bool CanWrite => false;

private string DebuggerDisplay => $"Read-only converter for marshalable interfaces";

public override bool CanConvert(Type objectType) =>
MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(objectType, out _, out _);

public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(objectType, out JsonRpcProxyOptions? proxyOptions, out _))
{
var token = (MessageFormatterRpcMarshaledContextTracker.MarshalToken?)JToken.Load(reader).ToObject(typeof(MessageFormatterRpcMarshaledContextTracker.MarshalToken), serializer);
return this.jsonMessageFormatter.RpcMarshaledContextTracker.GetObject(objectType, token, proxyOptions);
}

throw new InvalidOperationException($"Type {objectType.FullName} is not marshalable");
}

public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new NotSupportedException($"{nameof(RpcMarshalableReadConverter)} doesn't have write capabilities.");
}
}

[DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")]
private class RpcMarshalableConverter : JsonConverter
{
private readonly Type interfaceType;
private readonly JsonMessageFormatter jsonMessageFormatter;
private readonly JsonRpcProxyOptions proxyOptions;
private readonly JsonRpcTargetOptions targetOptions;

public RpcMarshalableImplicitConverter(Type implicitlyConvertedType, JsonMessageFormatter jsonMessageFormatter, JsonRpcProxyOptions proxyOptions, JsonRpcTargetOptions targetOptions)
public RpcMarshalableConverter(Type interfaceType, JsonMessageFormatter jsonMessageFormatter, JsonRpcProxyOptions proxyOptions, JsonRpcTargetOptions targetOptions)
{
this.implicitlyConvertedType = implicitlyConvertedType;
this.interfaceType = interfaceType;
this.jsonMessageFormatter = jsonMessageFormatter;
this.proxyOptions = proxyOptions;
this.targetOptions = targetOptions;
}

private RpcMarshalableImplicitConverter(RpcMarshalableImplicitConverter copyFrom, Type implicitlyConvertedType)
: this(implicitlyConvertedType, copyFrom.jsonMessageFormatter, copyFrom.proxyOptions, copyFrom.targetOptions)
{
}

private string DebuggerDisplay => $"Implicit converter for: {this.implicitlyConvertedType.Name}";
private string DebuggerDisplay => $"Converter for marshalable objects of type {this.interfaceType.FullName}";

public override bool CanConvert(Type objectType) =>
objectType == this.implicitlyConvertedType ||
(this.implicitlyConvertedType.IsGenericTypeDefinition && objectType.IsGenericType && objectType.GetGenericTypeDefinition() == this.implicitlyConvertedType);
public override bool CanConvert(Type objectType) => objectType == this.interfaceType;

public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
Expand All @@ -1515,26 +1526,17 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer
{
writer.WriteNull();
}
else if (this.interfaceType.IsAssignableFrom(value.GetType()) is false)
{
throw new InvalidOperationException($"Type {value.GetType().FullName} doens't implement {this.interfaceType.FullName}");
}
else
{
IRpcMarshaledContext<object> context = JsonRpc.MarshalWithControlledLifetime(this.implicitlyConvertedType, value, this.targetOptions);
IRpcMarshaledContext<object> context = JsonRpc.MarshalWithControlledLifetime(this.interfaceType, value, this.targetOptions);
MessageFormatterRpcMarshaledContextTracker.MarshalToken token = this.jsonMessageFormatter.RpcMarshaledContextTracker.GetToken(context);
serializer.Serialize(writer, token);
}
}

internal RpcMarshalableImplicitConverter WithClosedType(Type implicitlyMarshaledType)
{
if (this.implicitlyConvertedType == implicitlyMarshaledType ||
!this.implicitlyConvertedType.IsGenericType ||
this.implicitlyConvertedType.IsConstructedGenericType)
{
return this;
}

Assumes.True(implicitlyMarshaledType.GetGenericTypeDefinition().Equals(this.implicitlyConvertedType.GetGenericTypeDefinition()));
return new RpcMarshalableImplicitConverter(this, implicitlyMarshaledType);
}
}

private class JsonConverterFormatter : IFormatterConverter
Expand Down Expand Up @@ -1719,7 +1721,7 @@ public override JsonContract ResolveContract(Type type)
continue;
}

if (property.PropertyType is object && this.formatter.TryGetImplicitlyMarshaledJsonConverter(property.PropertyType, out RpcMarshalableImplicitConverter? converter))
if (property.PropertyType is not null && this.formatter.TryGetImplicitlyMarshaledJsonConverter(property.PropertyType, out RpcMarshalableConverter? converter))
{
property.Converter = converter;
}
Expand Down
47 changes: 17 additions & 30 deletions src/StreamJsonRpc/MessagePackFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ private MessagePackSerializerOptions MassageUserDataOptions(MessagePackSerialize
this.exceptionResolver,

// Support for marshalled objects.
new RpcMarshalableImplicitResolver(this, MessageFormatterRpcMarshaledContextTracker.ImplicitlyMarshaledTypes),
new RpcMarshalableImplicitResolver(this),
};

// Wrap the resolver in another class as a way to pass information to our custom formatters.
Expand Down Expand Up @@ -1389,13 +1389,11 @@ public void Serialize(ref MessagePackWriter writer, T? value, MessagePackSeriali
private class RpcMarshalableImplicitResolver : IFormatterResolver
{
private readonly MessagePackFormatter formatter;
private readonly IReadOnlyCollection<(Type Type, JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions)> implicitlyMarshaledTypes;
private readonly Dictionary<Type, object> formatters = new Dictionary<Type, object>();

internal RpcMarshalableImplicitResolver(MessagePackFormatter formatter, IReadOnlyCollection<(Type Type, JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions)> implicitlyMarshaledTypes)
internal RpcMarshalableImplicitResolver(MessagePackFormatter formatter)
{
this.formatter = formatter;
this.implicitlyMarshaledTypes = implicitlyMarshaledTypes;
}

public IMessagePackFormatter<T>? GetFormatter<T>()
Expand All @@ -1413,37 +1411,26 @@ internal RpcMarshalableImplicitResolver(MessagePackFormatter formatter, IReadOnl
}
}

(Type Type, JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions)? matchingCandidate = null;
foreach ((Type Type, JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions) candidate in this.implicitlyMarshaledTypes)
if (MessageFormatterRpcMarshaledContextTracker.TryGetMarshalOptionsForType(typeof(T), out JsonRpcProxyOptions? proxyOptions, out JsonRpcTargetOptions? targetOptions))
{
if (candidate.Type == typeof(T) ||
(candidate.Type.IsGenericTypeDefinition && typeof(T).IsConstructedGenericType && candidate.Type == typeof(T).GetGenericTypeDefinition()))
{
matchingCandidate = candidate;
break;
}
}

if (!matchingCandidate.HasValue)
{
return null;
}

object formatter = Activator.CreateInstance(
typeof(RpcMarshalableImplicitFormatter<>).MakeGenericType(typeof(T)),
this.formatter,
matchingCandidate.Value.ProxyOptions,
matchingCandidate.Value.TargetOptions)!;
object formatter = Activator.CreateInstance(
typeof(RpcMarshalableImplicitFormatter<>).MakeGenericType(typeof(T)),
this.formatter,
proxyOptions,
targetOptions)!;

lock (this.formatters)
{
if (!this.formatters.TryGetValue(typeof(T), out object? cachedFormatter))
lock (this.formatters)
{
this.formatters.Add(typeof(T), cachedFormatter = formatter);
}
if (!this.formatters.TryGetValue(typeof(T), out object? cachedFormatter))
{
this.formatters.Add(typeof(T), cachedFormatter = formatter);
}

return (IMessagePackFormatter<T>)cachedFormatter;
return (IMessagePackFormatter<T>)cachedFormatter;
}
}

return null;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace StreamJsonRpc.Reflection
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
Expand All @@ -18,7 +19,7 @@ namespace StreamJsonRpc.Reflection
/// </summary>
internal class MessageFormatterRpcMarshaledContextTracker
{
internal static readonly IReadOnlyCollection<(Type ImplicitlyMarshaledType, JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions)> ImplicitlyMarshaledTypes = new (Type ImplicitlyMarshaledType, JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions)[]
private static readonly IReadOnlyCollection<(Type ImplicitlyMarshaledType, JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions)> ImplicitlyMarshaledTypes = new (Type ImplicitlyMarshaledType, JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions)[]
{
(typeof(IDisposable), new JsonRpcProxyOptions { MethodNameTransform = CommonMethodNameTransforms.CamelCase }, new JsonRpcTargetOptions { MethodNameTransform = CommonMethodNameTransforms.CamelCase }),

Expand Down Expand Up @@ -48,6 +49,8 @@ internal class MessageFormatterRpcMarshaledContextTracker
new JsonRpcTargetOptions { MethodNameTransform = CommonMethodNameTransforms.CamelCase }),
};

private static readonly ConcurrentDictionary<Type, (JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions)> MarshaledTypes = new ConcurrentDictionary<Type, (JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions)>();
private static readonly (JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions) RpcMarshalableInterfaceDefaultOptions = (new JsonRpcProxyOptions(), new JsonRpcTargetOptions { NotifyClientOfEvents = false, DisposeOnDisconnect = true });
private static readonly MethodInfo ReleaseMarshaledObjectMethodInfo = typeof(MessageFormatterRpcMarshaledContextTracker).GetMethod(nameof(ReleaseMarshaledObject), BindingFlags.NonPublic | BindingFlags.Instance)!;

private readonly Dictionary<long, (IRpcMarshaledContext<object> Context, IDisposable Revert)> marshaledObjects = new Dictionary<long, (IRpcMarshaledContext<object> Context, IDisposable Revert)>();
Expand Down Expand Up @@ -87,6 +90,62 @@ private enum MarshalMode
MarshallingRealObject = 1,
}

internal static bool TryGetMarshalOptionsForType(Type type, [NotNullWhen(true)] out JsonRpcProxyOptions? proxyOptions, [NotNullWhen(true)] out JsonRpcTargetOptions? targetOptions)
{
proxyOptions = null;
targetOptions = null;
if (type.IsInterface is false)
{
return false;
}

if (MarshaledTypes.TryGetValue(type, out (JsonRpcProxyOptions ProxyOptions, JsonRpcTargetOptions TargetOptions) options))
{
proxyOptions = options.ProxyOptions;
targetOptions = options.TargetOptions;
return true;
}

foreach ((Type implicitlyMarshaledType, JsonRpcProxyOptions typeProxyOptions, JsonRpcTargetOptions typeTargetOptions) in ImplicitlyMarshaledTypes)
{
if (implicitlyMarshaledType == type ||
(implicitlyMarshaledType.IsGenericTypeDefinition &&
type.IsConstructedGenericType &&
implicitlyMarshaledType == type.GetGenericTypeDefinition()))
{
proxyOptions = typeProxyOptions;
targetOptions = typeTargetOptions;
MarshaledTypes.TryAdd(type, (proxyOptions, targetOptions));
return true;
}
}

if (type.GetCustomAttribute<RpcMarshalableAttribute>() is not null)
{
if (typeof(IDisposable).IsAssignableFrom(type) is false)
{
throw new NotSupportedException(Resources.MarshalableInterfaceNotDisposable);
}

if (type.GetEvents().Length > 0)
{
throw new NotSupportedException(Resources.MarshalableInterfaceHasEvents);
}

if (type.GetProperties().Length > 0)
{
throw new NotSupportedException(Resources.MarshalableInterfaceHasProperties);
}

proxyOptions = RpcMarshalableInterfaceDefaultOptions.ProxyOptions;
targetOptions = RpcMarshalableInterfaceDefaultOptions.TargetOptions;
MarshaledTypes.TryAdd(type, (proxyOptions, targetOptions));
return true;
}

return false;
}

/// <summary>
/// Prepares a local object to be marshaled over the wire.
/// </summary>
Expand Down
14 changes: 14 additions & 0 deletions src/StreamJsonRpc/Reflection/RpcMarshalableAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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;

using System;

/// <summary>
/// Marks an interface as marshalable.
/// </summary>
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public class RpcMarshalableAttribute : Attribute
{
}
Loading

0 comments on commit 3570d33

Please sign in to comment.