From 3c801a8a3b29eabc29ae88e1961c772fda17a1d2 Mon Sep 17 00:00:00 2001 From: Charles Stoner Date: Mon, 16 Nov 2020 10:01:30 -0800 Subject: [PATCH] Support System.Type.Missing arguments for late bound calls to IDynamicObject methods with 8 or more arguments (#44434) --- .../src/ILLink/ILLink.Suppressions.xml | 8 +- .../src/Microsoft.VisualBasic.Core.vbproj | 4 + .../VisualBasic/CompilerServices/CacheDict.vb | 69 ++++++++++++ .../VisualBasic/CompilerServices/IDOBinder.vb | 125 +++++++++++---------- .../tests/Microsoft.VisualBasic.Core.Tests.csproj | 1 + .../tests/NewLateBindingTests.cs | 116 +++++++++++++++++++ 6 files changed, 265 insertions(+), 58 deletions(-) create mode 100644 src/libraries/Microsoft.VisualBasic.Core/src/Microsoft/VisualBasic/CompilerServices/CacheDict.vb create mode 100644 src/libraries/Microsoft.VisualBasic.Core/tests/NewLateBindingTests.cs diff --git a/src/libraries/Microsoft.VisualBasic.Core/src/ILLink/ILLink.Suppressions.xml b/src/libraries/Microsoft.VisualBasic.Core/src/ILLink/ILLink.Suppressions.xml index cf74061..2092add 100644 --- a/src/libraries/Microsoft.VisualBasic.Core/src/ILLink/ILLink.Suppressions.xml +++ b/src/libraries/Microsoft.VisualBasic.Core/src/ILLink/ILLink.Suppressions.xml @@ -113,6 +113,12 @@ ILLink IL2075 member + M:Microsoft.VisualBasic.CompilerServices.IDOUtils.CreateInvoker(System.Int32) + + + ILLink + IL2075 + member M:Microsoft.VisualBasic.CompilerServices.LateBinding.InternalLateSet(System.Object,System.Type@,System.String,System.Object[],System.String[],System.Boolean,Microsoft.VisualBasic.CallType) @@ -158,4 +164,4 @@ M:Microsoft.VisualBasic.CompilerServices.Symbols.Container.LookupNamedMembers(System.String) - \ No newline at end of file + diff --git a/src/libraries/Microsoft.VisualBasic.Core/src/Microsoft.VisualBasic.Core.vbproj b/src/libraries/Microsoft.VisualBasic.Core/src/Microsoft.VisualBasic.Core.vbproj index 0243aff..98b5806 100644 --- a/src/libraries/Microsoft.VisualBasic.Core/src/Microsoft.VisualBasic.Core.vbproj +++ b/src/libraries/Microsoft.VisualBasic.Core/src/Microsoft.VisualBasic.Core.vbproj @@ -30,6 +30,7 @@ + @@ -112,6 +113,9 @@ + + + diff --git a/src/libraries/Microsoft.VisualBasic.Core/src/Microsoft/VisualBasic/CompilerServices/CacheDict.vb b/src/libraries/Microsoft.VisualBasic.Core/src/Microsoft/VisualBasic/CompilerServices/CacheDict.vb new file mode 100644 index 0000000..390bb13 --- /dev/null +++ b/src/libraries/Microsoft.VisualBasic.Core/src/Microsoft/VisualBasic/CompilerServices/CacheDict.vb @@ -0,0 +1,69 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. + +Imports System.Collections.Generic +Imports System.Runtime.InteropServices + +Namespace Microsoft.VisualBasic.CompilerServices + + ' Implements a MRU collection for caching dynamic methods used in IDO late binding. + Friend Class CacheDict(Of TKey, TValue) + ' The Dictionary to quickly access cached data + Private ReadOnly _dict As Dictionary(Of TKey, KeyInfo) + ' MRU sorted linked list + Private ReadOnly _list As LinkedList(Of TKey) + ' Maximum size + Private ReadOnly _maxSize As Integer + + Friend Sub New(ByVal maxSize As Integer) + _dict = New Dictionary(Of TKey, KeyInfo) + _list = New LinkedList(Of TKey) + _maxSize = maxSize + End Sub + + Friend Sub Add(ByVal key As TKey, ByVal value As TValue) + Dim info As New KeyInfo + If _dict.TryGetValue(key, info) Then + ' If the key is already in the collection, remove it + _list.Remove(info.List) + ElseIf (_list.Count = _maxSize) Then + ' Age out the last element if we hit the max size + Dim last As LinkedListNode(Of TKey) = _list.Last + _list.RemoveLast() + _dict.Remove(last.Value) + End If + + ' Add the new element + Dim node As New LinkedListNode(Of TKey)(key) + _list.AddFirst(node) + _dict.Item(key) = New KeyInfo(value, node) + End Sub + + Friend Function TryGetValue(ByVal key As TKey, ByRef value As TValue) As Boolean + Dim info As New KeyInfo + If _dict.TryGetValue(key, info) Then + Dim list As LinkedListNode(Of TKey) = info.List + If (list.Previous IsNot Nothing) Then + _list.Remove(list) + _list.AddFirst(list) + End If + value = info.Value + Return True + End If + value = Nothing + Return False + End Function + + ' KeyInfo to store in the dictionary + Private Structure KeyInfo + Friend ReadOnly Value As TValue + Friend ReadOnly List As LinkedListNode(Of TKey) + + Friend Sub New(ByVal v As TValue, ByVal l As LinkedListNode(Of TKey)) + Value = v + List = l + End Sub + End Structure + End Class + +End Namespace diff --git a/src/libraries/Microsoft.VisualBasic.Core/src/Microsoft/VisualBasic/CompilerServices/IDOBinder.vb b/src/libraries/Microsoft.VisualBasic.Core/src/Microsoft/VisualBasic/CompilerServices/IDOBinder.vb index fc5a085..abf5300 100644 --- a/src/libraries/Microsoft.VisualBasic.Core/src/Microsoft/VisualBasic/CompilerServices/IDOBinder.vb +++ b/src/libraries/Microsoft.VisualBasic.Core/src/Microsoft/VisualBasic/CompilerServices/IDOBinder.vb @@ -6,6 +6,7 @@ Imports System.Collections.Generic Imports System.Dynamic Imports System.Linq.Expressions Imports System.Reflection +Imports System.Reflection.Emit Imports System.Runtime.CompilerServices Imports Microsoft.VisualBasic.CompilerServices.NewLateBinding @@ -1047,15 +1048,6 @@ Namespace Microsoft.VisualBasic.CompilerServices End Function End Class - Public Delegate Function SiteDelegate0(ByVal site As CallSite, ByVal instance As Object) As Object - Public Delegate Function SiteDelegate1(ByVal site As CallSite, ByVal instance As Object, ByRef arg0 As Object) As Object - Public Delegate Function SiteDelegate2(ByVal site As CallSite, ByVal instance As Object, ByRef arg0 As Object, ByRef arg1 As Object) As Object - Public Delegate Function SiteDelegate3(ByVal site As CallSite, ByVal instance As Object, ByRef arg0 As Object, ByRef arg1 As Object, ByRef arg2 As Object) As Object - Public Delegate Function SiteDelegate4(ByVal site As CallSite, ByVal instance As Object, ByRef arg0 As Object, ByRef arg1 As Object, ByRef arg2 As Object, ByRef arg3 As Object) As Object - Public Delegate Function SiteDelegate5(ByVal site As CallSite, ByVal instance As Object, ByRef arg0 As Object, ByRef arg1 As Object, ByRef arg2 As Object, ByRef arg3 As Object, ByRef arg4 As Object) As Object - Public Delegate Function SiteDelegate6(ByVal site As CallSite, ByVal instance As Object, ByRef arg0 As Object, ByRef arg1 As Object, ByRef arg2 As Object, ByRef arg3 As Object, ByRef arg4 As Object, ByRef arg5 As Object) As Object - Public Delegate Function SiteDelegate7(ByVal site As CallSite, ByVal instance As Object, ByRef arg0 As Object, ByRef arg1 As Object, ByRef arg2 As Object, ByRef arg3 As Object, ByRef arg4 As Object, ByRef arg5 As Object, ByRef arg6 As Object) As Object - Friend Class IDOUtils Private Sub New() @@ -1278,6 +1270,9 @@ Namespace Microsoft.VisualBasic.CompilerServices Return If(valueExpression.Type.Equals(GetType(Object)), valueExpression, Expression.Convert(valueExpression, GetType(Object))) End Function + ' MRU Dictionary of invoker delegates. We keep 16 most recently used ones, rest is GC'd + Private Shared Invokers As New CacheDict(Of Integer, Func(Of CallSiteBinder, Object, Object(), Object))(16) + Public Shared Function CreateRefCallSiteAndInvoke( ByVal action As CallSiteBinder, ByVal instance As Object, @@ -1285,55 +1280,71 @@ Namespace Microsoft.VisualBasic.CompilerServices action = GetCachedBinder(action) - Select Case arguments.Length - Case 0 - Dim c As CallSite(Of SiteDelegate0) = CallSite(Of SiteDelegate0).Create(action) - Return c.Target.Invoke(c, instance) - Case 1 - Dim c As CallSite(Of SiteDelegate1) = CallSite(Of SiteDelegate1).Create(action) - Return c.Target.Invoke(c, instance, arguments(0)) - Case 2 - Dim c As CallSite(Of SiteDelegate2) = CallSite(Of SiteDelegate2).Create(action) - Return c.Target.Invoke(c, instance, arguments(0), arguments(1)) - Case 3 - Dim c As CallSite(Of SiteDelegate3) = CallSite(Of SiteDelegate3).Create(action) - Return c.Target.Invoke(c, instance, arguments(0), arguments(1), arguments(2)) - Case 4 - Dim c As CallSite(Of SiteDelegate4) = CallSite(Of SiteDelegate4).Create(action) - Return c.Target.Invoke(c, instance, arguments(0), arguments(1), arguments(2), arguments(3)) - Case 5 - Dim c As CallSite(Of SiteDelegate5) = CallSite(Of SiteDelegate5).Create(action) - Return c.Target.Invoke(c, instance, arguments(0), arguments(1), arguments(2), arguments(3), arguments(4)) - Case 6 - Dim c As CallSite(Of SiteDelegate6) = CallSite(Of SiteDelegate6).Create(action) - Return c.Target.Invoke(c, instance, arguments(0), arguments(1), arguments(2), arguments(3), arguments(4), arguments(5)) - Case 7 - Dim c As CallSite(Of SiteDelegate7) = CallSite(Of SiteDelegate7).Create(action) - Return c.Target.Invoke(c, instance, arguments(0), arguments(1), arguments(2), arguments(3), arguments(4), arguments(5), arguments(6)) - Case Else - Dim signature(arguments.Length + 2) As Type - Dim refObject As Type = GetType(Object).MakeByRefType() - signature(0) = GetType(CallSite) ' First argument is a call site - signature(1) = GetType(Object) ' Second is the instance (ByVal) - signature(signature.Length - 1) = GetType(Object) ' Last type is the return type - For i As Integer = 2 To signature.Length - 2 ' All arguments are ByRef - signature(i) = refObject - Next + Dim Invoker As Func(Of CallSiteBinder, Object, Object(), Object) = Nothing - Dim c As CallSite = CallSite.Create(Expression.GetDelegateType(signature), action) - Dim args(arguments.Length + 1) As Object - args(0) = c - args(1) = instance - arguments.CopyTo(args, 2) - Dim siteTarget As System.Delegate = DirectCast(c.GetType().GetField("Target").GetValue(c), System.Delegate) - Try - Dim result As Object = siteTarget.DynamicInvoke(args) - Array.Copy(args, 2, arguments, 0, arguments.Length) - Return result - Catch ie As TargetInvocationException - Throw ie.InnerException - End Try - End Select + SyncLock Invokers + If Not Invokers.TryGetValue(arguments.Length, Invoker) Then + Invoker = CreateInvoker(arguments.Length) + Invokers.Add(arguments.Length, Invoker) + End If + End SyncLock + + Return Invoker.Invoke(action, instance, arguments) + End Function + + ''' Creates an invoker, a function such as: + ''' + ''' Delegate Function InvokerDelegate3(ByVal site As CallSite, ByVal instance As Object, ByRef arg0 As Object, ByRef arg1 As Object, ByRef arg2 As Object) As Object + ''' + ''' Function Invoker3(action as CallSiteBinder, instance as Object, args as Object()) as Object + ''' Dim site as CallSite(Of InvokerDelegate3) + ''' site = CallSite(Of Func(Of InvokerDelegate3).Create(action) + ''' ' args(0), args(1) and args(2) are passed ByRef + ''' return site.Target.Invoke(site, instance, args(0), args(1), args(2)) + ''' End Function + Private Shared Function CreateInvoker(ByVal ArgLength As Integer) As Func(Of CallSiteBinder, Object, Object(), Object) + ' Useful Types + Dim ObjectType As Type = GetType(Object) + Dim ObjectRefType As Type = ObjectType.MakeByRefType() + Dim CallSiteBinderType As Type = GetType(CallSiteBinder) + + ' Call Site Delegate Signature + Dim CallSiteSignature(ArgLength + 2) As Type + CallSiteSignature(0) = GetType(CallSite) ' CallSite must go first + CallSiteSignature(1) = ObjectType ' Instance: Object + For i As Integer = 2 To CallSiteSignature.Length - 2 ' Arguments: Object& + CallSiteSignature(i) = ObjectRefType + Next + CallSiteSignature(CallSiteSignature.Length - 1) = ObjectType ' Result: Object + + ' Call Site Delegate + Dim CallSiteDelegate As Type = Expression.GetDelegateType(CallSiteSignature) + Dim CallSiteType As Type = GetType(CallSite(Of )).MakeGenericType(CallSiteDelegate) + + ' Invoker(CallSiteBinder, Instance as Object, Args as Object()) + Dim InvokerMethod As New DynamicMethod("Invoker", ObjectType, {CallSiteBinderType, ObjectType, GetType(Object())}, True) + + ' Dim cs as CallSite(Of delegateType) = CallSite(Of delegateType).Create(Action) + Dim il As ILGenerator = InvokerMethod.GetILGenerator() + Dim site As LocalBuilder = il.DeclareLocal(CallSiteType) + il.Emit(OpCodes.Ldarg_0) + il.Emit(OpCodes.Call, CallSiteType.GetMethod("Create", {CallSiteBinderType})) + il.Emit(OpCodes.Stloc, site) + + ' return site.Target.Invoke(site, Instance, ref args(0), ref args(1), ...) + il.Emit(OpCodes.Ldloc, site) + il.Emit(OpCodes.Ldfld, CallSiteType.GetField("Target")) + il.Emit(OpCodes.Ldloc, site) + il.Emit(OpCodes.Ldarg_1) 'Instance + For i As Integer = 0 To ArgLength - 1 + il.Emit(OpCodes.Ldarg_2) + il.Emit(OpCodes.Ldc_I4, i) + il.Emit(OpCodes.Ldelema, ObjectType) ' ref arg(i) + Next + il.Emit(OpCodes.Callvirt, CallSiteDelegate.GetMethod("Invoke")) + il.Emit(OpCodes.Ret) + + Return DirectCast(InvokerMethod.CreateDelegate(GetType(Func(Of CallSiteBinder, Object, Object(), Object))), Func(Of CallSiteBinder, Object, Object(), Object)) End Function Public Shared Function CreateFuncCallSiteAndInvoke( diff --git a/src/libraries/Microsoft.VisualBasic.Core/tests/Microsoft.VisualBasic.Core.Tests.csproj b/src/libraries/Microsoft.VisualBasic.Core/tests/Microsoft.VisualBasic.Core.Tests.csproj index 74c7f14..aa7d113 100644 --- a/src/libraries/Microsoft.VisualBasic.Core/tests/Microsoft.VisualBasic.Core.Tests.csproj +++ b/src/libraries/Microsoft.VisualBasic.Core/tests/Microsoft.VisualBasic.Core.Tests.csproj @@ -39,6 +39,7 @@ + diff --git a/src/libraries/Microsoft.VisualBasic.Core/tests/NewLateBindingTests.cs b/src/libraries/Microsoft.VisualBasic.Core/tests/NewLateBindingTests.cs new file mode 100644 index 0000000..d8efbe6 --- /dev/null +++ b/src/libraries/Microsoft.VisualBasic.Core/tests/NewLateBindingTests.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Dynamic; +using Xunit; + +namespace Microsoft.VisualBasic.CompilerServices.Tests +{ + public class NewLateBindingTests + { + private sealed class OptionalValuesType : DynamicObject + { + public object F1(T p1 = default) + { + return $"{typeof(T)}, {ToString(p1)}"; + } + public object F2(T p1 = default, int? p2 = 2) + { + return $"{typeof(T)}, {ToString(p1)}, {ToString(p2)}"; + } + public object F3(object p1, T p2 = default, int? p3 = 3) + { + return $"{typeof(T)}, {ToString(p2)}, {ToString(p3)}"; + } + public object F4(object p1, object p2, T p3 = default, int? p4 = 4) + { + return $"{typeof(T)}, {ToString(p3)}, {ToString(p4)}"; + } + public object F5(object p1, object p2, object p3, T p4 = default, int? p5 = 5) + { + return $"{typeof(T)}, {ToString(p4)}, {ToString(p5)}"; + } + public object F6(object p1, object p2, object p3, object p4, T p5 = default, int? p6 = 6) + { + return $"{typeof(T)}, {ToString(p5)}, {ToString(p6)}"; + } + public object F7(object p1, object p2, object p3, object p4, object p5, T p6 = default, int? p7 = 7) + { + return $"{typeof(T)}, {ToString(p6)}, {ToString(p7)}"; + } + public object F8(object p1, object p2, object p3, object p4, object p5, object p6, T p7 = default, int? p8 = 8) + { + return $"{typeof(T)}, {ToString(p7)}, {ToString(p8)}"; + } + private static string ToString(object obj) => obj?.ToString() ?? "null"; + } + + public static IEnumerable LateCall_OptionalValues_Data() + { + // If System.Type.Missing is used for a parameter with type parameter type, + // System.Reflection.Missing is used in type inference. This matches .NET Framework behavior. + + yield return CreateData("F1", new object[] { -1 }, null, "System.Int32, -1"); + yield return CreateData("F1", new object[] { Type.Missing }, null, "System.Reflection.Missing, null"); + yield return CreateData("F1", new object[] { Type.Missing }, new[] { typeof(int) }, "System.Int32, 0"); + + yield return CreateData("F2", new object[] { 1, -1 }, null, "System.Int32, 1, -1"); + yield return CreateData("F2", new object[] { 1, Type.Missing }, null, "System.Int32, 1, 2"); + yield return CreateData("F2", new object[] { Type.Missing, Type.Missing }, null, "System.Reflection.Missing, null, 2"); + yield return CreateData("F2", new object[] { Type.Missing, Type.Missing }, new[] { typeof(int) }, "System.Int32, 0, 2"); + + yield return CreateData("F3", new object[] { 1, 2, -1 }, null, "System.Int32, 2, -1"); + yield return CreateData("F3", new object[] { 1, 2, Type.Missing }, null, "System.Int32, 2, 3"); + yield return CreateData("F3", new object[] { 1, Type.Missing, Type.Missing }, null, "System.Reflection.Missing, null, 3"); + yield return CreateData("F3", new object[] { 1, Type.Missing, Type.Missing }, new[] { typeof(int) }, "System.Int32, 0, 3"); + + yield return CreateData("F4", new object[] { 1, 2, 3, -1 }, null, "System.Int32, 3, -1"); + yield return CreateData("F4", new object[] { 1, 2, 3, Type.Missing }, null, "System.Int32, 3, 4"); + yield return CreateData("F4", new object[] { 1, 2, Type.Missing, Type.Missing }, null, "System.Reflection.Missing, null, 4"); + yield return CreateData("F4", new object[] { 1, 2, Type.Missing, Type.Missing }, new[] { typeof(int) }, "System.Int32, 0, 4"); + + yield return CreateData("F5", new object[] { 1, 2, 3, 4, -1 }, null, "System.Int32, 4, -1"); + yield return CreateData("F5", new object[] { 1, 2, 3, 4, Type.Missing }, null, "System.Int32, 4, 5"); + yield return CreateData("F5", new object[] { 1, 2, 3, Type.Missing, Type.Missing }, null, "System.Reflection.Missing, null, 5"); + yield return CreateData("F5", new object[] { 1, 2, 3, Type.Missing, Type.Missing }, new[] { typeof(int) }, "System.Int32, 0, 5"); + + yield return CreateData("F6", new object[] { 1, 2, 3, 4, 5, -1 }, null, "System.Int32, 5, -1"); + yield return CreateData("F6", new object[] { 1, 2, 3, 4, 5, Type.Missing }, null, "System.Int32, 5, 6"); + yield return CreateData("F6", new object[] { 1, 2, 3, 4, Type.Missing, Type.Missing }, null, "System.Reflection.Missing, null, 6"); + yield return CreateData("F6", new object[] { 1, 2, 3, 4, Type.Missing, Type.Missing }, new[] { typeof(int) }, "System.Int32, 0, 6"); + + yield return CreateData("F7", new object[] { 1, 2, 3, 4, 5, 6, -1 }, null, "System.Int32, 6, -1"); + yield return CreateData("F7", new object[] { 1, 2, 3, 4, 5, 6, Type.Missing }, null, "System.Int32, 6, 7"); + yield return CreateData("F7", new object[] { 1, 2, 3, 4, 5, Type.Missing, Type.Missing }, null, "System.Reflection.Missing, null, 7"); + yield return CreateData("F7", new object[] { 1, 2, 3, 4, 5, Type.Missing, Type.Missing }, new[] { typeof(int) }, "System.Int32, 0, 7"); + + yield return CreateData("F8", new object[] { 1, 2, 3, 4, 5, 6, 7, -1 }, null, "System.Int32, 7, -1"); + yield return CreateData("F8", new object[] { 1, 2, 3, 4, 5, 6, 7, Type.Missing }, null, "System.Int32, 7, 8"); + yield return CreateData("F8", new object[] { 1, 2, 3, 4, 5, 6, Type.Missing, Type.Missing }, null, "System.Reflection.Missing, null, 8"); + yield return CreateData("F8", new object[] { 1, 2, 3, 4, 5, 6, Type.Missing, Type.Missing }, new[] { typeof(int) }, "System.Int32, 0, 8"); + + static object[] CreateData(string memberName, object[] arguments, Type[] typeArguments, string expectedValue) => new object[] { memberName, arguments, typeArguments, expectedValue }; + } + + [Theory] + [MemberData(nameof(LateCall_OptionalValues_Data))] + public void LateCall_OptionalValues(string memberName, object[] arguments, Type[] typeArguments, string expectedValue) + { + // NewLateBinding.LateCall() corresponds to a call to the member when using late binding: + // Dim instance = New OptionalValuesType() + // instance.Member(arguments) + var actualValue = NewLateBinding.LateCall( + Instance: new OptionalValuesType(), + Type: null, + MemberName: memberName, + Arguments: arguments, + ArgumentNames: null, + TypeArguments: typeArguments, + CopyBack: null, + IgnoreReturn: true); + Assert.Equal(expectedValue, actualValue); + } + } +} -- 2.7.4