Skip to content

Commit

Permalink
Add support for named arguments.
Browse files Browse the repository at this point in the history
Refactoring
  • Loading branch information
JLChnToZ committed Jan 9, 2024
1 parent 7df384c commit 23d6a36
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 38 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/JLChnToZ.CommonUtils.Dynamic.Test/bin/Debug/net6.0/JLChnToZ.CommonUtils.Dynamic.Test.dll",
"program": "${workspaceFolder}/JLChnToZ.CommonUtils.Dynamic.Test/bin/Debug/net7.0/JLChnToZ.CommonUtils.Dynamic.Test.dll",
"args": [],
"cwd": "${workspaceFolder}/JLChnToZ.CommonUtils.Dynamic.Test",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
Expand Down
5 changes: 5 additions & 0 deletions JLChnToZ.CommonUtils.Dynamic.Test/LimitlessTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ void TestMethodAccess(dynamic limitless, bool isStatic) {
if (isStatic) return;
Assert.IsTrue(limitless.PrivateMethod(true), "Access private method");
Assert.IsNull(limitless.PrivateMethod(null), "Access private method with null");
limitless.PrivateMethodMultiArgs(1, qux: 2);
Assert.IsTrue(
"PrivateMethodMultiArgs1,0,0,2" == TestSubject.lastCalledMethod,
"Access private method with multiple arguments. Get " + TestSubject.lastCalledMethod
);
}

void TestGenericMethodAccess(dynamic limitless, bool isStatic) {
Expand Down
6 changes: 5 additions & 1 deletion JLChnToZ.CommonUtils.Dynamic.Test/TestSubject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private object PrivateMethod(object par) {
}

private bool PrivateMethod(bool par) {
lastCalledMethod = "PrivateMethodBool";
lastCalledMethod = "PrivateMethodBool";
return par;
}

Expand All @@ -106,6 +106,10 @@ bool IInterface.HiddenInterfaceMethod(bool par) {
lastCalledMethod = "HiddenInterfaceMethodBool";
return par;
}

void PrivateMethodMultiArgs(int foo = 0, int bar = 0, int baz = 0, int qux = 0) {
lastCalledMethod = "PrivateMethodMultiArgs" + string.Join(',', foo, bar, baz, qux);
}

public static IEnumerable<object> EnumerableMethod() {
lastCalledMethod = "EnumerableMethod";
Expand Down
40 changes: 29 additions & 11 deletions JLChnToZ.CommonUtils.Dynamic/Delegates.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,17 +224,7 @@ public static Delegate BindConverter(Type srcType, Type destType, Type delegateT
static Delegate BindConverterUnchecked(Type srcType, Type destType, Type delegateType) {
lock (converterCache) {
if (!converterCache.TryGetValue((srcType, destType, delegateType), out var converterFunc)) {
var method =
destType.IsAssignableFrom(srcType) ?
typeof(Delegates).GetMethod(nameof(ToBaseType), STATIC_FLAGS).MakeGenericMethod(srcType, destType) :
srcType.IsAssignableFrom(destType) ?
typeof(Delegates).GetMethod(nameof(ToInheritedType), STATIC_FLAGS).MakeGenericMethod(srcType, destType) :
TypeInfo.Get(destType).TryGetConverter(srcType, true, out var m) ? m :
TypeInfo.Get(srcType).TryGetConverter(destType, false, out m) ? m :
srcType.IsPrimitive && destType.IsPrimitive ?
typeof(Convert).GetMethod($"To{Type.GetTypeCode(destType)}", STATIC_FLAGS, null, new[] { srcType }, null) :
typeof(Delegates).GetMethod(nameof(ToType), STATIC_FLAGS).MakeGenericMethod(srcType, destType);
if (method != null)
if (TryGetConverterMethod(srcType, destType, out var method))
converterFunc = Delegate.CreateDelegate(
delegateType ?? typeof(Func<,>).MakeGenericType(srcType, destType),
method, false
Expand All @@ -245,6 +235,34 @@ static Delegate BindConverterUnchecked(Type srcType, Type destType, Type delegat
}
}

static bool TryGetConverterMethod(Type srcType, Type destType, out MethodInfo result) {
if (srcType == null) throw new ArgumentNullException(nameof(srcType));
if (destType == null) throw new ArgumentNullException(nameof(destType));
if (srcType == destType) {
result = typeof(Delegates).GetMethod(nameof(ToSameType), STATIC_FLAGS).MakeGenericMethod(srcType);
return true;
}
if (destType.IsAssignableFrom(srcType)) {
result = typeof(Delegates).GetMethod(nameof(ToBaseType), STATIC_FLAGS).MakeGenericMethod(srcType, destType);
return true;
}
if (srcType.IsAssignableFrom(destType)) {
result = typeof(Delegates).GetMethod(nameof(ToInheritedType), STATIC_FLAGS).MakeGenericMethod(srcType, destType);
return true;
}
if (TypeInfo.Get(destType).TryGetConverter(srcType, true, out result) ||
TypeInfo.Get(srcType).TryGetConverter(destType, false, out result))
return true;
if (srcType.IsPrimitive && destType.IsPrimitive) {
result = typeof(Convert).GetMethod($"To{Type.GetTypeCode(destType)}", STATIC_FLAGS, null, new[] { srcType }, null);
return result != null;
}
result = null;
return false;
}

static T ToSameType<T>(T value) => value;

static TDest ToBaseType<TSource, TDest>(TSource value) where TSource : TDest => value;

static TDest ToInheritedType<TSource, TDest>(TSource value) where TDest : TSource => (TDest)value;
Expand Down
12 changes: 6 additions & 6 deletions JLChnToZ.CommonUtils.Dynamic/Limitless.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ public override bool TryDeleteMember(DeleteMemberBinder binder) {
}

public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) =>
TryInvoke(false, binder.Name, out result, args) ||
TryInvoke(false, binder.Name, out result, args, binder.CallInfo) ||
(target is DynamicObject dynamicObject && dynamicObject.TryInvokeMember(binder, args, out result));

public override bool TryInvoke(InvokeBinder binder, object[] args, out object result) {
Expand All @@ -181,15 +181,15 @@ public override bool TryInvoke(InvokeBinder binder, object[] args, out object re
return false;
}

internal bool TryInvoke(bool isStatic, string name, out object result, params object[] args) =>
typeInfo.TryInvoke(isStatic ? null : target, name, args, out result);
internal bool TryInvoke(bool isStatic, string name, out object result, object[] args = null, CallInfo callInfo = null) =>
typeInfo.TryInvoke(isStatic ? null : target, name, args, out result, callInfo);

public override bool TryBinaryOperation(BinaryOperationBinder binder, object arg, out object result) =>
TryInvoke(true, binder.Operation.ToOperatorMethodName(), out result, target, arg) ||
TryInvoke(true, binder.Operation.ToOperatorMethodName(), out result, new [] { target, arg }) ||
(target is DynamicObject dynamicObject && dynamicObject.TryBinaryOperation(binder, arg, out result));

public override bool TryUnaryOperation(UnaryOperationBinder binder, out object result) =>
TryInvoke(true, binder.Operation.ToOperatorMethodName(), out result, target) ||
TryInvoke(true, binder.Operation.ToOperatorMethodName(), out result, new [] { target }) ||
(target is DynamicObject dynamicObject && dynamicObject.TryUnaryOperation(binder, out result));

public override bool TryConvert(ConvertBinder binder, out object result) {
Expand All @@ -209,7 +209,7 @@ public override bool TryConvert(ConvertBinder binder, out object result) {

// Supports await ... syntax
public virtual LimitlessAwaiter GetAwaiter() =>
new LimitlessAwaiter(TryInvoke(false, nameof(GetAwaiter), out var awaiter, emptyArgs) ? InternalUnwrap(awaiter) : target);
new LimitlessAwaiter(TryInvoke(false, nameof(GetAwaiter), out var awaiter) ? InternalUnwrap(awaiter) : target);

// Supports foreach (... in ...) { ... } syntax
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
Expand Down
2 changes: 1 addition & 1 deletion JLChnToZ.CommonUtils.Dynamic/LimitlessAwaiter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ void INotifyCompletion.OnCompleted(Action continuation) {
}

public dynamic GetResult() {
if (target is INotifyCompletion && typeInfo.TryInvoke(target, nameof(GetResult), emptyArgs, out var result))
if (target is INotifyCompletion && typeInfo.TryInvoke(target, nameof(GetResult), null, out var result))
return result;
return InternalWrap(target);
}
Expand Down
8 changes: 4 additions & 4 deletions JLChnToZ.CommonUtils.Dynamic/LimitlessInvokable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, o
}

public override bool TryInvoke(InvokeBinder binder, object[] args, out object result) =>
TryInvoke(args, out result);
TryInvoke(args, out result, binder.CallInfo);

bool TryInvoke(object[] args, out object result) {
bool TryInvoke(object[] args, out object result, CallInfo callInfo = null) {
var safeArgs = args;
if (TryGetMatchingMethod(MethodInfos, ref safeArgs, out var methodInfo)) {
if (TryGetMatchingMethod(MethodInfos, ref safeArgs, out var methodInfo, callInfo, out var bindState)) {
result = InternalWrap(methodInfo.Invoke(InvokeTarget, safeArgs));
InternalWrap(safeArgs, args);
InternalWrap(safeArgs, args, bindState, callInfo?.ArgumentNames.Count ?? 0);
return true;
}
result = null;
Expand Down
11 changes: 6 additions & 5 deletions JLChnToZ.CommonUtils.Dynamic/TypeInfo.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Reflection;

namespace JLChnToZ.CommonUtils.Dynamic {
Expand Down Expand Up @@ -159,12 +160,12 @@ internal bool TrySetValue(object instance, object[] indexes, object value) {
internal bool TryGetConverter(Type type, bool isIn, out MethodInfo method) =>
(isIn ? castInOperators : castOutOperators).TryGetValue(type, out method);

internal bool TryInvoke(object instance, string methodName, object[] args, out object result) {
var safeArgs = args;
internal bool TryInvoke(object instance, string methodName, object[] args, out object result, CallInfo callInfo = null) {
var safeArgs = args ?? Array.Empty<object>();
if (methods.TryGetValue(methodName, out var methodArrays) &&
TryGetMatchingMethod(methodArrays, ref safeArgs, out var resultMethod)) {
TryGetMatchingMethod(methodArrays, ref safeArgs, out var resultMethod, callInfo, out var bindState)) {
result = InternalWrap(resultMethod.Invoke(resultMethod.IsStatic ? null : InternalUnwrap(instance), safeArgs));
InternalWrap(safeArgs, args);
InternalWrap(safeArgs, args, bindState, callInfo?.ArgumentNames.Count ?? 0);
return true;
}
result = null;
Expand All @@ -191,7 +192,7 @@ internal bool TryCast(object instance, Type type, bool isIn, out object result)
}

internal bool TryConstruct(object[] args, out object result) {
if (TryGetMatchingMethod(constructors, ref args, out var resultConstructor)) {
if (TryGetMatchingMethod(constructors, ref args, out var resultConstructor, null, out _)) {
result = InternalWrap(resultConstructor.Invoke(args));
return true;
}
Expand Down
70 changes: 62 additions & 8 deletions JLChnToZ.CommonUtils.Dynamic/Utilites.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq.Expressions;
using System.Reflection;

Expand All @@ -9,7 +10,6 @@ internal static class Utilites {
public const BindingFlags STATIC_FLAGS = BASE_FLAGS | BindingFlags.Static;
public const BindingFlags INSTANCE_FLAGS = BASE_FLAGS | BindingFlags.Instance;
public const BindingFlags DEFAULT_FLAGS = STATIC_FLAGS | INSTANCE_FLAGS;
public static readonly object[] emptyArgs = new object[0];

public static object InternalUnwrap(object obj, Type requestedType = null) {
if (obj == null) return null;
Expand All @@ -33,18 +33,72 @@ public static object[] InternalUnwrap(object[] args, ParameterInfo[] parameterIn
public static object InternalWrap(object obj, Type type = null) =>
obj == null || obj is Limitless || obj is LimitlessInvokable || Type.GetTypeCode(obj.GetType()) != TypeCode.Object ? obj : new Limitless(obj, type);

public static void InternalWrap(object[] sourceObj, object[] destObj) {
public static void InternalWrap(object[] sourceObj, object[] destObj, object bindState = null, int namedCount = 0) {
if (sourceObj == null || destObj == null) return;
// If namedCount is not zero and smaller than the length of the array,
// which means the arguments array has been adjusted to fit BindToMethod,
// we need to swap the arguments back.
if (namedCount > 0 && namedCount < sourceObj.Length) {
var temp = new object[namedCount];
int orderedCount = sourceObj.Length - namedCount;
Array.Copy(sourceObj, 0, temp, 0, namedCount);
Array.Copy(sourceObj, namedCount, sourceObj, 0, orderedCount);
Array.Copy(temp, 0, sourceObj, orderedCount, namedCount);
}
if (bindState != null) Type.DefaultBinder.ReorderArgumentArray(ref sourceObj, bindState);
if (sourceObj != destObj) Array.Copy(sourceObj, 0, destObj, 0, Math.Min(sourceObj.Length, destObj.Length));
for (int i = 0; i < destObj.Length; i++) destObj[i] = InternalWrap(destObj[i]);
}

public static bool TryGetMatchingMethod<T>(T[] methodInfos, ref object[] args, out T bestMatches) where T : MethodBase {
if (args == null) args = emptyArgs;
bestMatches = SelectMethod(methodInfos, Array.ConvertAll(args, GetUndelyType));
if (bestMatches != null) {
args = InternalUnwrap(args, bestMatches.GetParameters());
return true;
public static bool TryGetMatchingMethod<T>(T[] methodInfos, ref object[] args, out T bestMatches, CallInfo callInfo, out object bindState) where T : MethodBase {
if (args == null) args = Array.Empty<object>();
string[] names = null;
if (callInfo != null) {
var inputNames = callInfo.ArgumentNames;
int namedCount = inputNames.Count;
// Named input args detected.
if (namedCount > 0) {
names = new string[namedCount];
inputNames.CopyTo(names, 0);
// Named arguments are queued to the end of the array but BindToMethod requires them to be at the beginning,
// therefore we need to swap here.
if (args.Length > namedCount) {
var temp = new object[namedCount];
int orderedCount = args.Length - namedCount;
Array.Copy(args, orderedCount, temp, 0, namedCount);
Array.Copy(args, 0, args, namedCount, orderedCount);
Array.Copy(temp, 0, args, 0, namedCount);
}
}
}
try {
var unwrappedArgs = InternalUnwrap(args);
bestMatches = Type.DefaultBinder.BindToMethod(DEFAULT_FLAGS, methodInfos, ref unwrappedArgs, null, null, names, out bindState) as T;
if (bestMatches != null) {
args = unwrappedArgs;
return true;
}
} catch (MissingMethodException) {
// No matching method found.
bestMatches = null;
bindState = null;
} catch (AmbiguousMatchException) {
// BindToMethod is too stict sometimes, so in case of failing (and named arguments not used),
// we can give a second try: use SelectMethod as a fallback.
bindState = null;
if (names == null) {
bestMatches = SelectMethod(methodInfos, Array.ConvertAll(args, GetUndelyType));
if (bestMatches != null) {
args = InternalUnwrap(args, bestMatches.GetParameters());
return true;
}
} else
bestMatches = null;
} catch (IndexOutOfRangeException) {
// Should not get here, but theres a bug in .NET Core 6 or earlier may.
// (https://github.com/dotnet/runtime/issues/66237)
bestMatches = null;
bindState = null;
}
return false;
}
Expand Down

0 comments on commit 23d6a36

Please sign in to comment.