Skip to content

Commit

Permalink
Update SK2 and SteamUnifiedMessages in particular to be more Trim-fri…
Browse files Browse the repository at this point in the history
…endly
  • Loading branch information
yaakov-h committed Aug 1, 2021
1 parent 6624c33 commit 807fa51
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 54 deletions.
4 changes: 2 additions & 2 deletions Samples/8.UnifiedMessages/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,14 @@ static void OnLoggedOn( SteamUser.LoggedOnCallback callback )
// now that we're logged onto Steam, lets query the IPlayer service for our badge levels

// first, build our request object, these are autogenerated and can normally be found in the SteamKit2.Internal namespace
CPlayer_GetGameBadgeLevels_Request req = new CPlayer_GetGameBadgeLevels_Request
var req = new CPlayer_GetGameBadgeLevels_Request
{
// we want to know our 440 (TF2) badge level
appid = 440,
};

// now lets send the request, this is done by building an expression tree with the IPlayer interface
badgeRequest = playerService.SendMessage( x => x.GetGameBadgeLevels( req ) );
badgeRequest = playerService.SendMessage( nameof(IPlayer.GetGameBadgeLevels), req );

// alternatively, the request can be made using SteamUnifiedMessages directly, but then you must build the service request name manually
// the name format is in the form of <Service>.<Method>#<Version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ internal ServiceMethodResponse( JobID jobID, EResult result, CMsgClientServiceMe
/// <summary>
/// This callback represents a service notification recieved though <see cref="SteamUnifiedMessages"/>.
/// </summary>
[RequiresUnreferencedCode( SteamUnifiedMessages.TrimmingMessageOfShame )]
public class ServiceMethodNotification : CallbackMsg
{
/// <summary>
Expand Down Expand Up @@ -106,15 +105,19 @@ public string RpcName
/// </summary>
public object Body { get; private set; }


[RequiresUnreferencedCode( SteamUnifiedMessages.TrimmingMessageOfShame )]
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage( "ReflectionAnalysis", "IL2060", Justification = "Method should be kept available." )]
#endif
internal ServiceMethodNotification( [DynamicallyAccessedMembers(Trimming.ForProtobufNet)] Type messageType, IPacketMsg packetMsg )
{
// Bounce into generic-land.
var setupMethod = GetType().GetMethod( nameof(Setup), BindingFlags.Static | BindingFlags.NonPublic )!.MakeGenericMethod( messageType )!;
var setupMethod = GetTypeWithPrivateMethods<ServiceMethodNotification>().GetMethod( nameof(Setup), BindingFlags.Static | BindingFlags.NonPublic )!.MakeGenericMethod( messageType )!;
(MethodName, Body) = ((string, object))setupMethod.Invoke( this, new[] { packetMsg } )!;
}

[return: DynamicallyAccessedMembers( DynamicallyAccessedMemberTypes.NonPublicMethods )]
static Type GetTypeWithPrivateMethods< [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.NonPublicMethods)] T>() => typeof( T );

static (string methodName, object body) Setup< [DynamicallyAccessedMembers(Trimming.ForProtobufNet)] T>( IPacketMsg packetMsg )
where T : IExtensible, new()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@


using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
Expand All @@ -20,12 +21,10 @@ namespace SteamKit2
/// </summary>
public partial class SteamUnifiedMessages : ClientMsgHandler
{
internal const string TrimmingMessageOfShame = "SteamUnifiedMessages needs to be rewritten in order to become compatible with trimming.";

/// <summary>
/// This wrapper is used for expression-based RPC calls using Steam Unified Messaging.
/// </summary>
public class UnifiedService<TService>
public class UnifiedService<[DynamicallyAccessedMembers( Trimming.ForProtobufNet )] TService>
{
internal UnifiedService( SteamUnifiedMessages steamUnifiedMessages )
{
Expand All @@ -43,7 +42,7 @@ internal UnifiedService( SteamUnifiedMessages steamUnifiedMessages )
/// <param name="expr">RPC call expression, e.g. x => x.SomeMethodCall(message);</param>
/// <param name="isNotification">Whether this message is a notification or not.</param>
/// <returns>The JobID of the request. This can be used to find the appropriate <see cref="ServiceMethodResponse"/>.</returns>
[RequiresUnreferencedCode( SteamUnifiedMessages.TrimmingMessageOfShame )]
[RequiresUnreferencedCode("Expressions require unreferenced code. Use the other overload instead.")]
public AsyncJob<ServiceMethodResponse> SendMessage<[DynamicallyAccessedMembers( Trimming.ForProtobufNet )] TResponse>( Expression<Func<TService, TResponse>> expr, bool isNotification = false )
{
if ( expr == null )
Expand All @@ -69,17 +68,34 @@ internal UnifiedService( SteamUnifiedMessages steamUnifiedMessages )
throw new NotSupportedException( "Unknown Expression type" );
}

var serviceName = typeof(TService).Name.Substring( 1 ); // IServiceName - remove 'I'
var methodName = methodInfo.Name;
var version = 1;

var rpcName = string.Format( "{0}.{1}#{2}", serviceName, methodName, version );

var rpcName = GetRpcName( methodInfo.Name );
var method = typeof(SteamUnifiedMessages).GetMethod( nameof(SteamUnifiedMessages.SendMessage) )!.MakeGenericMethod( message.GetType() );
var result = method.Invoke( this.steamUnifiedMessages, new[] { rpcName, message, isNotification } );
return ( AsyncJob<ServiceMethodResponse> )result!;
}


/// <summary>
/// Sends a message.
/// Results are returned in a <see cref="ServiceMethodResponse"/>.
/// The returned <see cref="AsyncJob{T}"/> can also be awaited to retrieve the callback result.
/// </summary>
/// <typeparam name="TRequest">The type of the protobuf object which is the request of the RPC call.</typeparam>
/// <param name="methodName">RPC call method name, e.g. nameof(ISomeService.SomeMethod)</param>
/// <param name="request">Request object</param>
/// <param name="isNotification">Whether this message is a notification or not.</param>
/// <returns>The JobID of the request. This can be used to find the appropriate <see cref="ServiceMethodResponse"/>.</returns>
public AsyncJob<ServiceMethodResponse> SendMessage<[DynamicallyAccessedMembers( Trimming.ForProtobufNet )] TRequest>( string methodName, TRequest request, bool isNotification = false )
where TRequest : IExtensible
{
if ( request == null )
{
throw new ArgumentNullException( nameof( request ) );
}

var rpcName = GetRpcName( methodName );
return steamUnifiedMessages.SendMessage<TRequest>( rpcName, request, isNotification );
}

static MethodCallExpression ExtractMethodCallExpression<TResponse>( Expression<Func<TService, TResponse>> expression, string paramName )
{
switch ( expression.NodeType )
Expand All @@ -100,21 +116,41 @@ static MethodCallExpression ExtractMethodCallExpression<TResponse>( Expression<F

throw new ArgumentException( "Expression must be a method call.", paramName );
}
}

static string GetRpcName( string methodName ) => SteamUnifiedMessages.GetRpcName( typeof( TService ), methodName );
static string GetRpcName( string methodName, int version ) => SteamUnifiedMessages.GetRpcName( typeof( TService ), methodName, version );
}

readonly ConcurrentDictionary<string, Type> services;
Dictionary<EMsg, Action<IPacketMsg>> dispatchMap;

[RequiresUnreferencedCode( SteamUnifiedMessages.TrimmingMessageOfShame )]
internal SteamUnifiedMessages()
{
dispatchMap = new Dictionary<EMsg, Action<IPacketMsg>>
{
{ EMsg.ClientServiceMethodLegacyResponse, HandleClientServiceMethodResponse },
{ EMsg.ServiceMethod, HandleServiceMethod },
};

services = new ConcurrentDictionary<string, Type>();
}

static string GetRpcName( Type serviceType, string methodName ) => GetRpcName( GetServiceName( serviceType ), methodName );
static string GetRpcName( Type serviceType, string methodName, int version ) => GetRpcName( GetServiceName( serviceType ), methodName, version );

static string GetServiceName( Type serviceType )
{
var serviceName = serviceType.Name;
if ( serviceName.Length > 0 && serviceName[ 0 ] == 'I' )
{
serviceName = serviceName.Substring( 1 );
}
return serviceName;
}

static string GetRpcName( string serviceName, string methodName ) => GetRpcName( serviceName, methodName, version: 1 );
static string GetRpcName( string serviceName, string methodName, int version ) => string.Format( "{0}.{1}#{2}", serviceName, methodName, version );

/// <summary>
/// Sends a message.
/// Results are returned in a <see cref="ServiceMethodResponse"/>.
Expand Down Expand Up @@ -155,11 +191,29 @@ internal SteamUnifiedMessages()
/// </summary>
/// <typeparam name="TService">The type of a service interface.</typeparam>
/// <returns>The <see cref="UnifiedService&lt;TService&gt;"/> wrapper.</returns>
public UnifiedService<TService> CreateService<TService>()
public UnifiedService<TService> CreateService<[DynamicallyAccessedMembers( Trimming.ForProtobufNet )] TService>()
{
RegisterService<TService>();
return new UnifiedService<TService>( this );
}

/// <summary>
/// Registers a service type in order to recieve notifications. It is not neccesary to call this if
/// <see cref="CreateService{TService}"/> has been called with the same TService parameter.
/// </summary>
/// <typeparam name="TService">The type of a service interface.</typeparam>
public void RegisterService<[DynamicallyAccessedMembers( Trimming.ForProtobufNet )] TService>()
{
const string ServiceTypePrefix = "SteamKit2.Internal.I";
var serviceType = typeof( TService );
if ( serviceType.FullName is null || !serviceType.FullName.StartsWith( ServiceTypePrefix, StringComparison.Ordinal ) )
{
throw new InvalidOperationException( "Service type provided is not a generated SteamKit2 service interface." );
}
var serviceName = serviceType.FullName.Substring( ServiceTypePrefix.Length );
services.TryAdd( serviceName, typeof( TService ) );
}


/// <summary>
/// Handles a client message. This should not be called directly.
Expand Down Expand Up @@ -187,7 +241,14 @@ void HandleClientServiceMethodResponse( IPacketMsg packetMsg )
Client.PostCallback( callback );
}

[RequiresUnreferencedCode( SteamUnifiedMessages.TrimmingMessageOfShame )]
[return: DynamicallyAccessedMembers( Trimming.ForProtobufNet )]
Type? GetServiceInterfaceType( string serviceName )
=> services.TryGetValue( serviceName, out var serviceInterfaceType ) ? serviceInterfaceType : null;

#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage( "ReflectionAnalysis", "IL2072: UnrecognizedReflectionPattern",
Justification = "Any properties of the notification type that the consumer is using will have to be preserved by the linker anyway." )]
#endif
void HandleServiceMethod( IPacketMsg packetMsg )
{
var notification = new ClientMsgProtobuf( packetMsg );
Expand All @@ -201,9 +262,7 @@ void HandleServiceMethod( IPacketMsg packetMsg )
var serviceName = splitByDot[0];
var methodName = splitByHash[0];

var serviceInterfaceName = "SteamKit2.Internal.I" + serviceName;
var serviceInterfaceType = Type.GetType( serviceInterfaceName );
if (serviceInterfaceType != null)
if ( GetServiceInterfaceType( serviceName ) is { } serviceInterfaceType )
{
var method = serviceInterfaceType.GetMethod( methodName );
if ( method != null )
Expand Down
7 changes: 1 addition & 6 deletions SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using ProtoBuf;
Expand Down Expand Up @@ -39,7 +38,6 @@ public sealed partial class SteamClient : CMClient
/// <summary>
/// Initializes a new instance of the <see cref="SteamClient"/> class with the default configuration.
/// </summary>
[RequiresUnreferencedCode( SteamUnifiedMessages.TrimmingMessageOfShame )]
public SteamClient()
: this( SteamConfiguration.CreateDefault() )
{
Expand All @@ -49,7 +47,6 @@ public SteamClient()
/// Initializes a new instance of the <see cref="SteamClient"/> class a specific identifier.
/// </summary>
/// <param name="identifier">A specific identifier to be used to uniquely identify this instance.</param>
[RequiresUnreferencedCode( SteamUnifiedMessages.TrimmingMessageOfShame )]
public SteamClient( string identifier )
: this( SteamConfiguration.CreateDefault(), identifier )
{
Expand All @@ -60,7 +57,6 @@ public SteamClient( string identifier )
/// </summary>
/// <param name="configuration">The configuration to use for this client.</param>
/// <exception cref="ArgumentNullException">The configuration object is <c>null</c></exception>
[RequiresUnreferencedCode( SteamUnifiedMessages.TrimmingMessageOfShame )]
public SteamClient( SteamConfiguration configuration )
: this( configuration, Guid.NewGuid().ToString( "N" ) )
{
Expand All @@ -73,7 +69,6 @@ public SteamClient( SteamConfiguration configuration )
/// <param name="identifier">A specific identifier to be used to uniquely identify this instance.</param>
/// <exception cref="ArgumentNullException">The configuration object or identifier is <c>null</c></exception>
/// <exception cref="ArgumentException">The identifier is an empty string</exception>
[RequiresUnreferencedCode( SteamUnifiedMessages.TrimmingMessageOfShame )]
public SteamClient( SteamConfiguration configuration, string identifier )
: base( configuration, identifier )
{
Expand Down Expand Up @@ -384,7 +379,7 @@ protected override bool OnClientMsgReceived( IPacketMsg? packetMsg )
}
catch ( Exception ex )
{
LogDebug( "SteamClient", "Unhandled '{0}' exception from '{1}' handler: '{2}'", ex.GetType().Name, key.Name, ex.Message );
LogDebug( "SteamClient", "Unhandled '{0}' exception from '{1}' handler: '{2}'", ex.GetType().Name, key.Name, ex );
Disconnect();
return false;
}
Expand Down
2 changes: 2 additions & 0 deletions SteamKit2/SteamKit2/SteamKit2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
<ProjectGuid>{4B2B0365-DE37-4B65-B614-3E4E7C05147D}</ProjectGuid>
<LangVersion Condition="'$(TargetFramework)' == 'netstandard2.0'">8.0</LangVersion>
<Nullable>enable</Nullable>
<IsTrimmable>true</IsTrimmable>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
</PropertyGroup>

<PropertyGroup>
Expand Down
47 changes: 23 additions & 24 deletions SteamKit2/SteamKit2/Util/TrimmingAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,37 @@
{
static class Trimming
{
public const DynamicallyAccessedMemberTypes ForProtobufNet =
DynamicallyAccessedMemberTypes.PublicConstructors |
DynamicallyAccessedMemberTypes.NonPublicConstructors |
DynamicallyAccessedMemberTypes.PublicMethods |
DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.PublicFields |
DynamicallyAccessedMemberTypes.NonPublicFields |
DynamicallyAccessedMemberTypes.PublicProperties |
DynamicallyAccessedMemberTypes.NonPublicProperties;
// BUG BUG BUG: https://github.com/mono/linker/issues/2185
// Ideally we should be able to list the same set at protobuf-net (all public-and non-public constructors, fields, method, and properties).
// However since we have nested types, we also need to set public and non-public nested types.
// In practice (at least as of .NET 6 Preview 6), the properties of the nested types get linked away. The implementation of the property getter
// and setter is replaced with `throw new NotSupportedException("Linked away")`, which makes me wonder what on earth the linker was thinking, to
// keep the type but not the implementation of any of its members. This then manifests itself at runtime in a trimmed application as:
// Unhandled 'InvalidOperationException' exception from 'SteamFriends' handler: 'Cannot apply changes to property SteamKit2.Internal.CMsgClientFriendsList+Friend.ulfriendid'
public const DynamicallyAccessedMemberTypes ForProtobufNet = DynamicallyAccessedMemberTypes.All;
}

#if !NET5_0_OR_GREATER

[Flags]
internal enum DynamicallyAccessedMemberTypes
{
All = -1,
None = 0,
PublicParameterlessConstructor = 1,
PublicConstructors = 3,
NonPublicConstructors = 4,
PublicMethods = 8,
NonPublicMethods = 16,
PublicFields = 32,
NonPublicFields = 64,
PublicNestedTypes = 128,
NonPublicNestedTypes = 256,
PublicProperties = 512,
NonPublicProperties = 1024,
PublicEvents = 2048,
NonPublicEvents = 4096,
Interfaces = 8192
PublicParameterlessConstructor = 0x0001,
PublicConstructors = 0x0002 | PublicParameterlessConstructor,
NonPublicConstructors = 0x0004,
PublicMethods = 0x0008,
NonPublicMethods = 0x0010,
PublicFields = 0x0020,
NonPublicFields = 0x0040,
PublicNestedTypes = 0x0080,
NonPublicNestedTypes = 0x0100,
PublicProperties = 0x0200,
NonPublicProperties = 0x0400,
PublicEvents = 0x0800,
NonPublicEvents = 0x1000,
Interfaces = 0x2000,
All = ~None
}

[AttributeUsage( AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Interface | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, Inherited = false )]
Expand Down

0 comments on commit 807fa51

Please sign in to comment.