using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Threading; using System.Threading.Tasks; using JetBrains.Core; using JetBrains.Diagnostics; using JetBrains.Lifetimes; using JetBrains.Rd.Tasks; using JetBrains.Util; namespace JetBrains.Rd.Reflection { public class ProxyGenerator : IProxyGenerator { private static ILog ourLog = Log.GetLog(); private const String DynamicAssemblyName = "JetBrains.Rd.ProxyGenerator"; private readonly bool myAllowSave; // todo remove /* * ValueTuple package does not exist for net35 */ public struct FakeTuple { public T1 Item1; public FakeTuple(T1 item1) { Item1 = item1; } } public struct FakeTuple { public T1 Item1; public T2 Item2; public FakeTuple(T1 item1, T2 item2) { Item1 = item1; Item2 = item2; } } public struct FakeTuple { public T1 Item1; public T2 Item2; public T3 Item3; public FakeTuple(T1 item1, T2 item2, T3 item3) { Item1 = item1; Item2 = item2; Item3 = item3; } } public struct FakeTuple { public T1 Item1; public T2 Item2; public T3 Item3; public T4 Item4; public FakeTuple(T1 item1, T2 item2, T3 item3, T4 item4) { Item1 = item1; Item2 = item2; Item3 = item3; Item4 = item4; } } public struct FakeTuple { public T1 Item1; public T2 Item2; public T3 Item3; public T4 Item4; public T5 Item5; public FakeTuple(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5) { Item1 = item1; Item2 = item2; Item3 = item3; Item4 = item4; Item5 = item5; } } public struct FakeTuple { public T1 Item1; public T2 Item2; public T3 Item3; public T4 Item4; public T5 Item5; public T6 Item6; public FakeTuple(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6) { Item1 = item1; Item2 = item2; Item3 = item3; Item4 = item4; Item5 = item5; Item6 = item6; } } public struct FakeTuple { public T1 Item1; public T2 Item2; public T3 Item3; public T4 Item4; public T5 Item5; public T6 Item6; public T7 Item7; public FakeTuple(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6, T7 item7) { Item1 = item1; Item2 = item2; Item3 = item3; Item4 = item4; Item5 = item5; Item6 = item6; Item7 = item7; } } public struct FakeTuple { public T1 Item1; public T2 Item2; public T3 Item3; public T4 Item4; public T5 Item5; public T6 Item6; public T7 Item7; public TRest Rest; public FakeTuple(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6, T7 item7, TRest rest) { Item1 = item1; Item2 = item2; Item3 = item3; Item4 = item4; Item5 = item5; Item6 = item6; Item7 = item7; Rest = rest; } } public static readonly Type[] ValueTuples = new[] { typeof(FakeTuple<>), typeof(FakeTuple<,>), typeof(FakeTuple<,,>), typeof(FakeTuple<,,,>), typeof(FakeTuple<,,,,>), typeof(FakeTuple<,,,,,>), typeof(FakeTuple<,,,,,,>), // T1, T2, T3, T4, T5, T6, T7 typeof(FakeTuple<,,,,,,,>), // T1, T2, T3, T4, T5, T6, T7, TRest }; public const int MaxTuplePayload = 7; private static Lazy ourLazyMembers => new Lazy(() => new ProxyGeneratorMembers()); private static ProxyGeneratorMembers Members => ourLazyMembers.Value; private readonly Lazy myAssemblyBuilder; private readonly Lazy myModuleBuilder; public AssemblyBuilder DynamicAssembly => myAssemblyBuilder.Value; public ModuleBuilder DynamicModule => myModuleBuilder.Value; public ProxyGenerator(bool allowSave = false) { myAllowSave = allowSave; #if NETSTANDARD myAssemblyBuilder = new Lazy(() => AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(DynamicAssemblyName), AssemblyBuilderAccess.Run)); myModuleBuilder = new Lazy(() => myAssemblyBuilder.Value.DefineDynamicModule(DynamicAssemblyName)); #else myAssemblyBuilder = new Lazy(() => AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(DynamicAssemblyName), allowSave ? AssemblyBuilderAccess.RunAndSave : AssemblyBuilderAccess.Run)); if (allowSave) myModuleBuilder = new Lazy(() => myAssemblyBuilder.Value.DefineDynamicModule(DynamicAssemblyName, "RdProxy.dll")); else myModuleBuilder = new Lazy(() => myAssemblyBuilder.Value.DefineDynamicModule(DynamicAssemblyName)); #endif } public Type CreateType(Type interfaceType) { if (!interfaceType.IsInterface) throw new ArgumentException("Only interfaces are supported."); if (interfaceType.GetGenericArguments().Length > 0) throw new ArgumentException("Generic interfaces are not supported."); // RdRpc attribute can be specified in RdExt attribute. Therefore, now this assert cannot be verified. // if (!ReflectionSerializerVerifier.IsRpcAttributeDefined(typeof(TInterface))) // throw new ArgumentException($"Unable to create proxy for {typeof(TInterface)}. No {nameof(RdRpcAttribute)} specified."); lock (myModuleBuilder.Value) { var moduleBuilder = myModuleBuilder.Value; var className = interfaceType.Name.Substring(1); var proxyTypeName = "Proxy." + className; var typebuilder = moduleBuilder.DefineType( proxyTypeName, TypeAttributes.NotPublic | TypeAttributes.Class | TypeAttributes.Sealed, typeof(RdExtReflectionBindableBase)); // Implement interface typebuilder.AddInterfaceImplementation(interfaceType); // mark it as proxy type typebuilder.AddInterfaceImplementation(typeof(IProxyTypeMarker)); // Add RdExt attribute to type var rdExtConstructor = Members.RdExtConstructor; typebuilder.SetCustomAttribute(new CustomAttributeBuilder(rdExtConstructor, new object[] { interfaceType })); var ctx = new TypeBuilderContext(typebuilder); ImplementInterface(interfaceType, ctx); foreach (var baseInterface in interfaceType.GetInterfaces()) ImplementInterface(baseInterface, ctx); void ImplementInterface(Type baseInterface, TypeBuilderContext ctx) { if (baseInterface.GetCustomAttribute() is { } timeouts) { ctx.SetDefaultTimeout(timeouts); } foreach (var member in baseInterface.GetMembers(BindingFlags.Instance | BindingFlags.Public)) { ImplementMember(ctx, member); } } if (ctx.TimeoutFields.IsValueCreated) { var cctor = typebuilder.DefineTypeInitializer(); var il = cctor.GetILGenerator(); foreach (var kvp in ctx.TimeoutFields.Value) { // Re-create RpcTimeouts attribute. It used only to re-create RpcTimeouts. It is more robust to use types for // ProxyGeneration which are located on our project. il.Emit(OpCodes.Ldc_I8, kvp.Value.WarnAwaitTime.Ticks); il.Emit(OpCodes.Ldc_I8, kvp.Value.ErrorAwaitTime.Ticks); il.Emit(OpCodes.Call, ProxyGeneratorMembers.CreateRpcTimeoutMethod); // set the static field il.Emit(OpCodes.Stsfld, kvp.Key); } il.Emit(OpCodes.Ret); } return typebuilder.CreateTypeInfo().NotNull("Unable to create type"); } } /// /// Wrap method into Tuple-like adapter for using regular .NEt method as RdCall endpoint. /// /// Expected signature for sync methods /// (this, Lifetime, TReq) → RdTask{TRes} /// async methods: /// (this, Lifetime, TReq) → Task{TRes} /// /// public DynamicMethod CreateAdapter(Type selfType, MethodInfo method) { Assertion.Require(!method.IsGenericMethod, "generics are not supported"); Assertion.Require(!method.IsStatic, "only instance methods are supported"); lock (myModuleBuilder.Value) { // var type = ModuleBuilder.DefineType(selfType.FullName + "_adapter", // TypeAttributes.Public & TypeAttributes.Sealed & TypeAttributes.Abstract & TypeAttributes.BeforeFieldInit); var requestType = GetRequstType(method)[0]; var responseType = GetResponseType(method, unwrapTask: false); Type returnType; if (IsSync(method)) { returnType = typeof(RdTask<>).MakeGenericType(responseType); } else { returnType = responseType; } var methodBuilder = new DynamicMethod(method.Name, returnType, new[] { selfType, typeof(Lifetime), requestType }, myModuleBuilder.Value); var il = methodBuilder.GetILGenerator(); // Invoke adapter method il.Emit(OpCodes.Ldarg_0); // this/self FieldInfo[] fields; if (requestType == typeof(Unit)) { fields = new FieldInfo[0]; } else { fields = requestType.GetFields(); } var parameters = method.GetParameters(); for (int parameterIndex = 0, fi = 0; parameterIndex < parameters.Length; parameterIndex++) { if (parameters[parameterIndex].ParameterType == typeof(Lifetime)) { LoadArgument(il, 1 /* external cancellation lifetime in SetHandler */); } else { il.Emit(OpCodes.Ldarg_2); // value tuple int i = fi; var f = fields; while (i >= MaxTuplePayload) { il.Emit(OpCodes.Ldfld, f[MaxTuplePayload]); f = f[MaxTuplePayload].FieldType.GetFields(); i -= MaxTuplePayload; } if (Mode.IsAssertion) Assertion.Assert(parameters[parameterIndex].ParameterType == f[i].FieldType, "parameters[pi].ParameterType == fields[i].FieldType"); il.Emit(OpCodes.Ldfld, f[i]); fi++; } } // call wrapped method il.Emit(OpCodes.Callvirt, method); // load Unit result if necessary if (method.ReturnType == typeof(void) && IsSync(method)) { il.Emit(OpCodes.Ldsfld, Members.UnitInstance); } if (IsSync(method)) { // Create RdTask var taskFactoryMethod = typeof(RdTask).GetMethod(nameof(RdTask.Successful))?.MakeGenericMethod(responseType); il.Emit(OpCodes.Call, taskFactoryMethod.NotNull("RdTask.Successful not found")); } else { // regular task already on stack } il.Emit(OpCodes.Ret); /* if (myAllowSave) { // shadow methods are required only for reviewing dynamic methods bodies in dotpeek var typeBuilder = ModuleBuilder.DefineType(selfType.FullName + "_shadow"); foreach (var dynamicMethod in result) { var shadowMethod = typeBuilder.DefineMethod(dynamicMethod.Name, MethodAttributes.Static | MethodAttributes.Public, dynamicMethod.ReturnType, dynamicMethod.GetParameters().Select(p => p.ParameterType).ToArray()); var body = dynamicMethod.GetMethodBody(); shadowMethod.SetMethodBody(body.GetILAsByteArray(), body.MaxStackSize, new byte[0], new ExceptionHandler[0], new int[0]); } typeBuilder.CreateType(); }*/ return methodBuilder; } } public static bool IsSync(MethodInfo impl) { var returnType = impl.ReturnType; return (returnType != typeof(Task)) && (!returnType.IsGenericType || returnType.GetGenericTypeDefinition() != typeof(Task<>)); } private void ImplementMember(TypeBuilderContext ctx, MemberInfo member) { switch (member.MemberType) { case MemberTypes.Constructor: throw new NotSupportedException("Unexpected constructor member in an interface."); case MemberTypes.Event: throw new NotSupportedException("Events delegation not supported yet."); case MemberTypes.Field: throw new NotSupportedException("Unexpected field member in an interface."); case MemberTypes.Method: if (member is MethodInfo { IsSpecialName: false } method) { ImplementMethod(ctx, method); } break; case MemberTypes.Property: ImplementProperty(ctx, ((PropertyInfo)member)); break; default: var ex = new InvalidOperationException("Unexpected Member Type bit fields combination."); throw ex; } } private void ImplementProperty(TypeBuilderContext ctx, PropertyInfo propertyInfo) { var typebuilder = ctx.Builder; var type = propertyInfo.PropertyType; var property = typebuilder.DefineProperty(propertyInfo.Name, PropertyAttributes.HasDefault, type, EmptyArray.Instance); var field = typebuilder.DefineField(MakeBackingFieldName(propertyInfo.Name), type, FieldAttributes.Private); if (propertyInfo.GetSetMethod() != null) { throw new Exception("Setter for properties in proxy interface is prohibited due to unclear semantic"); } if (propertyInfo.GetGetMethod() != null) { var getMethod = typebuilder.DefineMethod(propertyInfo.GetGetMethod().Name, MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.Private, type, EmptyArray.Instance); var il = getMethod.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldfld, field); il.Emit(OpCodes.Ret); typebuilder.DefineMethodOverride(getMethod, propertyInfo.GetGetMethod()); } } /// /// Get the list of tuples, used to /// /// Note the special treatment of Lifetime and CancellationToken types - they are not included in the result. /// /// /// public static Type[] GetRequstType(MethodInfo method) { var parameters = method.GetParameters(); if (parameters.Length == 0) return new[] {typeof(Unit)}; var parms = new List(parameters.Length); foreach (var p in parameters) { // Lifetime treats as cancellation token if (p.ParameterType != typeof(Lifetime) || p.ParameterType == typeof(CancellationToken)) parms.Add(p.ParameterType); } if (parms.Count == 0) return new[] {typeof(Unit)}; Type? type = null; while (parms.Count > 0) { // fold the last aligned to MaxTupleArgs types to the "Rest" value type var rest = parms.Count % MaxTuplePayload; var toFold = rest == 0 ? MaxTuplePayload : rest; var foldTypeArguments = parms.GetRange(parms.Count - toFold, toFold); parms.RemoveRange(parms.Count - toFold, toFold); if (type != null) foldTypeArguments.Add(type); type = ValueTuples[foldTypeArguments.Count - 1].MakeGenericType(foldTypeArguments.ToArray()); } return new[] { type! }; } public static Type GetResponseType(MethodInfo method, bool unwrapTask = false) { if (method.ReturnType == typeof(void)) return typeof(Unit); if (unwrapTask && !IsSync(method)) { if (method.ReturnType == typeof(Task)) return typeof(Unit); var arguments = method.ReturnType.GetGenericArguments(); if (arguments.Length == 1) return arguments[0]; } return method.ReturnType; } private void ImplementMethod(TypeBuilderContext ctx, MethodInfo method) { var typebuilder = ctx.Builder; // add field for IRdCall instance var requestType = GetRequstType(method)[0]; var responseType = GetResponseType(method, true); Assertion.Require(!requestType.IsByRef, "ByRef is not supported. ({0}.{1})", typebuilder, requestType); Assertion.Require(!responseType.IsByRef, "ByRef is not supported. ({0}.{1})", typebuilder, responseType); var fieldType = typeof(IRdCall<,>).MakeGenericType(requestType, responseType); var field = typebuilder.DefineField(ProxyFieldName(method), fieldType , FieldAttributes.Public); var isSyncCall = !typeof(IAsyncResult).IsAssignableFrom(method.ReturnType); FieldInfo? timeoutsField = null; if (isSyncCall && method.GetCustomAttribute() is { } timeouts) { timeoutsField = ctx.DefineCustomRpcTimeout(timeouts, method); } var parameters = method.GetParameters(); MethodBuilder methodbuilder = typebuilder.DefineMethod(method.Name, MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.Private, method.CallingConvention, method.ReturnType, method.ReturnParameter.GetRequiredCustomModifiers(), method.ReturnParameter.GetOptionalCustomModifiers(), parameters.Select(param => param.ParameterType).ToArray(), parameters.Select(param => param.GetRequiredCustomModifiers()).ToArray(), parameters.Select(param => param.GetOptionalCustomModifiers()).ToArray()); ILGenerator ilgen = methodbuilder.GetILGenerator(); // load IRdCall field for further call ilgen.Emit(OpCodes.Ldarg_0); ilgen.Emit(OpCodes.Ldfld, field); int lifetimeArgument = -1; // Lifetime for (int i = 0; i < parameters.Length; i++) { if (parameters[i].ParameterType == typeof(Lifetime)) { Assertion.Require(lifetimeArgument == -1, "Only one lifetime parameter is allowed"); lifetimeArgument = i; } } if (lifetimeArgument != -1) LoadArgument(ilgen, lifetimeArgument + 1); else { ilgen.Emit(OpCodes.Call, Members.EternalLifetimeGet); } // TReq if (parameters.Length - (lifetimeArgument == -1 ? 0 : 1) > 0) { // Others arguments, skip `this` argument (0) for (int i = 0; i < parameters.Length; i++) { if (i != lifetimeArgument) { ReflectionSerializerVerifier.AssertValidScalar(parameters[i].ParameterType.GetTypeInfo()); // load args LoadArgument(ilgen, i + 1 /* #0 is `self/this` argument */); } } var genArgs = requestType.GetGenericArguments(); if (genArgs.Length > MaxTuplePayload) { var toInit = new Stack(); var rest = genArgs[MaxTuplePayload]; while (rest != null) { toInit.Push(rest.GetConstructors().Single()); var ar = rest.GetGenericArguments(); rest = ar.Length > MaxTuplePayload ? ar[MaxTuplePayload] : null; } while (toInit.Count != 0) { ilgen.Emit(OpCodes.Newobj, toInit.Pop()); } } // create tuple and load it to stack ilgen.Emit(OpCodes.Newobj, requestType.GetConstructors().Single()); } else { ilgen.Emit(OpCodes.Ldsfld, Members.UnitInstance); } if (isSyncCall) { // RpcTimeouts var rpcTimeoutsField = timeoutsField ?? ctx.DefaultTimeoutField; if (rpcTimeoutsField != null) ilgen.Emit(OpCodes.Ldsfld, rpcTimeoutsField); else ilgen.Emit(OpCodes.Ldnull); ilgen.Emit(OpCodes.Call, Members.SyncNested4.MakeGenericMethod(requestType, responseType)); } else { // Start(Lifetime, TReq, Scheduler) var startMethod = ProxyGeneratorMembers.StartRdCall(fieldType); // async ilgen.Emit(OpCodes.Ldnull); // ResponseScheduler ilgen.Emit(OpCodes.Callvirt, startMethod.NotNull("fieldType.GetMethod(Start) != null")); ilgen.Emit(OpCodes.Call, Members.ToTask.MakeGenericMethod(responseType)); } if (method.ReturnType == typeof(void)) { ilgen.Emit(OpCodes.Pop); } else { // ilgen.Emit(OpCodes.Ldnull); } ilgen.Emit(OpCodes.Ret); typebuilder.DefineMethodOverride(methodbuilder, method); } private static string MakeBackingFieldName(string propertyName) { // Debug.Assert((char)GeneratedNameKind.AutoPropertyBackingField == 'k'); return "<" + propertyName + ">k__BackingField"; } public static string ProxyFieldName(MethodInfo method) => $"{method}_proxy"; /// /// Return the expected list of names in BindableChildren collection for interfaces /// public static IEnumerable GetBindableFieldsNames(Type rpcInterface) { foreach (var i in rpcInterface.GetInterfaces()) foreach (var fieldName in GetBindableFieldsNames(i)) yield return fieldName; Assertion.Require(rpcInterface.IsInterface, "Interface is expected"); foreach (var member in rpcInterface.GetMembers(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public)) { switch (member.MemberType) { case MemberTypes.Method: if (member is MethodInfo { IsSpecialName: false } method) yield return ProxyFieldName(method); break; case MemberTypes.Property: yield return MakeBackingFieldName(member.Name); break; } } } /// /// Loads the given argument on the stack /// private static void LoadArgument(ILGenerator ilgen, int nArg) { switch (nArg) { case 0: ilgen.Emit(OpCodes.Ldarg_0); return; case 1: ilgen.Emit(OpCodes.Ldarg_1); return; case 2: ilgen.Emit(OpCodes.Ldarg_2); return; case 3: ilgen.Emit(OpCodes.Ldarg_3); return; } if (nArg < 0x100) ilgen.Emit(OpCodes.Ldarg_S, (byte)nArg); else ilgen.Emit(OpCodes.Ldarg, (short)nArg); } private class TypeBuilderContext { public TypeBuilderContext(TypeBuilder builder) { Builder = builder; TimeoutFields = new (() => new Dictionary()); } public TypeBuilder Builder { get; } public Lazy> TimeoutFields { get; } public FieldInfo? DefaultTimeoutField { get; private set; } public FieldInfo DefineCustomRpcTimeout(RpcTimeoutAttribute timeouts, MethodInfo method) => DefineStaticTimeoutsField(method.Name + "_rpcTimeout", timeouts); public void SetDefaultTimeout(RpcTimeoutAttribute timeouts) { if (DefaultTimeoutField == null) { var field = DefineStaticTimeoutsField("default__rpcTimeout", timeouts); DefaultTimeoutField = field; } else ourLog.Warn($"Default timeout for RdRpc was defined more than once for {Builder.FullName}. The value on the nearest interface will be used."); } private FieldInfo DefineStaticTimeoutsField(string fieldName, RpcTimeoutAttribute timeouts) { var timeoutsField = Builder.DefineField(fieldName, typeof(RpcTimeouts), FieldAttributes.Private | FieldAttributes.Static); TimeoutFields.Value.Add(timeoutsField, timeouts.Timeout); return timeoutsField; } } } internal class ProxyGeneratorMembers { public readonly ConstructorInfo RdExtConstructor = typeof(RdExtAttribute) .GetConstructors() .Single(c => c.GetParameters().Length == 1) .NotNull(nameof(RdExtConstructor)); public readonly FieldInfo UnitInstance = typeof(Unit) .GetField(nameof(Unit.Instance)) .NotNull(nameof(UnitInstance)); // ReSharper disable once PossibleNullReferenceException public readonly MethodInfo EternalLifetimeGet = typeof(Lifetime) .GetProperty(nameof(Lifetime.Eternal), BindingFlags.Static | BindingFlags.Public) .GetGetMethod() .NotNull(nameof(EternalLifetimeGet)); public readonly MethodInfo SyncNested4 = typeof(ProxyGeneratorUtil) .GetMethods() .Single(m => m.Name == nameof(ProxyGeneratorUtil.SyncNested) && m.GetParameters().Length == 4) .NotNull(nameof(SyncNested4)); public MethodInfo ToTask = (typeof(ProxyGeneratorUtil)) .GetMethod(nameof(ProxyGeneratorUtil.ToTask)) .NotNull(nameof(ToTask)); public static readonly MethodInfo CreateRpcTimeoutMethod = typeof(ProxyGeneratorUtil) .GetMethod(nameof(ProxyGeneratorUtil.CreateRpcTimeouts)) .NotNull(); public static MethodInfo StartRdCall(Type rdCallType) { return rdCallType.GetMethods().Single(info => info.Name == nameof(IRdCall.Start) && info.GetParameters().Length == 3); } } }