From 5300be63c0ee1e73378028242ad7a0b86415fee8 Mon Sep 17 00:00:00 2001 From: Charles Stoner Date: Wed, 19 Jun 2019 01:26:05 -0700 Subject: [PATCH] Port DynamicMetaObjectProviderDebugView (dotnet/corefx#38656) * Port DynamicMetaObjectProviderDebugView * Remove System.Diagnostics.CodeAnalysis.SuppressMessage attributes * Add explicit private modifier Commit migrated from https://github.com/dotnet/corefx/commit/af04307b356e38e2b74df1f05f735ebc462850da --- src/libraries/Microsoft.CSharp/src/ILLinkTrim.xml | 8 + .../Microsoft.CSharp/src/Microsoft.CSharp.csproj | 1 + .../CSharp/RuntimeBinder/DynamicDebuggerProxy.cs | 464 +++++++++++++++++++++ .../Microsoft.CSharp/src/Resources/Strings.resx | 8 +- .../tests/DynamicDebuggerProxyTests.cs | 137 ++++++ .../tests/Microsoft.CSharp.Tests.csproj | 1 + 6 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 src/libraries/Microsoft.CSharp/src/ILLinkTrim.xml create mode 100644 src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/DynamicDebuggerProxy.cs create mode 100644 src/libraries/Microsoft.CSharp/tests/DynamicDebuggerProxyTests.cs diff --git a/src/libraries/Microsoft.CSharp/src/ILLinkTrim.xml b/src/libraries/Microsoft.CSharp/src/ILLinkTrim.xml new file mode 100644 index 0000000..a0422f8 --- /dev/null +++ b/src/libraries/Microsoft.CSharp/src/ILLinkTrim.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/libraries/Microsoft.CSharp/src/Microsoft.CSharp.csproj b/src/libraries/Microsoft.CSharp/src/Microsoft.CSharp.csproj index eed56f7..679e3ac 100644 --- a/src/libraries/Microsoft.CSharp/src/Microsoft.CSharp.csproj +++ b/src/libraries/Microsoft.CSharp/src/Microsoft.CSharp.csproj @@ -29,6 +29,7 @@ + diff --git a/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/DynamicDebuggerProxy.cs b/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/DynamicDebuggerProxy.cs new file mode 100644 index 0000000..6169d54 --- /dev/null +++ b/src/libraries/Microsoft.CSharp/src/Microsoft/CSharp/RuntimeBinder/DynamicDebuggerProxy.cs @@ -0,0 +1,464 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Dynamic; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; + +namespace Microsoft.CSharp.RuntimeBinder +{ + [Serializable] + [EditorBrowsable(EditorBrowsableState.Never)] + internal sealed class DynamicBindingFailedException : Exception + { + public DynamicBindingFailedException() + : base() + { + } + + private DynamicBindingFailedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + internal sealed class GetMemberValueBinder : GetMemberBinder + { + public GetMemberValueBinder(string name, bool ignoreCase) + : base(name, ignoreCase) + { + } + + public override DynamicMetaObject FallbackGetMember(DynamicMetaObject self, DynamicMetaObject onBindingError) + { + if (onBindingError == null) + { + var v = new List { self }; + var error = new DynamicMetaObject(System.Linq.Expressions.Expression.Throw( + System.Linq.Expressions.Expression.Constant(new DynamicBindingFailedException(), typeof(Exception)), typeof(object)), System.Dynamic.BindingRestrictions.Combine(v)); + return error; + } + return onBindingError; + } + } + + internal sealed class DynamicMetaObjectProviderDebugView + { + [System.Diagnostics.DebuggerDisplay("{value}", Name = "{name, nq}", Type = "{type, nq}")] + internal class DynamicProperty + { + [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] + private readonly string name; + + [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] + private readonly object value; + + [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] + private readonly string type; + + public DynamicProperty(string name, object value) + { + this.name = name; + this.value = value; + this.type = value == null ? "" : value.GetType().ToString(); + } + } + + [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] + private IList> results = null; + + [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] + private object obj; + + [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.RootHidden)] + internal DynamicProperty[] Items + { + get + { + if (results == null || results.Count == 0) + { + results = QueryDynamicObject(obj); + if (results == null || results.Count == 0) + { + throw new DynamicDebugViewEmptyException(); + } + } + DynamicProperty[] pairArray = new DynamicProperty[results.Count]; + for (int i = 0; i < results.Count; i++) + { + pairArray[i] = new DynamicProperty(results[i].Key, results[i].Value); + } + return pairArray; + } + } + + public DynamicMetaObjectProviderDebugView(object arg) + { + this.obj = arg; + } + + [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)] + private static readonly ParameterExpression parameter = Expression.Parameter(typeof(object), "debug"); + + public static object TryEvalBinaryOperators( + T1 arg1, + T2 arg2, + CSharpArgumentInfoFlags arg1Flags, + CSharpArgumentInfoFlags arg2Flags, + ExpressionType opKind, + Type accessibilityContext) + { + CSharpArgumentInfo arg1Info = CSharpArgumentInfo.Create(arg1Flags, null); + CSharpArgumentInfo arg2Info = CSharpArgumentInfo.Create(arg2Flags, null); + + CSharpBinaryOperationBinder binder = new CSharpBinaryOperationBinder( + opKind, + false, // isChecked + CSharpBinaryOperationFlags.None, + accessibilityContext, + new CSharpArgumentInfo[] { arg1Info, arg2Info }); + + var site = CallSite>.Create(binder); + return site.Target(site, arg1, arg2); + } + + public static object TryEvalUnaryOperators(T obj, ExpressionType oper, Type accessibilityContext) + { + if (oper == ExpressionType.IsTrue || oper == ExpressionType.IsFalse) + { + var trueFalseSite = CallSite> + .Create(new Microsoft.CSharp.RuntimeBinder.CSharpUnaryOperationBinder(oper, + false, + accessibilityContext, + new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) })); + return trueFalseSite.Target(trueFalseSite, obj); + } + + var site = CallSite> + .Create(new Microsoft.CSharp.RuntimeBinder.CSharpUnaryOperationBinder(oper, + false, + accessibilityContext, + new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) })); + return site.Target(site, obj); + } + + public static K TryEvalCast(T obj, Type type, CSharpBinderFlags kind, Type accessibilityContext) + { + var site = CallSite>.Create(Binder.Convert(kind, type, accessibilityContext)); + return site.Target(site, obj); + } + + /// + /// Creates array of types that describes delegate's signature and array of + /// CSharpArgumentInfoFlags that describe each of the arguments. + /// + private static void CreateDelegateSignatureAndArgumentInfos( + object[] args, + Type[] argTypes, + CSharpArgumentInfoFlags[] argFlags, + out Type[] delegateSignatureTypes, + out CSharpArgumentInfo[] argInfos) + { + int numberOfArguments = args.Length; + Debug.Assert((numberOfArguments == argTypes.Length) && (numberOfArguments == argFlags.Length), "Argument arrays size mismatch."); + + delegateSignatureTypes = new Type[numberOfArguments + 2]; + delegateSignatureTypes[0] = typeof(CallSite); + + argInfos = new CSharpArgumentInfo[numberOfArguments]; + + for (int i = 0; i < numberOfArguments; i++) + { + if (argTypes[i] != null) + { + delegateSignatureTypes[i + 1] = argTypes[i]; + } + else if (args[i] != null) + { + delegateSignatureTypes[i + 1] = args[i].GetType(); + } + else + { + delegateSignatureTypes[i + 1] = typeof(object); + } + + argInfos[i] = CSharpArgumentInfo.Create(argFlags[i], null); + } + + delegateSignatureTypes[numberOfArguments + 1] = typeof(object); // type of return value + } + + /// + /// Creates a delegate based on type array that describe its signature and invokes it. + /// + /// Result of invoking the delegate. + private static object CreateDelegateAndInvoke(Type[] delegateSignatureTypes, CallSiteBinder binder, object[] args) + { + Type delegateType = Expression.GetDelegateType(delegateSignatureTypes); + var site = CallSite.Create(delegateType, binder); + + Delegate target = (Delegate)site.GetType().GetField("Target").GetValue(site); + + object[] argsWithSite = new object[args.Length + 1]; + argsWithSite[0] = site; + args.CopyTo(argsWithSite, 1); + + object result = target.DynamicInvoke(argsWithSite); + return result; + } + + /// + /// DynamicOperatorRewriter in EE generates call to this method to dynamically invoke a method. + /// + /// Array that contains method arguments. The first element is an object on which method should be called. + /// Type of each argument in methodArgs. + /// Flags describing each argument. + /// Name of a method to invoke. + /// Type that determines context in which method should be called. + /// Generic type arguments if there are any. + /// Result of method invocation. + public static object TryEvalMethodVarArgs( + object[] methodArgs, + Type[] argTypes, + CSharpArgumentInfoFlags[] argFlags, + string methodName, + Type accessibilityContext, + Type[] typeArguments) + { + Type[] delegateSignatureTypes = null; + CSharpArgumentInfo[] argInfos = null; + + CreateDelegateSignatureAndArgumentInfos( + methodArgs, + argTypes, + argFlags, + out delegateSignatureTypes, + out argInfos); + + CallSiteBinder binder; + if (String.IsNullOrEmpty(methodName)) + { + //null or empty indicates delegate invocation. + binder = new CSharpInvokeBinder( + CSharpCallFlags.ResultDiscarded, + accessibilityContext, + argInfos); + } + else + { + binder = new CSharpInvokeMemberBinder( + CSharpCallFlags.ResultDiscarded, + methodName, + accessibilityContext, + typeArguments, + argInfos); + } + + return CreateDelegateAndInvoke(delegateSignatureTypes, binder, methodArgs); + } + + /// + /// DynamicOperatorRewriter in EE generates call to this method to dynamically invoke a property getter + /// with no arguments. + /// + /// Type of object on which property is defined. + /// Object on which property is defined. + /// Name of a property to invoke. + /// Type that determines context in which method should be called. + /// Determines if COM binder should return a callable object. + /// Result of property invocation. + public static object TryGetMemberValue(T obj, string propName, Type accessibilityContext, bool isResultIndexed) + { + // In most cases it's ok to use CSharpArgumentInfoFlags.None since target of property call is dynamic. + // The only possible case when target is not dynamic but we still treat is as dynamic access is when + // one of arguments is dynamic. This is only possible for indexed properties since we call this method and + // TryGetMemberValueVarArgs afterwards. + + CSharpGetMemberBinder binder = new CSharpGetMemberBinder( + propName, + isResultIndexed, + accessibilityContext, + new CSharpArgumentInfo[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }); + + var site = CallSite>.Create(binder); + return site.Target(site, obj); + } + + /// + /// DynamicOperatorRewriter in EE generates call to this method to dynamically invoke a property/indexer getter. + /// + /// Array that contains property arguments. The first element is an object on + /// which indexer should be called or call to TryGetMemberValue that selects the right property in case of + /// indexed properties. + /// Type of each argument in propArgs. + /// Flags describing each argument. + /// Type that determines context in which method should be called. + /// Result of property invocation. + public static object TryGetMemberValueVarArgs( + object[] propArgs, + Type[] argTypes, + CSharpArgumentInfoFlags[] argFlags, + Type accessibilityContext) + { + Type[] delegateSignatureTypes = null; + CSharpArgumentInfo[] argInfos = null; + + CreateDelegateSignatureAndArgumentInfos( + propArgs, + argTypes, + argFlags, + out delegateSignatureTypes, + out argInfos); + + CallSiteBinder binder = new CSharpGetIndexBinder(accessibilityContext, argInfos); + + return CreateDelegateAndInvoke(delegateSignatureTypes, binder, propArgs); + } + + /// + /// DynamicOperatorRewriter in EE generates call to this method to dynamically invoke a property setter + /// with no arguments. + /// + /// Type of object on which property is defined. + /// Type of value property needs to be set to. + /// Object on which property is defined. + /// Name of a property to invoke. + /// Value property needs to be set to. + /// + /// Type that determines context in which method should be called. + /// Result of property invocation. + public static object TrySetMemberValue( + TObject obj, + string propName, + TValue value, + CSharpArgumentInfoFlags valueFlags, + Type accessibilityContext) + { + CSharpArgumentInfo targetArgInfo = CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null); + CSharpArgumentInfo valueArgInfo = CSharpArgumentInfo.Create(valueFlags, null); + + CSharpSetMemberBinder binder = new CSharpSetMemberBinder( + propName, + false, // isCompoundAssignment + false, // isChecked + accessibilityContext, + new CSharpArgumentInfo[] { targetArgInfo, valueArgInfo }); + + var site = CallSite>.Create(binder); + return site.Target(site, obj, value); + } + + /// + /// DynamicOperatorRewriter in EE generates call to this method to dynamically invoke a property/indexer setter. + /// + /// Array that contains property arguments. The first element is an object on + /// which indexer should be called or call to TrySetMemberValue that selects the right property in case of + /// indexed properties. The last argument is value that property should be set to. + /// Type of each argument in propArgs. + /// Flags describing each argument. + /// Type that determines context in which method should be called. + /// Result of property invocation. + public static object TrySetMemberValueVarArgs( + object[] propArgs, + Type[] argTypes, + CSharpArgumentInfoFlags[] argFlags, + Type accessibilityContext) + { + Type[] delegateSignatureTypes = null; + CSharpArgumentInfo[] argInfos = null; + + CreateDelegateSignatureAndArgumentInfos( + propArgs, + argTypes, + argFlags, + out delegateSignatureTypes, + out argInfos); + + CallSiteBinder binder = new CSharpSetIndexBinder(/*isCompoundAssignment */ false, /* isChecked */ false, accessibilityContext, argInfos); + + return CreateDelegateAndInvoke(delegateSignatureTypes, binder, propArgs); + } + + //Called when we don't know if the member is a property or a method + internal static object TryGetMemberValue(object obj, string name, bool ignoreException) + { + // if you want to ignore case for VB, this is how you set it .. make it a member and add a ctor to init it + bool ignoreCase = false; + object value = null; + + var site = CallSite>.Create(new GetMemberValueBinder(name, ignoreCase)); + + try + { + value = site.Target(site, obj); + } + catch (DynamicBindingFailedException exp) + { + if (ignoreException) + value = null; + else + throw exp; + } + catch (MissingMemberException exp) + { + if (ignoreException) + value = SR.GetValueonWriteOnlyProperty; + else + throw exp; + } + return value; + } + + private static IList> QueryDynamicObject(object obj) + { + IDynamicMetaObjectProvider ido = obj as IDynamicMetaObjectProvider; + if (ido != null) + { + DynamicMetaObject mo = ido.GetMetaObject(parameter); + List names = new List(mo.GetDynamicMemberNames()); + names.Sort(); + if (names != null) + { + var result = new List>(); + foreach (string name in names) + { + object value; + if ((value = TryGetMemberValue(obj, name, true)) != null) + { + result.Add(new KeyValuePair(name, value)); + } + } + return result; + } + } + return new KeyValuePair[0]; + } + + [Serializable] + internal class DynamicDebugViewEmptyException : Exception + { + public DynamicDebugViewEmptyException() + { + } + + protected DynamicDebugViewEmptyException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + public string Empty + { + get + { + return SR.EmptyDynamicView; + } + } + } + } +} diff --git a/src/libraries/Microsoft.CSharp/src/Resources/Strings.resx b/src/libraries/Microsoft.CSharp/src/Resources/Strings.resx index 851a849..ecde81c 100644 --- a/src/libraries/Microsoft.CSharp/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.CSharp/src/Resources/Strings.resx @@ -137,6 +137,12 @@ Cannot implicitly convert type 'void' to 'object' + + No further information on this object could be discovered + + + Write Only properties are not supported + Operator '{0}' cannot be applied to operands of type '{1}' and '{2}' @@ -368,4 +374,4 @@ variable - \ No newline at end of file + diff --git a/src/libraries/Microsoft.CSharp/tests/DynamicDebuggerProxyTests.cs b/src/libraries/Microsoft.CSharp/tests/DynamicDebuggerProxyTests.cs new file mode 100644 index 0000000..94359a9 --- /dev/null +++ b/src/libraries/Microsoft.CSharp/tests/DynamicDebuggerProxyTests.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Reflection; +using Xunit; + +namespace Microsoft.CSharp.RuntimeBinder.Tests +{ + public class DynamicDebuggerProxyTests + { + private static readonly Type _debugViewType = GetType("DynamicMetaObjectProviderDebugView"); + + [Fact] + public void Items_Empty() + { + var obj = new DynamicTestObject(new Dictionary()); + var debugView = CreateDebugView(obj); + + var exceptionType = GetType("DynamicMetaObjectProviderDebugView+DynamicDebugViewEmptyException"); + var exception = Assert.Throws(exceptionType, () => GetItems(debugView)); + + // Get resource string. + var itemValue = (string)exceptionType.GetMethod("get_Empty", BindingFlags.Public | BindingFlags.Instance).Invoke(exception, new object[0]); + Assert.NotNull(itemValue); + } + + [Fact] + public void Items() + { + var obj = new DynamicTestObject(new Dictionary() { { "F", 1 }, { "G", "" } }); + var debugView = CreateDebugView(obj); + var items = GetItems(debugView); + Assert.Equal(2, items.Length); + } + + [Fact] + public void TryGetMemberValue() + { + var obj = new DynamicTestObject(new Dictionary() { { "F", 1 } }); + Assert.Equal(1, TryGetMemberValue(obj, "F", ignoreException: false)); + } + + [Fact] + public void TryGetMemberValue_BindingFailed() + { + var obj = new DynamicTestObject(new Dictionary()); + + // ignoreException: true + Assert.Null(TryGetMemberValue(obj, "F", ignoreException: true)); + + // ignoreException: false + var exceptionType = GetType("DynamicBindingFailedException"); + Assert.Throws(exceptionType, () => GetItems(TryGetMemberValue(obj, "F", ignoreException: false))); + } + + [Fact] + public void TryGetMemberValue_MissingMember() + { + var obj = new DynamicTestObject(new Dictionary(), throwIfMissing: true); + + // ignoreException: true + string message = (string)TryGetMemberValue(obj, "F", ignoreException: true); + Assert.NotNull(message); + + // ignoreException: false + Assert.Throws(() => GetItems(TryGetMemberValue(obj, "F", ignoreException: false))); + } + + private static Type GetType(string typeName) + { + return typeof(Binder).Assembly.GetType("Microsoft.CSharp.RuntimeBinder." + typeName, throwOnError: true); + } + + private static object CreateDebugView(object arg) + { + return Activator.CreateInstance(_debugViewType, new object[] { arg }); + } + + private static object[] GetItems(object debugView) + { + var method = _debugViewType.GetMethod("get_Items", BindingFlags.NonPublic | BindingFlags.Instance); + return (object[])InvokeAndUnwrapException(method, debugView, new object[0]); + } + + private static object TryGetMemberValue(object obj, string name, bool ignoreException) + { + var method = _debugViewType.GetMethod("TryGetMemberValue", BindingFlags.NonPublic | BindingFlags.Static); + return InvokeAndUnwrapException(method, null, new object[] { obj, name, ignoreException }); + } + + private static object InvokeAndUnwrapException(MethodInfo method, object obj, object[] args) + { + try + { + return method.Invoke(obj, args); + } + catch (TargetInvocationException e) + { + throw e.InnerException; + } + } + + private sealed class DynamicTestObject : DynamicObject + { + private readonly Dictionary _members; + private readonly bool _throwIfMissing; + + internal DynamicTestObject(Dictionary members, bool throwIfMissing = false) + { + _members = members; + _throwIfMissing = throwIfMissing; + } + + public override IEnumerable GetDynamicMemberNames() + { + return _members.Keys; + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + if (_members.TryGetValue(binder.Name, out result)) + { + return true; + } + if (_throwIfMissing) + { + throw new MissingMemberException(); + } + return false; + } + } + } +} diff --git a/src/libraries/Microsoft.CSharp/tests/Microsoft.CSharp.Tests.csproj b/src/libraries/Microsoft.CSharp/tests/Microsoft.CSharp.Tests.csproj index 48b12b6..43e5d2f 100644 --- a/src/libraries/Microsoft.CSharp/tests/Microsoft.CSharp.Tests.csproj +++ b/src/libraries/Microsoft.CSharp/tests/Microsoft.CSharp.Tests.csproj @@ -14,6 +14,7 @@ + -- 2.7.4