Skip to content
Open
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
16 changes: 16 additions & 0 deletions protobuf-net.Grpc.sln
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TraderSys.Portfolios.Shared
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "docs", "docs\docs.csproj", "{9479D65D-A0E8-4520-99EC-93774A217EF6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
ProjectSection(SolutionItems) = preProject
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
Directory.Packages.props = Directory.Packages.props
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "protobuf-net.Grpc.Tests.TestProxy", "src\protobuf-net.Grpc.Tests.TestProxy\protobuf-net.Grpc.Tests.TestProxy.csproj", "{69A6D227-F8C8-AED8-8C94-31F9A3A005C4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -402,6 +411,12 @@ Global
{9479D65D-A0E8-4520-99EC-93774A217EF6}.Release|Any CPU.Build.0 = Release|Any CPU
{9479D65D-A0E8-4520-99EC-93774A217EF6}.VS|Any CPU.ActiveCfg = Debug|Any CPU
{9479D65D-A0E8-4520-99EC-93774A217EF6}.VS|Any CPU.Build.0 = Debug|Any CPU
{69A6D227-F8C8-AED8-8C94-31F9A3A005C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{69A6D227-F8C8-AED8-8C94-31F9A3A005C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{69A6D227-F8C8-AED8-8C94-31F9A3A005C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{69A6D227-F8C8-AED8-8C94-31F9A3A005C4}.Release|Any CPU.Build.0 = Release|Any CPU
{69A6D227-F8C8-AED8-8C94-31F9A3A005C4}.VS|Any CPU.ActiveCfg = Debug|Any CPU
{69A6D227-F8C8-AED8-8C94-31F9A3A005C4}.VS|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -463,6 +478,7 @@ Global
{AC65761B-5CF5-4F3E-AF60-41F977E8070D} = {2B3F5AED-24B3-4915-900E-EFC58414EA29}
{80479131-FE55-473E-AE68-55601AD96668} = {2B3F5AED-24B3-4915-900E-EFC58414EA29}
{9479D65D-A0E8-4520-99EC-93774A217EF6} = {8D5FDB6F-6111-4604-8EDF-6A59F8B12D7E}
{69A6D227-F8C8-AED8-8C94-31F9A3A005C4} = {0A84599D-2CE9-416E-888F-24652EEAB0B3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BA14B07C-CA29-430D-A600-F37A050636D3}
Expand Down
18 changes: 18 additions & 0 deletions src/protobuf-net.Grpc.Tests.TestProxy/IProxy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using ProtoBuf;
using ProtoBuf.Grpc;
using ProtoBuf.Grpc.Configuration;

namespace protobuf_net.Grpc.Tests.TestProxy;

[Service]
public interface IProxy
{
[Operation]
ValueTask<Out> Operation(In request, CallContext callContext = default);
}

[ProtoContract]
public class In { }

[ProtoContract]
public class Out { }
22 changes: 22 additions & 0 deletions src/protobuf-net.Grpc.Tests.TestProxy/ProxyFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Reflection;
using System.Runtime.Loader;
using ProtoBuf.Grpc.ClientFactory;
using Grpc.Net.ClientFactory;
using Microsoft.Extensions.DependencyInjection;

namespace protobuf_net.Grpc.Tests.TestProxy;
public static class ProxyFactory
{
public static IProxy Create()
{
var services = new ServiceCollection();
services.AddGrpcClient<IProxy>(o =>
{
o.Address = new Uri("http://localhost");
}).ConfigureCodeFirstGrpcClient<IProxy>();
var serviceProvider = services.BuildServiceProvider();

var clientFactory = serviceProvider.GetRequiredService<GrpcClientFactory>();
return clientFactory.CreateClient<IProxy>(nameof(IProxy));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<RootNamespace>protobuf_net.Grpc.Tests.TestProxy</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\protobuf-net.Grpc.ClientFactory\protobuf-net.Grpc.ClientFactory.csproj">
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
<ProjectReference Include="..\protobuf-net.Grpc\protobuf-net.Grpc.csproj">
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
</Project>
31 changes: 20 additions & 11 deletions src/protobuf-net.Grpc/Internal/ProxyEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,14 @@
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
#if NET6_0_OR_GREATER
using System.Runtime.Loader;
#endif

namespace ProtoBuf.Grpc.Internal
{
internal static class ProxyEmitter
{
private static readonly string ProxyIdentity = typeof(ProxyEmitter).Namespace + ".Proxies";

private static readonly ModuleBuilder s_module = AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName(ProxyIdentity), AssemblyBuilderAccess.RunAndCollect).DefineDynamicModule(ProxyIdentity);

private static void Ldc_I4(ILGenerator il, int value)
{
switch (value)
Expand Down Expand Up @@ -142,14 +140,25 @@ internal static Func<CallInvoker, TService> EmitFactory<TService>(BinderConfigur
{
Type baseType = GrpcClientFactory.ClientBaseType;

#if NET6_0_OR_GREATER
var proxyLoadContext = AssemblyLoadContext.GetLoadContext(typeof(TService).Assembly) ??
AssemblyLoadContext.Default;
// Once we have the ALC for reflection, get or create the module for it.
// Any references will be resolved against the ALC that owns the service interface.
ModuleBuilder moduleBuilder = ProxyModuleHelper.GetOrCreateProxyModule(proxyLoadContext);
#else
ModuleBuilder moduleBuilder = ProxyModuleHelper.MainProxyModule;
#endif
var typeIdentity = ProxyModuleHelper.ProxyModuleIdentity + "." + baseType.Name + "." + typeof(TService).Name + "_Proxy_" + _typeIndex++;

var callInvoker = baseType.GetProperty("CallInvoker", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)?.GetGetMethod(true);
if (callInvoker == null || callInvoker.ReturnType != typeof(CallInvoker) || callInvoker.GetParameters().Length != 0)
throw new ArgumentException($"The base-type {baseType} for service-proxy {typeof(TService)} lacks a suitable CallInvoker API");

lock (s_module)
lock (moduleBuilder)
{
// private sealed class IFooProxy...
var type = s_module.DefineType(ProxyIdentity + "." + baseType.Name + "." + typeof(TService).Name + "_Proxy_" + _typeIndex++,
var type = moduleBuilder.DefineType(typeIdentity,
TypeAttributes.Class | TypeAttributes.Sealed | TypeAttributes.NotPublic | TypeAttributes.BeforeFieldInit);

type.SetParent(baseType);
Expand Down Expand Up @@ -209,7 +218,7 @@ FieldBuilder Marshaller(Type forType)
type.DefineMethodOverride(impl, iMethod);

var il = impl.GetILGenerator();

// check whether the method belongs to [ServiceInherited] interface
var isMethodInherited =
iMethod.DeclaringType?.IsDefined(typeof(SubServiceAttribute)) ?? false;
Expand Down Expand Up @@ -238,7 +247,7 @@ FieldBuilder Marshaller(Type forType)
}
continue;
}

// in case method belongs to an sub-service interface, we have to find the service contract name inheriting it
if (isMethodInherited)
{
Expand Down Expand Up @@ -340,7 +349,7 @@ FieldBuilder Marshaller(Type forType)
var finalType = type.CreateType()!;
#endif
// assign the marshallers and invoke the init
foreach((var field, var name, var instance) in marshallers.Values)
foreach ((var field, var name, var instance) in marshallers.Values)
{
finalType.GetField(name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public)!.SetValue(null, instance);
}
Expand Down Expand Up @@ -371,7 +380,7 @@ internal static readonly FieldInfo
s_CallContext_Default = typeof(CallContext).GetField(nameof(CallContext.Default))!,
#pragma warning disable CS0618 // Empty
s_Empty_Instance = typeof(Empty).GetField(nameof(Empty.Instance))!,
s_Empty_InstaneTask= typeof(Empty).GetField(nameof(Empty.InstanceTask))!;
s_Empty_InstaneTask = typeof(Empty).GetField(nameof(Empty.InstanceTask))!;
#pragma warning restore CS0618

internal static readonly MethodInfo s_CallContext_FromCancellationToken = typeof(CallContext).GetMethod("op_Implicit", BindingFlags.Public | BindingFlags.Static, null, [typeof(CancellationToken)], null)!;
Expand Down
50 changes: 50 additions & 0 deletions src/protobuf-net.Grpc/Internal/ProxyModuleHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Reflection;
using System.Reflection.Emit;
#if NET6_0_OR_GREATER
using System.Collections.Concurrent;
using System.Runtime.Loader;
using System.Threading;
#endif

namespace ProtoBuf.Grpc.Internal;
internal static class ProxyModuleHelper
{
static ProxyModuleHelper() { }
public static readonly string ProxyModuleIdentity = typeof(ProxyEmitter).Namespace + ".Proxies";

#if NET6_0_OR_GREATER
private static int s_moduleCounter = 0;
private static string GetNextModuleIdentity()
{
return ProxyModuleIdentity + "-" + Interlocked.Increment(ref s_moduleCounter);
}

private static readonly ConcurrentDictionary<AssemblyLoadContext, ModuleBuilder> _proxyModules = new();

public static ModuleBuilder GetOrCreateProxyModule(AssemblyLoadContext assemblyLoadContext)
{
return _proxyModules.GetOrAdd(assemblyLoadContext, key =>
{
using var _ = key.EnterContextualReflection();
var alc = CreateProxyModule(GetNextModuleIdentity());
key.Unloading += _ => RemoveAssemblyLoadContext(key);
return alc;
});
}
private static bool RemoveAssemblyLoadContext(AssemblyLoadContext alc)
{
return _proxyModules.TryRemove(alc, out _);
}
#else

public static readonly ModuleBuilder MainProxyModule = CreateProxyModule(ProxyModuleIdentity);

#endif

private static ModuleBuilder CreateProxyModule(string moduleIdentity)
{
var name = new AssemblyName(moduleIdentity);
var assembly = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndCollect);
return assembly.DefineDynamicModule(moduleIdentity);
}
}
117 changes: 117 additions & 0 deletions tests/protobuf-net.Grpc.Test/AssemblyLoadContextsClientFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#if CLIENT_FACTORY
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Xunit;

namespace protobuf_net.Grpc.Test;
public class AssemblyLoadContextsClientFactoryTests
{
private const string TestProxyAssemblyFileName = "protobuf-net.Grpc.Tests.TestProxy.dll";
private static readonly string CurrentAssemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
private static readonly string ProxyPath = Path.Combine(CurrentAssemblyFolder, TestProxyAssemblyFileName);

[Fact]
public void CanCreateProxiesWithPureIsolation()
{
AssemblyLoadContext plugin1 = CreatePluginAssemblyLoadContext("alc1", CurrentAssemblyFolder);
AssemblyLoadContext plugin2 = CreatePluginAssemblyLoadContext("alc2", CurrentAssemblyFolder);

AssertPlugins(plugin1, plugin2);
}

[Fact]
public void CanCreateProxiesWithSharedALC()
{
AssemblyLoadContext shared = CreatePluginAssemblyLoadContext("Shared", CurrentAssemblyFolder);
AssemblyLoadContext plugin1 = CreatePluginAssemblyLoadContext("alc1", CurrentAssemblyFolder, shared);
AssemblyLoadContext plugin2 = CreatePluginAssemblyLoadContext("alc2", CurrentAssemblyFolder, shared);

AssertPlugins(plugin1, plugin2);
}

[Fact]
public void CanCreateProxiesWithSharedPlugin()
{
AssemblyLoadContext plugin1 = CreatePluginAssemblyLoadContext("alc1", CurrentAssemblyFolder);
AssemblyLoadContext plugin2 = CreatePluginAssemblyLoadContext("alc2", CurrentAssemblyFolder, plugin1);

AssertPlugins(plugin1, plugin2);
}

private static void AssertPlugins(AssemblyLoadContext plugin1, AssemblyLoadContext plugin2)
{
var proxyAssembly1 = plugin1.LoadFromAssemblyPath(ProxyPath);
var proxyAssembly2 = plugin2.LoadFromAssemblyPath(ProxyPath);

object? proxy1 = CreateAndAssertProxy(proxyAssembly1, plugin1);
object? proxy2 = CreateAndAssertProxy(proxyAssembly2, plugin2);

Assert.NotSame(proxy1!.GetType(), proxy2!.GetType());

object? anotherProxy1 = CreateAndAssertProxy(proxyAssembly1, plugin1);
Assert.NotSame(proxy1, anotherProxy1);
Assert.Equal(proxy1.GetType(), anotherProxy1!.GetType());


object? anotherProxy2 = CreateAndAssertProxy(proxyAssembly2, plugin2);
Assert.NotSame(proxy2, anotherProxy2);
Assert.Equal(proxy2.GetType(), anotherProxy2!.GetType());
}

private static object? CreateAndAssertProxy(Assembly proxyAssembly, AssemblyLoadContext expectedAssemblyLoadContext)
{
Assert.NotNull(proxyAssembly);
object? proxy = CreateProxy(proxyAssembly);
Assert.NotNull(proxy);
Type pluginType = proxy!.GetType();
Assert.Equal("IProxy", pluginType.GetInterfaces().First().Name);
Assert.Equal(expectedAssemblyLoadContext, AssemblyLoadContext.GetLoadContext(pluginType.Assembly));
return proxy;
}

private static AssemblyLoadContext CreatePluginAssemblyLoadContext(string name, string folder, AssemblyLoadContext? shared = null)
{
return new PluginLoadContext(name, folder, shared);
}

private static object? CreateProxy(Assembly assembly1)
{
var proxycreator = assembly1.DefinedTypes.FirstOrDefault(t => t.Name == "ProxyFactory");
Assert.NotNull(proxycreator);
var method = proxycreator!.GetMethod("Create", BindingFlags.Public | BindingFlags.Static);
Assert.NotNull(method);
return method.Invoke(null, new object[] { });
}

private class PluginLoadContext : AssemblyLoadContext
{
private readonly AssemblyLoadContext? _sharedContext;
private readonly string _folder;

public PluginLoadContext(string name, string folder, AssemblyLoadContext? sharedContext = null)
: base(name, isCollectible: true)
{
_sharedContext = sharedContext;
_folder = folder;
}

protected override Assembly? Load(AssemblyName assemblyName)
{
if (_sharedContext is not null)
{
return _sharedContext.LoadFromAssemblyName(assemblyName);
}

string path = Path.Combine(_folder, assemblyName.Name + ".dll");
if (File.Exists(path))
{
return LoadFromAssemblyPath(path);
}
return null;
}
}
}
#endif
21 changes: 11 additions & 10 deletions tests/protobuf-net.Grpc.Test/ClientFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.ServiceModel;
using System.Threading.Tasks;
using Xunit;

namespace protobuf_net.Grpc.Test
{
public class ClientFactoryTests
Expand All @@ -21,7 +22,7 @@ public void CanConfigureCodeFirstClient()
o.Address = new Uri("http://localhost");
});
var serviceProvider = services.BuildServiceProvider();

var clientFactory = serviceProvider.GetRequiredService<GrpcClientFactory>();
var client = clientFactory.CreateClient<IMyService>(nameof(IMyService));

Expand All @@ -47,16 +48,16 @@ public void CanConfigureHttpClientBuilder()
var name = client.GetType().FullName;
Assert.StartsWith("ProtoBuf.Grpc.Internal.Proxies.ClientBase.", name);
}
}

[ServiceContract]
public interface IMyService
{
[OperationContract]
public ValueTask<Dummy> UnaryCall(Dummy value);
}
[ServiceContract]
public interface IMyService
{
[OperationContract]
public ValueTask<Dummy> UnaryCall(Dummy value);
}

[DataContract]
public class Dummy { }
[DataContract]
public class Dummy { }
}
}
#endif
Loading