diff --git a/StreamJsonRpc.sln b/StreamJsonRpc.sln index 8b8d49b2d..03a80d4bc 100644 --- a/StreamJsonRpc.sln +++ b/StreamJsonRpc.sln @@ -38,6 +38,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F446B894-5 test\Directory.Build.targets = test\Directory.Build.targets EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeAOTCompatibility.Test", "test\NativeAOTCompatibility.Test\NativeAOTCompatibility.Test.csproj", "{1837EED6-4236-09AA-AEAA-0B8F5C35813E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -60,6 +62,10 @@ Global {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 + {1837EED6-4236-09AA-AEAA-0B8F5C35813E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1837EED6-4236-09AA-AEAA-0B8F5C35813E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1837EED6-4236-09AA-AEAA-0B8F5C35813E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1837EED6-4236-09AA-AEAA-0B8F5C35813E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -69,6 +75,7 @@ Global {8BF355B2-E3B0-4615-BFC1-7563EADC4F8B} = {F446B894-56AA-4653-ADC0-5FFC911C9C13} {CEF0F77F-19EB-4C76-A050-854984BB0364} = {F446B894-56AA-4653-ADC0-5FFC911C9C13} {5936CF62-A59D-4E5C-9EE4-5E8BAFA9F215} = {F446B894-56AA-4653-ADC0-5FFC911C9C13} + {1837EED6-4236-09AA-AEAA-0B8F5C35813E} = {F446B894-56AA-4653-ADC0-5FFC911C9C13} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4946F7E7-0619-414B-BE56-DDF0261CA8A9} diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml index 9f4ecb3c6..9d6993b30 100644 --- a/azure-pipelines/build.yml +++ b/azure-pipelines/build.yml @@ -199,6 +199,7 @@ jobs: parameters: Is1ESPT: ${{ parameters.Is1ESPT }} RunTests: ${{ parameters.RunTests }} + osRID: win IsOptProf: ${{ parameters.IsOptProf }} - ${{ if and(parameters.EnableDotNetFormatCheck, not(parameters.EnableLinuxBuild)) }}: @@ -243,6 +244,7 @@ jobs: parameters: Is1ESPT: ${{ parameters.Is1ESPT }} RunTests: ${{ parameters.RunTests }} + osRID: linux - ${{ if parameters.EnableDotNetFormatCheck }}: - script: dotnet format --verify-no-changes displayName: 💅 Verify formatted code @@ -277,6 +279,7 @@ jobs: parameters: Is1ESPT: ${{ parameters.Is1ESPT }} RunTests: ${{ parameters.RunTests }} + osRID: osx - job: WrapUp dependsOn: diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml index 77757190a..7e9e25a83 100644 --- a/azure-pipelines/dotnet.yml +++ b/azure-pipelines/dotnet.yml @@ -1,5 +1,7 @@ parameters: - name: RunTests +- name: osRID + type: string - name: IsOptProf type: boolean default: false @@ -40,6 +42,10 @@ steps: - script: dotnet pack src\VSInsertionMetadata -c $(BuildConfiguration) -warnaserror /bl:"$(Build.ArtifactStagingDirectory)/build_logs/VSInsertion-Pack.binlog" displayName: 🔧 dotnet pack VSInsertionMetadata +- script: dotnet publish -r ${{ parameters.osRID }}-x64 -warnaserror + displayName: 🧪 NativeAOT test + workingDirectory: test/NativeAOTCompatibility.Test + - powershell: tools/variables/_define.ps1 failOnStderr: true displayName: ⚙ Update pipeline variables based on build outputs diff --git a/src/StreamJsonRpc/HeaderDelimitedMessageHandler.cs b/src/StreamJsonRpc/HeaderDelimitedMessageHandler.cs index 93319e4a8..3658681be 100644 --- a/src/StreamJsonRpc/HeaderDelimitedMessageHandler.cs +++ b/src/StreamJsonRpc/HeaderDelimitedMessageHandler.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Buffers.Text; +using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; using System.Net.Http.Headers; using System.Runtime.CompilerServices; @@ -97,6 +98,7 @@ public HeaderDelimitedMessageHandler(Stream duplexStream, IJsonRpcMessageFormatt /// Initializes a new instance of the class. /// /// The stream to use for transmitting and receiving messages. + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] public HeaderDelimitedMessageHandler(Stream duplexStream) : this(duplexStream, duplexStream) { @@ -107,6 +109,7 @@ public HeaderDelimitedMessageHandler(Stream duplexStream) /// /// The stream to use for transmitting messages. /// The stream to use for receiving messages. + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] public HeaderDelimitedMessageHandler(Stream? sendingStream, Stream? receivingStream) : this(sendingStream, receivingStream, new JsonMessageFormatter()) { diff --git a/src/StreamJsonRpc/JsonMessageFormatter.cs b/src/StreamJsonRpc/JsonMessageFormatter.cs index 112913b0a..8f21c5d02 100644 --- a/src/StreamJsonRpc/JsonMessageFormatter.cs +++ b/src/StreamJsonRpc/JsonMessageFormatter.cs @@ -26,6 +26,7 @@ namespace StreamJsonRpc; /// /// Each instance of this class may only be used with a single instance. /// +[RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] public class JsonMessageFormatter : FormatterBase, IJsonRpcAsyncMessageTextFormatter, IJsonRpcMessageFactory { /// @@ -1009,6 +1010,7 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer /// /// Converts a progress token to an . /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] private class JsonProgressServerConverter : JsonConverter { private readonly JsonMessageFormatter formatter; @@ -1042,6 +1044,7 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer /// /// Converts an enumeration token to an . /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] private class AsyncEnumerableConsumerConverter : JsonConverter { private static readonly MethodInfo ReadJsonOpenGenericMethod = typeof(AsyncEnumerableConsumerConverter).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).Single(m => m.Name == nameof(ReadJson) && m.IsGenericMethod); @@ -1062,7 +1065,7 @@ internal AsyncEnumerableConsumerConverter(JsonMessageFormatter jsonMessageFormat return null; } - Type? iface = TrackerHelpers>.FindInterfaceImplementedBy(objectType); + Type? iface = TrackerHelpers.FindIAsyncEnumerableInterfaceImplementedBy(objectType); Assumes.NotNull(iface); MethodInfo genericMethod = ReadJsonOpenGenericMethod.MakeGenericMethod(iface.GenericTypeArguments[0]); try @@ -1100,6 +1103,7 @@ private IAsyncEnumerable ReadJson(JsonReader reader, JsonSerializer serial /// /// Converts an instance of to an enumeration token. /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] private class AsyncEnumerableGeneratorConverter : JsonConverter { private static readonly MethodInfo WriteJsonOpenGenericMethod = typeof(AsyncEnumerableGeneratorConverter).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Single(m => m.Name == nameof(WriteJson) && m.IsGenericMethod); @@ -1120,7 +1124,7 @@ internal AsyncEnumerableGeneratorConverter(JsonMessageFormatter jsonMessageForma public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { - Type? iface = TrackerHelpers>.FindInterfaceImplementedBy(value!.GetType()); + Type? iface = TrackerHelpers.FindIAsyncEnumerableInterfaceImplementedBy(value!.GetType()); Assumes.NotNull(iface); MethodInfo genericMethod = WriteJsonOpenGenericMethod.MakeGenericMethod(iface.GenericTypeArguments[0]); try @@ -1249,7 +1253,9 @@ public override void WriteJson(JsonWriter writer, Stream? value, JsonSerializer } [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] - private class RpcMarshalableConverter(Type interfaceType, JsonMessageFormatter jsonMessageFormatter, JsonRpcProxyOptions proxyOptions, JsonRpcTargetOptions targetOptions, RpcMarshalableAttribute rpcMarshalableAttribute) : JsonConverter + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + [RequiresUnreferencedCode(RuntimeReasons.RefEmit)] + private class RpcMarshalableConverter([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents | DynamicallyAccessedMemberTypes.Interfaces)] Type interfaceType, JsonMessageFormatter jsonMessageFormatter, JsonRpcProxyOptions proxyOptions, JsonRpcTargetOptions targetOptions, RpcMarshalableAttribute rpcMarshalableAttribute) : JsonConverter { private string DebuggerDisplay => $"Converter for marshalable objects of type {interfaceType.FullName}"; @@ -1333,6 +1339,7 @@ public object Convert(object value, TypeCode typeCode) public ulong ToUInt64(object value) => ((JToken)value).ToObject(this.serializer); } + [RequiresUnreferencedCode(RuntimeReasons.LoadType)] private class ExceptionConverter : JsonConverter { /// diff --git a/src/StreamJsonRpc/JsonRpc.cs b/src/StreamJsonRpc/JsonRpc.cs index f7bab5221..3047c7bec 100644 --- a/src/StreamJsonRpc/JsonRpc.cs +++ b/src/StreamJsonRpc/JsonRpc.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; +using System.Runtime.CompilerServices; using Microsoft.VisualStudio.Threading; using Newtonsoft.Json; using StreamJsonRpc.Protocol; @@ -37,6 +38,12 @@ public class JsonRpc : IDisposableObservable, IJsonRpcFormatterCallbacks, IJsonR /// private static readonly JsonRpcError DroppedError = new(); +#if NET + private static readonly MethodInfo ValueTaskAsTaskMethodInfo = typeof(ValueTask<>).GetMethod(nameof(ValueTask.AsTask))!; + private static readonly MethodInfo ValueTaskGetResultMethodInfo = typeof(ValueTask<>).GetMethod("get_Result")!; + private static readonly MethodInfo TaskGetResultMethodInfo = typeof(Task<>).GetMethod("get_Result")!; +#endif + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly object syncObject = new object(); @@ -171,6 +178,7 @@ public class JsonRpc : IDisposableObservable, IJsonRpcFormatterCallbacks, IJsonR /// /// It is important to call to begin receiving messages. /// + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] public JsonRpc(Stream stream) : this(new HeaderDelimitedMessageHandler(Requires.NotNull(stream, nameof(stream)), stream, new JsonMessageFormatter())) { @@ -187,6 +195,7 @@ public JsonRpc(Stream stream) /// /// It is important to call to begin receiving messages. /// + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] public JsonRpc(Stream? sendingStream, Stream? receivingStream, object? target = null) : this(new HeaderDelimitedMessageHandler(sendingStream, receivingStream, new JsonMessageFormatter())) { @@ -204,6 +213,8 @@ public JsonRpc(Stream? sendingStream, Stream? receivingStream, object? target = /// /// It is important to call to begin receiving messages. /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + [RequiresUnreferencedCode(RuntimeReasons.UntypedRpcTarget)] public JsonRpc(IJsonRpcMessageHandler messageHandler, object? target) : this(messageHandler) { @@ -662,6 +673,7 @@ public IActivityTracingStrategy? ActivityTracingStrategy /// An optional target object to invoke when incoming RPC requests arrive. /// The initialized and listening object. #pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] 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 { @@ -679,6 +691,7 @@ public static JsonRpc Attach(Stream stream, object? target = null) /// The stream used to receive messages. May be null. /// An optional target object to invoke when incoming RPC requests arrive. /// The initialized and listening object. + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] public static JsonRpc Attach(Stream? sendingStream, Stream? receivingStream, object? target = null) { if (sendingStream is null && receivingStream is null) @@ -713,6 +726,7 @@ public static JsonRpc Attach(Stream? sendingStream, Stream? receivingStream, obj /// In addition to implementing , it also implements /// and should be disposed of to close the connection. /// + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] public static T Attach(Stream stream) where T : class { @@ -730,6 +744,7 @@ public static T Attach(Stream stream) /// In addition to implementing , it also implements /// and should be disposed of to close the connection. /// + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] public static T Attach(Stream? sendingStream, Stream? receivingStream) where T : class { @@ -749,6 +764,7 @@ public static T Attach(Stream? sendingStream, Stream? receivingStream) /// In addition to implementing , it also implements /// and should be disposed of to close the connection. /// + [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] public static T Attach(IJsonRpcMessageHandler handler) where T : class { @@ -766,6 +782,7 @@ public static T Attach(IJsonRpcMessageHandler handler) /// In addition to implementing , it also implements /// and should be disposed of to close the connection. /// + [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] public static T Attach(IJsonRpcMessageHandler handler, JsonRpcProxyOptions? options) where T : class { @@ -780,6 +797,7 @@ public static T Attach(IJsonRpcMessageHandler handler, JsonRpcProxyOptions? o /// /// The interface that describes the functions available on the remote end. /// An instance of the generated proxy. + [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] public T Attach() where T : class { @@ -792,6 +810,7 @@ public T Attach() /// The interface that describes the functions available on the remote end. /// A set of customizations for how the client proxy is wired up. If , default options will be used. /// An instance of the generated proxy. + [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] public T Attach(JsonRpcProxyOptions? options) where T : class { @@ -803,6 +822,7 @@ public T Attach(JsonRpcProxyOptions? options) /// /// The interface that describes the functions available on the remote end. /// An instance of the generated proxy. + [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] public object Attach(Type interfaceType) => this.Attach(interfaceType, options: null); /// @@ -811,6 +831,7 @@ public T Attach(JsonRpcProxyOptions? options) /// The interface that describes the functions available on the remote end. /// A set of customizations for how the client proxy is wired up. If , default options will be used. /// An instance of the generated proxy. + [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] public object Attach(Type interfaceType, JsonRpcProxyOptions? options) { Requires.NotNull(interfaceType, nameof(interfaceType)); @@ -823,6 +844,7 @@ public object Attach(Type interfaceType, JsonRpcProxyOptions? options) /// The interfaces that describes the functions available on the remote end. /// A set of customizations for how the client proxy is wired up. If , default options will be used. /// An instance of the generated proxy. + [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] public object Attach(ReadOnlySpan interfaceTypes, JsonRpcProxyOptions? options) { Requires.Argument(interfaceTypes.Length > 0, nameof(interfaceTypes), Resources.RequiredArgumentMissing); @@ -830,19 +852,28 @@ public object Attach(ReadOnlySpan interfaceTypes, JsonRpcProxyOptions? opt } /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + [RequiresUnreferencedCode(RuntimeReasons.UntypedRpcTarget)] public void AddLocalRpcTarget(object target) => this.AddLocalRpcTarget(target, null); /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + [RequiresUnreferencedCode(RuntimeReasons.UntypedRpcTarget)] public void AddLocalRpcTarget(object target, JsonRpcTargetOptions? options) => this.AddLocalRpcTarget(Requires.NotNull(target, nameof(target)).GetType(), target, options); /// /// - public void AddLocalRpcTarget(T target, JsonRpcTargetOptions? options) + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + public void AddLocalRpcTarget<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(T target, JsonRpcTargetOptions? options) where T : notnull => this.AddLocalRpcTarget(typeof(T), target, options); /// /// Thrown if called after is called and is . - public void AddLocalRpcTarget(Type exposingMembersOn, object target, JsonRpcTargetOptions? options) + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + public void AddLocalRpcTarget( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type exposingMembersOn, + object target, + JsonRpcTargetOptions? options) { Requires.NotNull(exposingMembersOn, nameof(exposingMembersOn)); Requires.NotNull(target, nameof(target)); @@ -1200,6 +1231,8 @@ internal static IRpcMarshaledContext MarshalWithControlledLifetimeOpen(T m /// /// /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + [UnconditionalSuppressMessage("Trimming", "IL2060", Justification = "The generic method we construct has no dynamic member access requirements.")] internal static IRpcMarshaledContext MarshalWithControlledLifetime(Type interfaceType, object marshaledObject, JsonRpcTargetOptions options) { return (IRpcMarshaledContext)MarshalWithControlledLifetimeOpenGenericMethodInfo.MakeGenericMethod(interfaceType).Invoke(null, new object?[] { marshaledObject, options })!; @@ -1222,6 +1255,7 @@ internal static T MarshalLimitedArgument(T marshaledObject) } /// + [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] internal object Attach(Type contractInterface, (Type Type, int Code)[]? implementedOptionalInterfaces, JsonRpcProxyOptions? options, long? marshaledObjectHandle) { return this.CreateProxy(contractInterface.GetTypeInfo(), default, implementedOptionalInterfaces, options, marshaledObjectHandle); @@ -1236,7 +1270,12 @@ internal void AddLocalRpcMethod(MethodInfo handler, object? target, JsonRpcMetho } /// - internal RpcTargetInfo.RevertAddLocalRpcTarget? AddLocalRpcTargetInternal(Type exposingMembersOn, object target, JsonRpcTargetOptions? options, bool requestRevertOption) + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + internal RpcTargetInfo.RevertAddLocalRpcTarget? AddLocalRpcTargetInternal( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type exposingMembersOn, + object target, + JsonRpcTargetOptions? options, + bool requestRevertOption) { return this.rpcTargetInfo.AddLocalRpcTarget(exposingMembersOn, target, options, requestRevertOption); } @@ -1250,7 +1289,7 @@ internal void AddLocalRpcMethod(MethodInfo handler, object? target, JsonRpcMetho /// /// An optional object that may be disposed of to revert the addition of the target object. /// - internal void AddRpcInterfaceToTargetInternal(Type exposingMembersOn, object target, JsonRpcTargetOptions? options, RpcTargetInfo.RevertAddLocalRpcTarget? revertAddLocalRpcTarget) + internal void AddRpcInterfaceToTargetInternal([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type exposingMembersOn, object target, JsonRpcTargetOptions? options, RpcTargetInfo.RevertAddLocalRpcTarget? revertAddLocalRpcTarget) { this.rpcTargetInfo.AddRpcInterfaceToTarget(exposingMembersOn, target, options, revertAddLocalRpcTarget); } @@ -1271,6 +1310,7 @@ internal void AddRpcInterfaceToTargetInternal(Type exposingMembersOn, object tar /// /// Implementations should avoid throwing , or other exceptions, preferring to return instead. /// + [RequiresUnreferencedCode(RuntimeReasons.LoadType)] protected internal virtual Type? LoadType(string typeFullName, string? assemblyName) { Requires.NotNull(typeFullName, nameof(typeFullName)); @@ -1691,7 +1731,7 @@ protected virtual async ValueTask DispatchRequestAsync(JsonRpcRe // Checking on the runtime result object itself is problematic because .NET / C# implements // async Task methods to return a Task instance, and we shouldn't consider // the VoidTaskResult internal struct as a meaningful result. - return TryGetTaskOfTOrValueTaskOfTType(targetMethod.ReturnType!.GetTypeInfo(), out _) + return TryGetTaskOfTOrValueTaskOfTType(targetMethod.ReturnType!.GetTypeInfo(), out _, out _) ? await this.HandleInvocationTaskOfTResultAsync(request, resultingTask, cancellationToken).ConfigureAwait(false) : this.HandleInvocationTaskResult(request, resultingTask); } @@ -1772,8 +1812,9 @@ private static Exception StripExceptionToInnerException(Exception exception) /// /// The original type of the value returned from an RPC-invoked method. /// Receives the type that is a base type of , if found. + /// Receives a value indicating whether is a ValueTask (true) or Task (false). /// if could be found in the type hierarchy; otherwise . - private static bool TryGetTaskOfTOrValueTaskOfTType(TypeInfo taskTypeInfo, [NotNullWhen(true)] out TypeInfo? taskOfTTypeInfo) + private static bool TryGetTaskOfTOrValueTaskOfTType(TypeInfo taskTypeInfo, [NotNullWhen(true)] out TypeInfo? taskOfTTypeInfo, out bool isValueTask) { Requires.NotNull(taskTypeInfo, nameof(taskTypeInfo)); @@ -1781,18 +1822,28 @@ private static bool TryGetTaskOfTOrValueTaskOfTType(TypeInfo taskTypeInfo, [NotN TypeInfo? taskTypeInfoLocal = taskTypeInfo; while (taskTypeInfoLocal is not null) { - bool isTaskOfTOrValueTaskOfT = taskTypeInfoLocal.IsGenericType && - (taskTypeInfoLocal.GetGenericTypeDefinition() == typeof(Task<>) || taskTypeInfoLocal.GetGenericTypeDefinition() == typeof(ValueTask<>)); - if (isTaskOfTOrValueTaskOfT) + if (taskTypeInfoLocal.IsGenericType) { - taskOfTTypeInfo = taskTypeInfoLocal; - return true; + Type genericTypeDefinition = taskTypeInfoLocal.GetGenericTypeDefinition(); + if (genericTypeDefinition == typeof(Task<>)) + { + isValueTask = false; + taskOfTTypeInfo = taskTypeInfoLocal; + return true; + } + else if (genericTypeDefinition == typeof(ValueTask<>)) + { + isValueTask = true; + taskOfTTypeInfo = taskTypeInfoLocal; + return true; + } } taskTypeInfoLocal = taskTypeInfoLocal.BaseType?.GetTypeInfo(); } taskOfTTypeInfo = null; + isValueTask = false; return false; } @@ -1815,7 +1866,13 @@ private static bool TryGetTaskFromValueTask(object? result, [NotNullWhen(true)] TypeInfo resultTypeInfo = result.GetType().GetTypeInfo(); if (resultTypeInfo.IsGenericType && resultTypeInfo.GetGenericTypeDefinition() == typeof(ValueTask<>)) { - task = (Task)resultTypeInfo.GetDeclaredMethod(nameof(ValueTask.AsTask))!.Invoke(result, Array.Empty())!; + MethodInfo valueTaskAsTaskMethodInfo = +#if NET + (MethodInfo)resultTypeInfo.GetMemberWithSameMetadataDefinitionAs(ValueTaskAsTaskMethodInfo); +#else + resultTypeInfo.GetDeclaredMethod(nameof(ValueTask.AsTask))!; +#endif + task = (Task)valueTaskAsTaskMethodInfo.Invoke(result, Array.Empty())!; return true; } } @@ -2308,7 +2365,7 @@ private JsonRpcMessage HandleInvocationTaskResult(JsonRpcRequest request, Task t private async ValueTask HandleInvocationTaskOfTResultAsync(JsonRpcRequest request, Task t, CancellationToken cancellationToken) { // This method should only be called for methods that declare to return Task (or a derived type), or ValueTask. - Assumes.True(TryGetTaskOfTOrValueTaskOfTType(t.GetType().GetTypeInfo(), out TypeInfo? taskOfTTypeInfo)); + Assumes.True(TryGetTaskOfTOrValueTaskOfTType(t.GetType().GetTypeInfo(), out TypeInfo? taskOfTTypeInfo, out bool isValueTask)); object? result = null; Type? declaredResultType = null; @@ -2318,6 +2375,14 @@ private async ValueTask HandleInvocationTaskOfTResultAsync(JsonR // If t is just a Task, there is no Result property on it. // We can't really write direct code to deal with Task, since we have no idea of T in this context, so we simply use reflection to // read the result at runtime. +#if NET + MethodInfo resultGetter = isValueTask + ? (MethodInfo)taskOfTTypeInfo.GetMemberWithSameMetadataDefinitionAs(ValueTaskGetResultMethodInfo) + : (MethodInfo)taskOfTTypeInfo.GetMemberWithSameMetadataDefinitionAs(TaskGetResultMethodInfo); + + declaredResultType = resultGetter.ReturnType; + result = resultGetter.Invoke(t, Array.Empty()); +#else #pragma warning disable VSTHRD103 // misfiring analyzer https://github.com/Microsoft/vs-threading/issues/60 const string ResultPropertyName = nameof(Task.Result); #pragma warning restore VSTHRD103 @@ -2326,6 +2391,7 @@ private async ValueTask HandleInvocationTaskOfTResultAsync(JsonR Assumes.NotNull(resultProperty); declaredResultType = resultProperty.PropertyType; result = resultProperty.GetValue(t); +#endif // Transfer the ultimate success/failure result of the operation from the original successful method to the post-processing step. t = this.ProcessResultBeforeSerializingAsync(result, cancellationToken); @@ -2724,6 +2790,7 @@ private void ThrowIfConfigurationLocked() } } + [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] private T CreateProxy(ReadOnlySpan additionalContractInterfaces, ReadOnlySpan<(Type Type, int Code)> implementedOptionalInterfaces, JsonRpcProxyOptions? options, long? marshaledObjectHandle) where T : class { @@ -2739,8 +2806,16 @@ private T CreateProxy(ReadOnlySpan additionalContractInterfaces, ReadOn /// A set of customizations for how the client proxy is wired up. If , default options will be used. /// The handle to the remote object that is being marshaled via this proxy. /// An instance of the generated proxy. + [RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] private IJsonRpcClientProxyInternal CreateProxy(Type contractInterface, ReadOnlySpan additionalContractInterfaces, ReadOnlySpan<(Type Type, int Code)> implementedOptionalInterfaces, JsonRpcProxyOptions? options, long? marshaledObjectHandle) { +#if !NETSTANDARD2_0 + if (!RuntimeFeature.IsDynamicCodeSupported) + { + throw new NotSupportedException("CreateProxy is not supported if dynamic code is not supported."); + } +#endif + TypeInfo proxyType = ProxyGeneration.Get(contractInterface, additionalContractInterfaces, implementedOptionalInterfaces); return (IJsonRpcClientProxyInternal)Activator.CreateInstance( proxyType.AsType(), diff --git a/src/StreamJsonRpc/MessagePackFormatter.cs b/src/StreamJsonRpc/MessagePackFormatter.cs index b4c6726e9..d2aa723da 100644 --- a/src/StreamJsonRpc/MessagePackFormatter.cs +++ b/src/StreamJsonRpc/MessagePackFormatter.cs @@ -27,6 +27,7 @@ namespace StreamJsonRpc; /// The README on that project site describes use cases and its performance compared to alternative /// .NET MessagePack implementations and this one appears to be the best by far. /// +[RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] public class MessagePackFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcFormatterTracingCallbacks, IJsonRpcMessageFactory { /// @@ -803,6 +804,7 @@ public void Serialize(ref MessagePackWriter writer, RawMessagePack value, Messag } } + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] private class ProgressFormatterResolver : IFormatterResolver { private readonly MessagePackFormatter mainFormatter; @@ -870,6 +872,7 @@ public void Serialize(ref MessagePackWriter writer, TClass value, MessagePackSer /// /// Converts a progress token to an or an into a token. /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] private class PreciseTypeFormatter : IMessagePackFormatter { private readonly MessagePackFormatter formatter; @@ -910,6 +913,7 @@ public void Serialize(ref MessagePackWriter writer, TClass value, MessagePackSer } } + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] private class AsyncEnumerableFormatterResolver : IFormatterResolver { private readonly MessagePackFormatter mainFormatter; @@ -927,11 +931,11 @@ internal AsyncEnumerableFormatterResolver(MessagePackFormatter formatter) { if (!this.enumerableFormatters.TryGetValue(typeof(T), out IMessagePackFormatter? formatter)) { - if (TrackerHelpers>.IsActualInterfaceMatch(typeof(T))) + if (TrackerHelpers.IsIAsyncEnumerable(typeof(T))) { formatter = (IMessagePackFormatter?)Activator.CreateInstance(typeof(PreciseTypeFormatter<>).MakeGenericType(typeof(T).GenericTypeArguments[0]), new object[] { this.mainFormatter }); } - else if (TrackerHelpers>.FindInterfaceImplementedBy(typeof(T)) is { } iface) + else if (TrackerHelpers.FindIAsyncEnumerableInterfaceImplementedBy(typeof(T)) is { } iface) { formatter = (IMessagePackFormatter?)Activator.CreateInstance(typeof(GeneratorFormatter<,>).MakeGenericType(typeof(T), iface.GenericTypeArguments[0]), new object[] { this.mainFormatter }); } @@ -1071,6 +1075,7 @@ public void Serialize(ref MessagePackWriter writer, TClass value, MessagePackSer } } + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] private class PipeFormatterResolver : IFormatterResolver { private readonly MessagePackFormatter mainFormatter; @@ -1253,6 +1258,8 @@ public void Serialize(ref MessagePackWriter writer, T? value, MessagePackSeriali } } + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + [RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class RpcMarshalableResolver : IFormatterResolver { private readonly MessagePackFormatter formatter; @@ -1303,7 +1310,9 @@ internal RpcMarshalableResolver(MessagePackFormatter formatter) } #pragma warning disable CA1812 - private class RpcMarshalableFormatter(MessagePackFormatter messagePackFormatter, JsonRpcProxyOptions proxyOptions, JsonRpcTargetOptions targetOptions, RpcMarshalableAttribute rpcMarshalableAttribute) : IMessagePackFormatter + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + [RequiresUnreferencedCode(RuntimeReasons.RefEmit)] + private class RpcMarshalableFormatter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.NonPublicEvents | DynamicallyAccessedMemberTypes.Interfaces)] T>(MessagePackFormatter messagePackFormatter, JsonRpcProxyOptions proxyOptions, JsonRpcTargetOptions targetOptions, RpcMarshalableAttribute rpcMarshalableAttribute) : IMessagePackFormatter where T : class #pragma warning restore CA1812 { @@ -1336,6 +1345,8 @@ public void Serialize(ref MessagePackWriter writer, T? value, MessagePackSeriali /// 2. Be attributed with /// 3. Declare a constructor with a signature of (, ). /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + [RequiresUnreferencedCode(RuntimeReasons.LoadType)] private class MessagePackExceptionResolver : IFormatterResolver { /// @@ -1378,6 +1389,8 @@ internal MessagePackExceptionResolver(MessagePackFormatter formatter) #pragma warning disable CS8766 // Nullability of reference types in return type doesn't match implicitly implemented member (possibly because of nullability attributes). #pragma warning disable CA1812 + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + [RequiresUnreferencedCode(RuntimeReasons.LoadType)] private class ExceptionFormatter : IMessagePackFormatter where T : Exception #pragma warning restore CA1812 @@ -1476,6 +1489,7 @@ public void Serialize(ref MessagePackWriter writer, T? value, MessagePackSeriali #pragma warning restore } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class JsonRpcMessageFormatter : IMessagePackFormatter { private readonly MessagePackFormatter formatter; @@ -1539,6 +1553,7 @@ public void Serialize(ref MessagePackWriter writer, JsonRpcMessage value, Messag } } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class JsonRpcRequestFormatter : IMessagePackFormatter { private readonly MessagePackFormatter formatter; @@ -1788,6 +1803,7 @@ private static unsafe string ReadTraceState(ref MessagePackReader reader, Messag } } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class JsonRpcResultFormatter : IMessagePackFormatter { private readonly MessagePackFormatter formatter; @@ -1865,6 +1881,7 @@ public void Serialize(ref MessagePackWriter writer, Protocol.JsonRpcResult value } } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class JsonRpcErrorFormatter : IMessagePackFormatter { private readonly MessagePackFormatter formatter; @@ -1935,6 +1952,7 @@ public void Serialize(ref MessagePackWriter writer, Protocol.JsonRpcError value, } } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class JsonRpcErrorDetailFormatter : IMessagePackFormatter { private static readonly CommonString CodePropertyName = new("code"); diff --git a/src/StreamJsonRpc/PolyfillAttributes.cs b/src/StreamJsonRpc/PolyfillAttributes.cs new file mode 100644 index 000000000..e73281d72 --- /dev/null +++ b/src/StreamJsonRpc/PolyfillAttributes.cs @@ -0,0 +1,305 @@ +// Copyright (c) All contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma warning disable SA1649 // File name should match first type name +#pragma warning disable SA1402 // File may only contain a single type + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +namespace System.Diagnostics.CodeAnalysis +{ +#if !NET6_0_OR_GREATER + /// + /// Specifies the types of members that are dynamically accessed. This enumeration + /// has a System.FlagsAttribute attribute that allows a bitwise combination of its + /// member values. + /// + [Flags] + internal enum DynamicallyAccessedMemberTypes + { + /// + /// Specifies all members. + /// + All = -1, + + /// + /// Specifies no members. + /// + None = 0, + + /// + /// Specifies the default, parameterless public constructor. + /// + PublicParameterlessConstructor = 1, + + /// + /// Specifies all public constructors. + /// + PublicConstructors = 3, + + /// + /// Specifies all non-public constructors. + /// + NonPublicConstructors = 4, + + /// + /// Specifies all public methods. + /// + PublicMethods = 8, + + /// + /// Specifies all non-public methods. + /// + NonPublicMethods = 16, + + /// + /// Specifies all public fields. + /// + PublicFields = 32, + + /// + /// Specifies all non-public fields. + /// + NonPublicFields = 64, + + /// + /// Specifies all public nested types. + /// + PublicNestedTypes = 128, + + /// + /// Specifies all non-public nested types. + /// + NonPublicNestedTypes = 256, + + /// + /// Specifies all public properties. + /// + PublicProperties = 512, + + /// + /// Specifies all non-public properties. + /// + NonPublicProperties = 1024, + + /// + /// Specifies all public events. + /// + PublicEvents = 2048, + + /// + /// Specifies all non-public events. + /// + NonPublicEvents = 4096, + + /// + /// Specifies all interfaces implemented by the type. + /// + Interfaces = 8192, + } +#endif + +#if !NET8_0_OR_GREATER + /// + /// Indicates that the specified method requires the ability to generate new code at runtime, + /// for example through . + /// + /// + /// This allows tools to understand which methods are unsafe to call when compiling ahead of time. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)] + [Conditional("NEVERDEFINED")] // We don't need these attributes preserved in the compilation if the runtime doesn't support it. + internal sealed class RequiresDynamicCodeAttribute : Attribute + { + /// + /// Initializes a new instance of the class + /// with the specified message. + /// + /// + /// A message that contains information about the usage of dynamic code. + /// + public RequiresDynamicCodeAttribute(string message) + { + this.Message = message; + } + + /// + /// Gets a message that contains information about the usage of dynamic code. + /// + public string Message { get; } + + /// + /// Gets or sets an optional URL that contains more information about the method, + /// why it requires dynamic code, and what options a consumer has to deal with it. + /// + public string? Url { get; set; } + } +#endif + +#if !NET6_0_OR_GREATER + + /// + /// Suppresses reporting of a specific rule violation, allowing multiple suppressions on a + /// single code artifact. + /// + /// + /// is different than + /// in that it doesn't have a + /// . So it is always preserved in the compiled assembly. + /// + [AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] + [Conditional("NEVERDEFINED")] // We don't need these attributes preserved in the compilation if the runtime doesn't support it. + internal sealed class UnconditionalSuppressMessageAttribute : Attribute + { + /// + /// Initializes a new instance of the + /// class, specifying the category of the tool and the identifier for an analysis rule. + /// + /// The category for the attribute. + /// The identifier of the analysis rule the attribute applies to. + public UnconditionalSuppressMessageAttribute(string category, string checkId) + { + this.Category = category; + this.CheckId = checkId; + } + + /// + /// Gets the category identifying the classification of the attribute. + /// + /// + /// The property describes the tool or tool analysis category + /// for which a message suppression attribute applies. + /// + public string Category { get; } + + /// + /// Gets the identifier of the analysis tool rule to be suppressed. + /// + /// + /// Concatenated together, the and + /// properties form a unique check identifier. + /// + public string CheckId { get; } + + /// + /// Gets or sets the scope of the code that is relevant for the attribute. + /// + /// + /// The Scope property is an optional argument that specifies the metadata scope for which + /// the attribute is relevant. + /// + public string? Scope { get; set; } + + /// + /// Gets or sets a fully qualified path that represents the target of the attribute. + /// + /// + /// The property is an optional argument identifying the analysis target + /// of the attribute. An example value is "System.IO.Stream.ctor():System.Void". + /// Because it is fully qualified, it can be long, particularly for targets such as parameters. + /// The analysis tool user interface should be capable of automatically formatting the parameter. + /// + public string? Target { get; set; } + + /// + /// Gets or sets an optional argument expanding on exclusion criteria. + /// + /// + /// The property is an optional argument that specifies additional + /// exclusion where the literal metadata target is not sufficiently precise. For example, + /// the cannot be applied within a method, + /// and it may be desirable to suppress a violation against a statement in the method that will + /// give a rule violation, but not against all statements in the method. + /// + public string? MessageId { get; set; } + + /// + /// Gets or sets the justification for suppressing the code analysis message. + /// + public string? Justification { get; set; } + } + + /// + /// Indicates that the specified method requires dynamic access to code that is not referenced + /// statically, for example through . + /// + /// + /// This allows tools to understand which methods are unsafe to call when removing unreferenced + /// code from an application. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Class, Inherited = false)] + [Conditional("NEVERDEFINED")] // We don't need these attributes preserved in the compilation if the runtime doesn't support it. + internal sealed class RequiresUnreferencedCodeAttribute : Attribute + { + /// + /// Initializes a new instance of the class + /// with the specified message. + /// + /// + /// A message that contains information about the usage of unreferenced code. + /// + public RequiresUnreferencedCodeAttribute(string message) + { + this.Message = message; + } + + /// + /// Gets a message that contains information about the usage of unreferenced code. + /// + public string Message { get; } + + /// + /// Gets or sets an optional URL that contains more information about the method, + /// why it requires unreferenced code, and what options a consumer has to deal with it. + /// + public string? Url { get; set; } + } + + /// + /// Indicates that certain members on a specified are accessed dynamically, + /// for example through . + /// + /// + /// This allows tools to understand which members are being accessed during the execution + /// of a program. + /// + /// This attribute is valid on members whose type is or . + /// + /// When this attribute is applied to a location of type , the assumption is + /// that the string represents a fully qualified type name. + /// + /// When this attribute is applied to a class, interface, or struct, the members specified + /// can be accessed dynamically on instances returned from calling + /// on instances of that class, interface, or struct. + /// + /// If the attribute is applied to a method it's treated as a special case and it implies + /// the attribute should be applied to the "this" parameter of the method. As such the attribute + /// should only be used on instance methods of types assignable to System.Type (or string, but no methods + /// will use it there). + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter | + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Method | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, + Inherited = false)] + [Conditional("NEVERDEFINED")] // We don't need these attributes preserved in the compilation if the runtime doesn't support it. + internal sealed class DynamicallyAccessedMembersAttribute : Attribute + { + /// + /// Initializes a new instance of the class + /// with the specified member types. + /// + /// The types of members dynamically accessed. + public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes) + { + this.MemberTypes = memberTypes; + } + + /// + /// Gets the which specifies the type + /// of members dynamically accessed. + /// + public DynamicallyAccessedMemberTypes MemberTypes { get; } + } +#endif +} diff --git a/src/StreamJsonRpc/ProxyGeneration.cs b/src/StreamJsonRpc/ProxyGeneration.cs index c1b5598a4..0b312cf4a 100644 --- a/src/StreamJsonRpc/ProxyGeneration.cs +++ b/src/StreamJsonRpc/ProxyGeneration.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Reflection; using System.Reflection.Emit; @@ -16,6 +17,7 @@ namespace StreamJsonRpc; +[RequiresDynamicCode(RuntimeReasons.RefEmit), RequiresUnreferencedCode(RuntimeReasons.RefEmit)] internal static class ProxyGeneration { private static readonly List<(ImmutableHashSet SkipVisibilitySet, ModuleBuilder Builder)> TransparentProxyModuleBuilderByVisibilityCheck = new List<(ImmutableHashSet, ModuleBuilder)>(); diff --git a/src/StreamJsonRpc/Reflection/ExceptionSerializationHelpers.cs b/src/StreamJsonRpc/Reflection/ExceptionSerializationHelpers.cs index aebb931d6..9f11b586e 100644 --- a/src/StreamJsonRpc/Reflection/ExceptionSerializationHelpers.cs +++ b/src/StreamJsonRpc/Reflection/ExceptionSerializationHelpers.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.Serialization; @@ -24,6 +25,7 @@ internal static class ExceptionSerializationHelpers private static StreamingContext Context => new StreamingContext(StreamingContextStates.Remoting); + [RequiresUnreferencedCode(RuntimeReasons.LoadType)] internal static T Deserialize(JsonRpc jsonRpc, SerializationInfo info, TraceSource? traceSource) where T : Exception { @@ -158,7 +160,8 @@ private static void EnsureSerializableAttribute(Type runtimeType) } } - private static ConstructorInfo? FindDeserializingConstructor(Type runtimeType) => runtimeType.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, DeserializingConstructorParameterTypes, null); + private static ConstructorInfo? FindDeserializingConstructor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicConstructors)] Type runtimeType) + => runtimeType.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, DeserializingConstructorParameterTypes, null); private static bool TryGetValue(SerializationInfo info, string key, out string? value) { diff --git a/src/StreamJsonRpc/Reflection/MessageFormatterEnumerableTracker.cs b/src/StreamJsonRpc/Reflection/MessageFormatterEnumerableTracker.cs index 883f3d72a..24097f4ab 100644 --- a/src/StreamJsonRpc/Reflection/MessageFormatterEnumerableTracker.cs +++ b/src/StreamJsonRpc/Reflection/MessageFormatterEnumerableTracker.cs @@ -100,20 +100,14 @@ private interface IGeneratingEnumeratorTracker : System.IAsyncDisposable /// /// The type which may implement . /// true if given implements ; otherwise, false. - /// - /// We use as a generic type argument in this because what we use doesn't matter, but we must use *something*. - /// - public static bool CanSerialize(Type objectType) => TrackerHelpers>.CanSerialize(objectType); + public static bool CanSerialize(Type objectType) => TrackerHelpers.FindIAsyncEnumerableInterfaceImplementedBy(objectType) is not null; /// /// Checks if a given is exactly some closed generic type based on . /// /// The type which may be . /// true if given is ; otherwise, false. - /// - /// We use as a generic type argument in this because what we use doesn't matter, but we must use *something*. - /// - public static bool CanDeserialize(Type objectType) => TrackerHelpers>.CanDeserialize(objectType); + public static bool CanDeserialize(Type objectType) => TrackerHelpers.IsIAsyncEnumerable(objectType); /// /// Used by the generator to assign a handle to the given . diff --git a/src/StreamJsonRpc/Reflection/MessageFormatterProgressTracker.cs b/src/StreamJsonRpc/Reflection/MessageFormatterProgressTracker.cs index 4cf72b8c7..bd7c48ccf 100644 --- a/src/StreamJsonRpc/Reflection/MessageFormatterProgressTracker.cs +++ b/src/StreamJsonRpc/Reflection/MessageFormatterProgressTracker.cs @@ -70,7 +70,7 @@ public MessageFormatterProgressTracker(JsonRpc jsonRpc, IJsonRpcFormatterState f /// /// The type which may implement . /// The from given object, or if no such interface was found in the given . - public static Type? FindIProgressOfT(Type objectType) => TrackerHelpers>.FindInterfaceImplementedBy(objectType); + public static Type? FindIProgressOfT(Type objectType) => TrackerHelpers.FindIProgressInterfaceImplementedBy(objectType); /// /// Checks if a given implements . @@ -85,14 +85,14 @@ public MessageFormatterProgressTracker(JsonRpc jsonRpc, IJsonRpcFormatterState f /// /// The type which may implement . /// true if given implements ; otherwise, false. - public static bool CanSerialize(Type objectType) => TrackerHelpers>.CanSerialize(objectType); + public static bool CanSerialize(Type objectType) => FindIProgressOfT(objectType) is not null; /// /// Checks if a given is a closed generic of . /// /// The type which may be . /// true if given is ; otherwise, false. - public static bool CanDeserialize(Type objectType) => TrackerHelpers>.CanDeserialize(objectType); + public static bool CanDeserialize(Type objectType) => TrackerHelpers.IsIProgress(objectType); /// /// Gets a type token to use as replacement of an implementing in the JSON message. @@ -173,6 +173,7 @@ public bool TryGetProgressObject(long progressId, [NotNullWhen(true)] out Progre /// /// This overload creates an that does not use named arguments in its notifications. /// + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] public object CreateProgress(JsonRpc rpc, object token, Type valueType) => this.CreateProgress(rpc, token, valueType, clientRequiresNamedArguments: false); /// @@ -183,6 +184,7 @@ public bool TryGetProgressObject(long progressId, [NotNullWhen(true)] out Progre /// A generic type whose first generic type argument is to serve as the type argument for the created . /// to issue $/progress notifications using named args; to use positional arguments. #pragma warning disable CA1822 // Mark members as static + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] public object CreateProgress(JsonRpc rpc, object token, Type valueType, bool clientRequiresNamedArguments) #pragma warning restore CA1822 // Mark members as static { @@ -214,6 +216,10 @@ private void CleanUpResources(RequestId requestId) /// public class ProgressParamInformation { +#if NET + private static readonly MethodInfo ReportMethodInfo = typeof(IProgress<>).GetMethod("Report")!; +#endif + /// /// Gets the of . /// @@ -238,7 +244,11 @@ internal ProgressParamInformation(object progressObject, long token) Verify.Operation(iProgressOfTType is not null, Resources.FindIProgressOfTError); this.ValueType = iProgressOfTType.GenericTypeArguments[0]; +#if NET + this.reportMethod = (MethodInfo)iProgressOfTType.GetMemberWithSameMetadataDefinitionAs(ReportMethodInfo); +#else this.reportMethod = iProgressOfTType.GetRuntimeMethod(nameof(IProgress.Report), new Type[] { this.ValueType })!; +#endif this.progressObject = progressObject; this.Token = token; } diff --git a/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs b/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs index d91b1ea38..58162f94a 100644 --- a/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs +++ b/src/StreamJsonRpc/Reflection/MessageFormatterRpcMarshaledContextTracker.cs @@ -92,7 +92,11 @@ private enum MarshalMode MarshallingRealObject = 1, } - internal static bool TryGetMarshalOptionsForType(Type type, [NotNullWhen(true)] out JsonRpcProxyOptions? proxyOptions, [NotNullWhen(true)] out JsonRpcTargetOptions? targetOptions, [NotNullWhen(true)] out RpcMarshalableAttribute? rpcMarshalableAttribute) + internal static bool TryGetMarshalOptionsForType( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] Type type, + [NotNullWhen(true)] out JsonRpcProxyOptions? proxyOptions, + [NotNullWhen(true)] out JsonRpcTargetOptions? targetOptions, + [NotNullWhen(true)] out RpcMarshalableAttribute? rpcMarshalableAttribute) { proxyOptions = null; targetOptions = null; @@ -193,7 +197,12 @@ internal static RpcMarshalableOptionalInterfaceAttribute[] GetMarshalableOptiona /// The marshalable interface type of as declared in the RPC contract. /// The attribute that defines certain options that control which marshaling rules will be followed. /// A token to be serialized so the remote party can invoke methods on the marshaled object. - internal MarshalToken GetToken(object marshaledObject, JsonRpcTargetOptions options, Type declaredType, RpcMarshalableAttribute rpcMarshalableAttribute) + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + internal MarshalToken GetToken( + object marshaledObject, + JsonRpcTargetOptions options, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type declaredType, + RpcMarshalableAttribute rpcMarshalableAttribute) { if (this.formatterState.SerializingMessageWithId.IsEmpty) { @@ -284,6 +293,7 @@ internal MarshalToken GetToken(object marshaledObject, JsonRpcTargetOptions opti /// The token received from the remote party that includes the handle to the remote object. /// The options to feed into proxy generation. /// The generated proxy, or if is null. + [RequiresUnreferencedCode(RuntimeReasons.RefEmit), RequiresDynamicCode(RuntimeReasons.RefEmit)] [return: NotNullIfNotNull("token")] internal object? GetObject(Type interfaceType, MarshalToken? token, JsonRpcProxyOptions options) { @@ -393,7 +403,9 @@ internal MarshalToken GetToken(object marshaledObject, JsonRpcTargetOptions opti /// The attribute that appears on the interface. /// When is not a valid marshalable interface: this /// can happen if has properties, events or it is not disposable. - private static void ValidateMarshalableInterface(Type type, RpcMarshalableAttribute attribute) + private static void ValidateMarshalableInterface( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicEvents | DynamicallyAccessedMemberTypes.PublicProperties)] Type type, + RpcMarshalableAttribute attribute) { // We only require marshalable interfaces to derive from IDisposable when they are not call-scoped. if (!attribute.CallScopedLifetime && !typeof(IDisposable).IsAssignableFrom(type)) diff --git a/src/StreamJsonRpc/Reflection/RpcMarshalableOptionalInterfaceAttribute.cs b/src/StreamJsonRpc/Reflection/RpcMarshalableOptionalInterfaceAttribute.cs index c3fde5f8e..95e1703db 100644 --- a/src/StreamJsonRpc/Reflection/RpcMarshalableOptionalInterfaceAttribute.cs +++ b/src/StreamJsonRpc/Reflection/RpcMarshalableOptionalInterfaceAttribute.cs @@ -1,6 +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 System.Diagnostics.CodeAnalysis; + namespace StreamJsonRpc; /// @@ -35,7 +37,7 @@ public class RpcMarshalableOptionalInterfaceAttribute : Attribute /// updating RPC interfaces. /// The of the known optional interface that the marshalable /// object may implement. - public RpcMarshalableOptionalInterfaceAttribute(int optionalInterfaceCode, Type optionalInterface) + public RpcMarshalableOptionalInterfaceAttribute(int optionalInterfaceCode, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type optionalInterface) { this.OptionalInterfaceCode = optionalInterfaceCode; this.OptionalInterface = optionalInterface; @@ -44,6 +46,7 @@ public RpcMarshalableOptionalInterfaceAttribute(int optionalInterfaceCode, Type /// /// Gets the of the known optional interface that the marshalable object may implement. /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public Type OptionalInterface { get; } /// diff --git a/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs b/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs index 93370adbc..eb2eec802 100644 --- a/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs +++ b/src/StreamJsonRpc/Reflection/RpcTargetInfo.cs @@ -6,7 +6,6 @@ using System.Globalization; using System.Reflection; using System.Runtime.ExceptionServices; -using Microsoft; using Microsoft.VisualStudio.Threading; using StreamJsonRpc.Protocol; @@ -95,7 +94,7 @@ public async ValueTask DisposeAsync() } } - internal static MethodNameMap GetMethodNameMap(TypeInfo type) + internal static MethodNameMap GetMethodNameMap([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TypeInfo type) { MethodNameMap? map; lock (MethodNameMaps) @@ -185,7 +184,12 @@ internal bool TryGetTargetMethod(JsonRpcRequest request, [NotNullWhen(true)] out /// /// When multiple target objects are added, the first target with a method that matches a request is invoked. /// - internal RevertAddLocalRpcTarget? AddLocalRpcTarget(Type exposingMembersOn, object target, JsonRpcTargetOptions? options, bool requestRevertOption) + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] + internal RevertAddLocalRpcTarget? AddLocalRpcTarget( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type exposingMembersOn, + object target, + JsonRpcTargetOptions? options, + bool requestRevertOption) { RevertAddLocalRpcTarget? revert = requestRevertOption ? new RevertAddLocalRpcTarget(this) : null; options = options ?? JsonRpcTargetOptions.Default; @@ -251,7 +255,7 @@ internal bool TryGetTargetMethod(JsonRpcRequest request, [NotNullWhen(true)] out /// Target to invoke when incoming messages are received. /// A set of customizations for how the target object is registered. If , default options will be used. /// An optional object that may be disposed of to revert the addition of the target object.. - internal void AddRpcInterfaceToTarget(Type exposingMembersOn, object target, JsonRpcTargetOptions? options, RevertAddLocalRpcTarget? revertAddLocalRpcTarget) + internal void AddRpcInterfaceToTarget([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type exposingMembersOn, object target, JsonRpcTargetOptions? options, RevertAddLocalRpcTarget? revertAddLocalRpcTarget) { Requires.Argument(exposingMembersOn.IsInterface, nameof(exposingMembersOn), Resources.AddRpcInterfaceToTargetParameterNotInterface); @@ -322,7 +326,11 @@ internal void UnregisterEventHandlersFromTargetObjects() /// /// /// Dictionary which maps a request method name to its clr method name. - private static IReadOnlyDictionary> GetRequestMethodToClrMethodMap(TypeInfo exposedMembersOnType, bool allowNonPublicInvocation, bool useSingleObjectParameterDeserialization, bool clientRequiresNamedArguments) + private static IReadOnlyDictionary> GetRequestMethodToClrMethodMap( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TypeInfo exposedMembersOnType, + bool allowNonPublicInvocation, + bool useSingleObjectParameterDeserialization, + bool clientRequiresNamedArguments) { Requires.NotNull(exposedMembersOnType, nameof(exposedMembersOnType)); @@ -344,26 +352,32 @@ private static IReadOnlyDictionary> GetRequestMeth MethodNameMap mapping = GetMethodNameMap(exposedMembersOnType); // We retrieve exposed types differently for interfaces vs. classes - var typesToMap = new List(); if (exposedMembersOnType.IsInterface) { - Type[] ifaces = exposedMembersOnType.GetInterfaces(); - typesToMap.Capacity = 1 + ifaces.Length; - typesToMap.Add(exposedMembersOnType.GetTypeInfo()); - foreach (Type iface in ifaces) - { - typesToMap.Add(iface.GetTypeInfo()); - } + ActOn(exposedMembersOnType.GetTypeInfo()); + ActOnInterfaces(exposedMembersOnType); } else { for (TypeInfo? t = exposedMembersOnType.GetTypeInfo(); t is not null && t != typeof(object).GetTypeInfo(); t = t.BaseType?.GetTypeInfo()) { - typesToMap.Add(t); + ActOn(t); } } - foreach (TypeInfo t in typesToMap) +#if !NET10_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2062:UnrecognizedReflectionPattern", Justification = "exposedMembersOnType is annotated as preserve All members, so any Types returned from GetInterfaces should be preserved as well. See https://github.com/dotnet/linker/issues/1731.")] +#endif + void ActOnInterfaces([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type exposedMembersOnType) + { + Type[] interfaces = exposedMembersOnType.GetInterfaces(); + for (int i = 0; i < interfaces.Length; i++) + { + ActOn(interfaces[i].GetTypeInfo()); + } + } + + void ActOn([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TypeInfo t) { // As we enumerate methods, skip accessor methods foreach (MethodInfo method in t.DeclaredMethods.Where(m => !m.IsSpecialName)) @@ -480,7 +494,10 @@ private static IReadOnlyDictionary> GetRequestMeth /// /// Type to reflect over and analyze its events. /// A list of EventInfos found. - private static IReadOnlyList GetEventInfos(TypeInfo exposedMembersOnType) +#if !NET10_0_OR_GREATER + [UnconditionalSuppressMessage("Trimming", "IL2065:UnrecognizedReflectionPattern", Justification = "false positive: https://github.com/dotnet/linker/issues/1731")] +#endif + private static IReadOnlyList GetEventInfos([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TypeInfo exposedMembersOnType) { List eventInfos = new List(); @@ -497,10 +514,10 @@ private static IReadOnlyList GetEventInfos(TypeInfo exposedMembersOnT if (exposedMembersOnType.IsInterface) { - Type[] ifaces = exposedMembersOnType.GetInterfaces(); - foreach (Type iface in ifaces) + Type[] interfaces = exposedMembersOnType.GetInterfaces(); + for (int i = 0; i < interfaces.Length; i++) { - foreach (EventInfo evt in iface.GetTypeInfo().DeclaredEvents) + foreach (EventInfo evt in interfaces[i].GetTypeInfo().DeclaredEvents) { if (evt.AddMethod is object && !evt.AddMethod.IsStatic) { @@ -573,7 +590,8 @@ internal class MethodNameMap private readonly Dictionary methodAttributes = new Dictionary(); private readonly Dictionary ignoreAttributes = new Dictionary(); - internal MethodNameMap(TypeInfo typeInfo) + [UnconditionalSuppressMessage("Trimming", "IL2111:UnrecognizedReflectionPattern", Justification = "typeInfo is annotated as preserve All members, so any Types returned from ImplementedInterfaces should be preserved as well. See https://github.com/dotnet/linker/issues/1731.")] + internal MethodNameMap([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TypeInfo typeInfo) { Requires.NotNull(typeInfo, nameof(typeInfo)); this.interfaceMaps = typeInfo.IsInterface ? default @@ -742,14 +760,15 @@ internal void RecordObjectToDispose(object target) private class EventReceiver : IDisposable { - private static readonly MethodInfo OnEventRaisedMethodInfo = typeof(EventReceiver).GetTypeInfo().DeclaredMethods.Single(m => m.Name == nameof(OnEventRaised)); - private static readonly MethodInfo OnEventRaisedGenericMethodInfo = typeof(EventReceiver).GetTypeInfo().DeclaredMethods.Single(m => m.Name == nameof(OnEventRaisedGeneric)); + private static readonly MethodInfo OnEventRaisedMethodInfo = typeof(EventReceiver).GetMethod(nameof(OnEventRaised), BindingFlags.Instance | BindingFlags.NonPublic)!; + private static readonly MethodInfo OnEventRaisedGenericMethodInfo = typeof(EventReceiver).GetMethod(nameof(OnEventRaisedGeneric), BindingFlags.Instance | BindingFlags.NonPublic)!; private readonly JsonRpc jsonRpc; private readonly object server; private readonly EventInfo eventInfo; private readonly Delegate registeredHandler; private readonly string rpcEventName; + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] internal EventReceiver(JsonRpc jsonRpc, object server, EventInfo eventInfo, JsonRpcTargetOptions options) { Requires.NotNull(jsonRpc, nameof(jsonRpc)); @@ -770,7 +789,11 @@ internal EventReceiver(JsonRpc jsonRpc, object server, EventInfo eventInfo, Json // It will work for EventHandler and EventHandler, at least. // If we want to support more, we'll likely have to use lightweight code-gen to generate a method // with the right signature. - ParameterInfo[] eventHandlerParameters = eventInfo.EventHandlerType!.GetTypeInfo().GetMethod("Invoke")!.GetParameters(); + [UnconditionalSuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.", Justification = "False positive: https://github.com/dotnet/runtime/issues/114113")] + static ParameterInfo[] GetParameters(EventInfo eventInfo) => + eventInfo.EventHandlerType!.GetTypeInfo().GetMethod("Invoke")!.GetParameters(); + + ParameterInfo[] eventHandlerParameters = GetParameters(eventInfo); if (eventHandlerParameters.Length != 2) { throw new NotSupportedException($"Unsupported event handler type for: \"{eventInfo.Name}\". Expected 2 parameters but had {eventHandlerParameters.Length}."); @@ -783,7 +806,13 @@ internal EventReceiver(JsonRpc jsonRpc, object server, EventInfo eventInfo, Json } else { - MethodInfo closedGenericMethod = OnEventRaisedGenericMethodInfo.MakeGenericMethod(argsType); + [UnconditionalSuppressMessage("Trimming", "IL2060", Justification = "The generic method we construct has no dynamic member access requirements.")] + static MethodInfo GetOnEventRaisedClosedGenericMethod(Type argsType) + { + return OnEventRaisedGenericMethodInfo.MakeGenericMethod(argsType); + } + + MethodInfo closedGenericMethod = GetOnEventRaisedClosedGenericMethod(argsType); this.registeredHandler = closedGenericMethod.CreateDelegate(eventInfo.EventHandlerType!, this); } } diff --git a/src/StreamJsonRpc/Reflection/TrackerHelpers.cs b/src/StreamJsonRpc/Reflection/TrackerHelpers.cs index a21611bc7..a2c32a24f 100644 --- a/src/StreamJsonRpc/Reflection/TrackerHelpers.cs +++ b/src/StreamJsonRpc/Reflection/TrackerHelpers.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace StreamJsonRpc.Reflection; @@ -8,41 +9,40 @@ namespace StreamJsonRpc.Reflection; /// /// Helper methods for message formatter tracker classes. /// -/// A generic interface. We only need the generic type definition, but since C# doesn't let us pass in open generic types, use as a generic type argument. -internal static class TrackerHelpers - where TInterface : class +internal static class TrackerHelpers { /// - /// Dictionary to record the calculation made in to obtain the type from a given . + /// Dictionary to record the calculation made in to obtain the IProgress{T} type from a given . /// - private static readonly Dictionary TypeToImplementedInterfaceMap = new(); + private static readonly Dictionary TypeToIProgressMap = new(); /// - /// Gets the generic type definition for whatever type parameter was given by . + /// Dictionary to record the calculation made in to obtain the IAsyncEnumerable{T} type from a given . /// - private static readonly Type InterfaceGenericTypeDefinition = typeof(TInterface).GetGenericTypeDefinition(); + private static readonly Dictionary TypeToIAsyncEnumerableMap = new(); /// - /// Extracts some interface from a given , if it is implemented. + /// Extracts the IProgress{T} interface from a given , if it is implemented. /// - /// The type which may implement . - /// The type from given object, or if no such interface was found in the given . - internal static Type? FindInterfaceImplementedBy(Type objectType) + /// The type which may implement IProgress{T}. + /// The IProgress{T} type from given object, or if no such interface was found in the given . + [UnconditionalSuppressMessage("Trimming", "IL2070:UnrecognizedReflectionPattern", Justification = "The 'IProgress<>' Type must exist and so trimmer kept it. In which case It also kept it on any type which implements it. The below call to GetInterfaces may return fewer results when trimmed but it will return 'IProgress<>' if the type implemented it, even after trimming.")] + internal static Type? FindIProgressInterfaceImplementedBy(Type objectType) { Requires.NotNull(objectType, nameof(objectType)); - if (objectType.IsConstructedGenericType && objectType.GetGenericTypeDefinition().Equals(InterfaceGenericTypeDefinition)) + if (objectType.IsConstructedGenericType && objectType.GetGenericTypeDefinition().Equals(typeof(IProgress<>))) { return objectType; } Type? interfaceFromType; - lock (TypeToImplementedInterfaceMap) + lock (TypeToIProgressMap) { - if (!TypeToImplementedInterfaceMap.TryGetValue(objectType, out interfaceFromType)) + if (!TypeToIProgressMap.TryGetValue(objectType, out interfaceFromType)) { - interfaceFromType = objectType.GetTypeInfo().GetInterfaces().FirstOrDefault(i => i.IsConstructedGenericType && i.GetGenericTypeDefinition() == InterfaceGenericTypeDefinition); - TypeToImplementedInterfaceMap.Add(objectType, interfaceFromType); + interfaceFromType = objectType.GetTypeInfo().GetInterfaces().FirstOrDefault(i => i.IsConstructedGenericType && i.GetGenericTypeDefinition() == typeof(IProgress<>)); + TypeToIProgressMap.Add(objectType, interfaceFromType); } } @@ -50,23 +50,44 @@ internal static class TrackerHelpers } /// - /// Checks if a given implements . + /// Checks whether the given type is the IProgress{T} interface. /// - /// The type which may implement . - /// true if given implements ; otherwise, false. - internal static bool CanSerialize(Type objectType) => FindInterfaceImplementedBy(objectType) is not null; + /// The type to check. + /// if is a closed generic form of IProgress{T}; otherwise. + internal static bool IsIProgress(Type objectType) => Requires.NotNull(objectType, nameof(objectType)).IsConstructedGenericType && objectType.GetGenericTypeDefinition() == typeof(IProgress<>); /// - /// Checks whether the given type is an interface compatible with . + /// Extracts the IAsyncEnumerable{T} interface from a given , if it is implemented. /// - /// The type that may be deserialized. - /// if is a closed generic form of ; otherwise. - internal static bool CanDeserialize(Type objectType) => IsActualInterfaceMatch(objectType); + /// The type which may implement IAsyncEnumerable{T}. + /// The IAsyncEnumerable{T} type from given object, or if no such interface was found in the given . + [UnconditionalSuppressMessage("Trimming", "IL2070:UnrecognizedReflectionPattern", Justification = "The 'IAsyncEnumerable<>' Type must exist and so trimmer kept it. In which case It also kept it on any type which implements it. The below call to GetInterfaces may return fewer results when trimmed but it will return 'IAsyncEnumerable<>' if the type implemented it, even after trimming.")] + internal static Type? FindIAsyncEnumerableInterfaceImplementedBy(Type objectType) + { + Requires.NotNull(objectType, nameof(objectType)); + + if (objectType.IsConstructedGenericType && objectType.GetGenericTypeDefinition().Equals(typeof(IAsyncEnumerable<>))) + { + return objectType; + } + + Type? interfaceFromType; + lock (TypeToIAsyncEnumerableMap) + { + if (!TypeToIAsyncEnumerableMap.TryGetValue(objectType, out interfaceFromType)) + { + interfaceFromType = objectType.GetTypeInfo().GetInterfaces().FirstOrDefault(i => i.IsConstructedGenericType && i.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)); + TypeToIAsyncEnumerableMap.Add(objectType, interfaceFromType); + } + } + + return interfaceFromType; + } /// - /// Checks whether the given type is an interface compatible with . + /// Checks whether the given type is the IAsyncEnumerable{T} interface. /// - /// The type that may be deserialized. - /// if is a closed generic form of ; otherwise. - internal static bool IsActualInterfaceMatch(Type objectType) => Requires.NotNull(objectType, nameof(objectType)).IsConstructedGenericType && objectType.GetGenericTypeDefinition().Equals(InterfaceGenericTypeDefinition); + /// The type to check. + /// if is a closed generic form of IAsyncEnumerable{T}; otherwise. + internal static bool IsIAsyncEnumerable(Type objectType) => Requires.NotNull(objectType, nameof(objectType)).IsConstructedGenericType && objectType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>); } diff --git a/src/StreamJsonRpc/RuntimeReasons.cs b/src/StreamJsonRpc/RuntimeReasons.cs new file mode 100644 index 000000000..294ec367e --- /dev/null +++ b/src/StreamJsonRpc/RuntimeReasons.cs @@ -0,0 +1,19 @@ +// 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; + +internal static class RuntimeReasons +{ + internal const string CloseGenerics = "This code closes generic types or methods at runtime."; + + internal const string ExtractTypes = "This code uses Reflection to extract types from other Types."; + + internal const string RefEmit = "This code generates IL at runtime and executes it."; + + internal const string Formatters = "This code uses a formatter/serializer that hasn't been hardened to avoid dynamic code."; + + internal const string LoadType = "This code loads a type from a string at runtime."; + + internal const string UntypedRpcTarget = "This code adds an untyped object as an RPC target."; +} diff --git a/src/StreamJsonRpc/SkipClrVisibilityChecks.cs b/src/StreamJsonRpc/SkipClrVisibilityChecks.cs index 7c2cec48a..6f094d9ab 100644 --- a/src/StreamJsonRpc/SkipClrVisibilityChecks.cs +++ b/src/StreamJsonRpc/SkipClrVisibilityChecks.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Emit; using System.Runtime.InteropServices; @@ -17,6 +18,8 @@ namespace StreamJsonRpc; /// Gives a dynamic assembly the ability to skip CLR visibility checks, /// allowing the assembly to access private members of another assembly. /// +[RequiresDynamicCode(RuntimeReasons.RefEmit)] +[RequiresUnreferencedCode(RuntimeReasons.RefEmit)] internal class SkipClrVisibilityChecks { /// @@ -130,7 +133,7 @@ internal void SkipVisibilityChecksFor(AssemblyName assemblyName) } } - private static IEnumerable ThisAndBaseTypes(TypeInfo interfaceType) + private static IEnumerable ThisAndBaseTypes([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] TypeInfo interfaceType) { Assumes.True(interfaceType.IsInterface); yield return interfaceType.GetTypeInfo(); diff --git a/src/StreamJsonRpc/StreamJsonRpc.csproj b/src/StreamJsonRpc/StreamJsonRpc.csproj index d13c2cf16..cd1e33d19 100644 --- a/src/StreamJsonRpc/StreamJsonRpc.csproj +++ b/src/StreamJsonRpc/StreamJsonRpc.csproj @@ -8,6 +8,8 @@ visualstudio stream json rpc jsonrpc $(NoWarn);SYSLIB0050 + + true diff --git a/src/StreamJsonRpc/SystemTextJsonFormatter.cs b/src/StreamJsonRpc/SystemTextJsonFormatter.cs index 49dac1d73..8a4a1d1e4 100644 --- a/src/StreamJsonRpc/SystemTextJsonFormatter.cs +++ b/src/StreamJsonRpc/SystemTextJsonFormatter.cs @@ -24,7 +24,8 @@ namespace StreamJsonRpc; /// /// A formatter that emits UTF-8 encoded JSON where user data should be serializable via the . /// -public class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcMessageFactory, IJsonRpcFormatterTracingCallbacks +[RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] +public partial class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, IJsonRpcMessageTextFormatter, IJsonRpcInstanceContainer, IJsonRpcMessageFactory, IJsonRpcFormatterTracingCallbacks { private static readonly JsonWriterOptions WriterOptions = new() { }; @@ -35,6 +36,7 @@ public class SystemTextJsonFormatter : FormatterBase, IJsonRpcMessageFormatter, /// private static readonly JsonSerializerOptions BuiltInSerializerOptions = new() { + TypeInfoResolver = SourceGenerationContext.Default, Converters = { RequestIdJsonConverter.Instance, @@ -392,6 +394,7 @@ private static class Utf8Strings #pragma warning restore SA1300 // Element should begin with upper-case letter } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class TopLevelPropertyBag : TopLevelPropertyBagBase { private readonly JsonDocument? incomingMessage; @@ -454,6 +457,7 @@ protected internal override bool TryGetTopLevelProperty(string name, [MaybeNu } } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class JsonRpcRequest : JsonRpcRequestBase { private readonly SystemTextJsonFormatter formatter; @@ -601,6 +605,7 @@ private static int CountArguments(JsonElement arguments) } } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class JsonRpcResult : JsonRpcResultBase { private readonly SystemTextJsonFormatter formatter; @@ -657,6 +662,7 @@ protected override void ReleaseBuffers() } } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class JsonRpcError : JsonRpcErrorBase { private readonly SystemTextJsonFormatter formatter; @@ -686,6 +692,7 @@ protected override void ReleaseBuffers() this.formatter.deserializingDocument = null; } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] internal new class ErrorDetail : Protocol.JsonRpcError.ErrorDetail { private readonly SystemTextJsonFormatter formatter; @@ -766,6 +773,7 @@ public override void Write(Utf8JsonWriter writer, RequestId value, JsonSerialize } } + [RequiresDynamicCode(RuntimeReasons.Formatters)] private class ProgressConverterFactory : JsonConverterFactory { private readonly SystemTextJsonFormatter formatter; @@ -775,17 +783,18 @@ internal ProgressConverterFactory(SystemTextJsonFormatter formatter) this.formatter = formatter; } - public override bool CanConvert(Type typeToConvert) => TrackerHelpers>.FindInterfaceImplementedBy(typeToConvert) is not null; + public override bool CanConvert(Type typeToConvert) => TrackerHelpers.FindIProgressInterfaceImplementedBy(typeToConvert) is not null; public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - Type? iface = TrackerHelpers>.FindInterfaceImplementedBy(typeToConvert); + Type? iface = TrackerHelpers.FindIProgressInterfaceImplementedBy(typeToConvert); Assumes.NotNull(iface); Type genericTypeArg = iface.GetGenericArguments()[0]; Type converterType = typeof(Converter<>).MakeGenericType(genericTypeArg); return (JsonConverter)Activator.CreateInstance(converterType, this.formatter)!; } + [RequiresDynamicCode(RuntimeReasons.CloseGenerics)] private class Converter : JsonConverter> { private readonly SystemTextJsonFormatter formatter; @@ -816,6 +825,7 @@ public override void Write(Utf8JsonWriter writer, IProgress value, JsonSerial } } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class AsyncEnumerableConverter : JsonConverterFactory { private readonly SystemTextJsonFormatter formatter; @@ -825,17 +835,18 @@ internal AsyncEnumerableConverter(SystemTextJsonFormatter formatter) this.formatter = formatter; } - public override bool CanConvert(Type typeToConvert) => TrackerHelpers>.FindInterfaceImplementedBy(typeToConvert) is not null; + public override bool CanConvert(Type typeToConvert) => TrackerHelpers.FindIAsyncEnumerableInterfaceImplementedBy(typeToConvert) is not null; public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - Type? iface = TrackerHelpers>.FindInterfaceImplementedBy(typeToConvert); + Type? iface = TrackerHelpers.FindIAsyncEnumerableInterfaceImplementedBy(typeToConvert); Assumes.NotNull(iface); Type genericTypeArg = iface.GetGenericArguments()[0]; Type converterType = typeof(Converter<>).MakeGenericType(genericTypeArg); return (JsonConverter)Activator.CreateInstance(converterType, this.formatter)!; } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class Converter : JsonConverter> { private readonly SystemTextJsonFormatter formatter; @@ -885,6 +896,7 @@ public override void Write(Utf8JsonWriter writer, IAsyncEnumerable value, Jso } } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class RpcMarshalableConverterFactory : JsonConverterFactory { private readonly SystemTextJsonFormatter formatter; @@ -910,6 +922,7 @@ public override bool CanConvert(Type typeToConvert) attribute)!; } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class Converter(SystemTextJsonFormatter formatter, JsonRpcProxyOptions proxyOptions, JsonRpcTargetOptions targetOptions, RpcMarshalableAttribute rpcMarshalableAttribute) : JsonConverter where T : class { @@ -1015,6 +1028,7 @@ public override void Write(Utf8JsonWriter writer, Stream value, JsonSerializerOp } } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class ExceptionConverter : JsonConverter { /// @@ -1102,6 +1116,7 @@ public override void Write(Utf8JsonWriter writer, Exception value, JsonSerialize } } + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class JsonConverterFormatter : IFormatterConverter { private readonly JsonSerializerOptions serializerOptions; @@ -1217,6 +1232,7 @@ public object Convert(object value, TypeCode typeCode) /// }; /// ]]> /// + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] private class DataContractResolver : IJsonTypeInfoResolver { private readonly ConcurrentDictionary typeInfoCache = new(); @@ -1259,7 +1275,10 @@ public DataContractResolver() return typeInfo; } - private static void PopulateMembersInfos(Type type, JsonTypeInfo jsonTypeInfo, DataContractAttribute? dataContractAttribute) + private static void PopulateMembersInfos( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicFields | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.PublicProperties)] Type type, + JsonTypeInfo jsonTypeInfo, + DataContractAttribute? dataContractAttribute) { BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance; @@ -1297,7 +1316,10 @@ private static void PopulateMembersInfos(Type type, JsonTypeInfo jsonTypeInfo, D } } - bool TryCreateJsonPropertyInfo(MemberInfo memberInfo, Type propertyType, [NotNullWhen(true)] out JsonPropertyInfo? jsonPropertyInfo) + bool TryCreateJsonPropertyInfo( + MemberInfo memberInfo, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type propertyType, + [NotNullWhen(true)] out JsonPropertyInfo? jsonPropertyInfo) { DataMemberAttribute? dataMemberAttribute = memberInfo.GetCustomAttribute(); if ((dataContractAttribute is null || dataMemberAttribute is not null) && memberInfo.GetCustomAttribute() is null) @@ -1355,4 +1377,7 @@ internal void Deactivate() this.jsonString = null; } } + + [JsonSerializable(typeof(RequestId))] + private partial class SourceGenerationContext : JsonSerializerContext; } diff --git a/src/StreamJsonRpc/WebSocketMessageHandler.cs b/src/StreamJsonRpc/WebSocketMessageHandler.cs index 0bf9425b8..66348a5bd 100644 --- a/src/StreamJsonRpc/WebSocketMessageHandler.cs +++ b/src/StreamJsonRpc/WebSocketMessageHandler.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Buffers; +using System.Diagnostics.CodeAnalysis; using System.Net.WebSockets; using System.Runtime.InteropServices; using Nerdbank.Streams; @@ -30,6 +31,7 @@ public class WebSocketMessageHandler : MessageHandlerBase, IJsonRpcMessageBuffer /// The used to communicate. /// This will not be automatically disposed of with this . /// + [RequiresDynamicCode(RuntimeReasons.Formatters), RequiresUnreferencedCode(RuntimeReasons.Formatters)] public WebSocketMessageHandler(WebSocket webSocket) : this(webSocket, new JsonMessageFormatter()) { diff --git a/test/NativeAOTCompatibility.Test/NativeAOTCompatibility.Test.csproj b/test/NativeAOTCompatibility.Test/NativeAOTCompatibility.Test.csproj new file mode 100644 index 000000000..de2d43594 --- /dev/null +++ b/test/NativeAOTCompatibility.Test/NativeAOTCompatibility.Test.csproj @@ -0,0 +1,19 @@ + + + + Exe + net8.0 + true + + + + + + + + + + + + diff --git a/test/NativeAOTCompatibility.Test/Program.cs b/test/NativeAOTCompatibility.Test/Program.cs new file mode 100644 index 000000000..4da373b15 --- /dev/null +++ b/test/NativeAOTCompatibility.Test/Program.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma warning disable SA1402 // File may only contain a single type +#pragma warning disable SA1649 // File name should match first type name + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using Nerdbank.Streams; +using StreamJsonRpc; + +Console.WriteLine("This test is run by \"dotnet publish -r [RID]-x64\" rather than by executing the program."); + +// That said, this "program" can run select scenarios to verify that they work in a Native AOT environment. +// When TUnit fixes https://github.com/thomhurst/TUnit/issues/2458, we can move this part of the program to unit tests. +(Stream clientPipe, Stream serverPipe) = FullDuplexStream.CreatePair(); +JsonRpc serverRpc = new JsonRpc(new HeaderDelimitedMessageHandler(serverPipe, CreateFormatter())); +JsonRpc clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientPipe, CreateFormatter())); +serverRpc.AddLocalRpcMethod("Add", new Server().Add); +serverRpc.StartListening(); +clientRpc.StartListening(); + +int sum = await clientRpc.InvokeAsync(nameof(Server.Add), 2, 5); +Console.WriteLine($"2 + 5 = {sum}"); + +// When properly configured, this formatter is safe in Native AOT scenarios for +// the very limited use case shown in this program. +[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Using the Json source generator.")] +[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Using the Json source generator.")] +IJsonRpcMessageFormatter CreateFormatter() => new SystemTextJsonFormatter() +{ + JsonSerializerOptions = { TypeInfoResolver = SourceGenerationContext.Default }, +}; + +internal class Server +{ + public int Add(int a, int b) => a + b; +} + +[JsonSerializable(typeof(int))] +internal partial class SourceGenerationContext : JsonSerializerContext;