Skip to content

Commit 0056dbd

Browse files
authored
Overload resolution improvements (#377)
1 parent d141fb6 commit 0056dbd

8 files changed

Lines changed: 1003 additions & 166 deletions

File tree

docs/reference/overloaded-methods.md

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,65 @@ function by dynamically checking for different argument counts and/or types.
66

77
## Overload resolution
88

9-
The JS [marshaller](../features/js-dotnet-marshalling) has limited support for overload
10-
resolution. It can examine the count and types of arguments provided by the JavaScript caller and
11-
select the best matching .NET overload accordingly.
9+
The JS [marshaller](../features/js-dotnet-marshalling) has support for overload resolution. It can
10+
examine the count and types of arguments provided by the JavaScript caller and select the best
11+
matching .NET overload accordingly.
1212

1313
```C#
1414
[JSExport]
1515
public class OverloadsExample
1616
{
1717
public static void AddValue(string stringValue);
18-
public static void AddValue(double numberValue);
18+
public static void AddValue(int intValue);
19+
public static void AddValue(double doubleValue);
1920
}
2021
```
2122
```JS
2223
OverloadsExample.addValue('test'); // Calls AddValue(string)
23-
OverloadsExample.addValue(77); // Calls AddValue(double)
24+
OverloadsExample.addValue(77); // Calls AddValue(int)
25+
OverloadsExample.addValue(0.5); // Calls AddValue(double)
2426
```
2527

26-
Currently the overload resolution is limited to examining the JavaScript type of each argument
27-
(`string`, `number`, `object`, etc), but that is not sufficient to select between overloads that
28-
differ only in the _type of object_.
29-
[More advanced overload resolution is planned.](https://github.com/microsoft/node-api-dotnet/issues/134)
28+
Overload resolution considers the following information when selecting the best match among method
29+
overloads:
30+
- **Argument count** - Resolution eliminates any overloads that do not accept the number of
31+
arguments that were supplied, taking into account when some .NET method parameters are
32+
optional or have default values.
33+
- **Argument JS value types** - Resolution initially does a quick filter by matching only on
34+
the [JavaScript value type](./dotnet/Microsoft.JavaScript.NodeApi/JSValueType) of each argument,
35+
e.g. JS `string` matches .NET `string`, JS `number` matches any .NET numeric type,
36+
JS `object` matches any .NET class, interface, or struct type.
37+
- **Nullability** - JS `null` or `undefined` arguments match with any method parameters that are
38+
.NET reference types or `Nullable<T>` value types. (Non-nullable reference type annotations are
39+
not considered.)
40+
- **Number argument properties** - If there are multiple overloads with different .NET numeric
41+
types (e.g. `int` and `double`), the properties of the JS number value are used to select the
42+
best overload, including whether it is negative, an integer, or outside the bounds of the .NET
43+
numeric type.
44+
- **Proxied .NET object types** - When a JS argument value is actually
45+
[a proxy to a .NET object](./classes-interfaces.md#marshalling-net-classes-to-js),
46+
then the .NET type is matched to the method parameter type.
47+
- **JS collection types** - When an argument value is a JS collection, the JS collection type
48+
such as `Array` or `Map` is matched to a corresponding .NET collection type. (Generic collection
49+
_element_ types are not considered, since JS collections do not have specific element types.)
50+
- **Other special types** - Types with special marshalling behavior including [dates](./dates),
51+
[guids](./other-types), [Tasks/Promises](./async-promises), and [delegates](./delegates) are
52+
matched accordingly during overload resolution.
53+
54+
If overload resolution finds multiple matches, or does not find any valid matches, then a
55+
`TypeError` is thrown.
56+
57+
### Performance considerations
58+
59+
Unlike compiled languages where the compiler can bind to the appropriate overload at compile time,
60+
with JavaScript the overload resolution process must be repeated at every invocation of the method.
61+
It is not super expensive, but consider avoiding calls to overloaded methods in performance-critical
62+
code.
63+
64+
### Limitations
65+
66+
When calling .NET methods from JavaScript, the dynamic overload resolution is not 100% consistent
67+
with C#'s compile-time overload resolution. There are some unavoidable limitations due to the
68+
dynamic-typed nature of JavaScript, and likely some deficienceies in the implementation. While it
69+
should work sufficiently well for the majority of cases, if you find a situation where overload
70+
resolution is not working as expected, please [report a bug](../support).

src/NodeApi.DotNetHost/JSMarshaller.cs

Lines changed: 156 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,7 +1262,8 @@ public Expression<Func<JSCallbackDescriptor>> BuildConstructorOverloadDescriptor
12621262
// TODO: Default parameter values
12631263

12641264
Type[] parameterTypes = constructors[i].GetParameters()
1265-
.Select(p => p.ParameterType).ToArray();
1265+
.Select(p => EnsureObjectCollectionTypeForOverloadResolution(p.ParameterType))
1266+
.ToArray();
12661267
statements[i + 1] = Expression.Assign(
12671268
Expression.ArrayAccess(overloadsVariable, Expression.Constant(i)),
12681269
Expression.New(
@@ -1353,7 +1354,8 @@ public Expression<Func<JSCallbackDescriptor>> BuildMethodOverloadDescriptorExpre
13531354
// TODO: Default parameter values
13541355

13551356
Type[] parameterTypes = methods[i].GetParameters()
1356-
.Select(p => p.ParameterType).ToArray();
1357+
.Select(p => EnsureObjectCollectionTypeForOverloadResolution(p.ParameterType))
1358+
.ToArray();
13571359
statements[i + 1] = Expression.Assign(
13581360
Expression.ArrayAccess(overloadsVariable, Expression.Constant(i)),
13591361
Expression.New(
@@ -1381,6 +1383,77 @@ public Expression<Func<JSCallbackDescriptor>> BuildMethodOverloadDescriptorExpre
13811383
Array.Empty<ParameterExpression>());
13821384
}
13831385

1386+
/// <summary>
1387+
/// For purposes of overload resolution, convert non-generic collections and collections
1388+
/// with specific generic element types to collection interfaces with object element types.
1389+
/// This simplifies the checks required during overload resolution and avoids the need
1390+
/// for reflection at resolution time, which is not supported in AOT. (The JS collections
1391+
/// will still be marshalled to the more specific collection type after resolution when
1392+
/// an overload is invoked.)
1393+
/// </summary>
1394+
private static Type EnsureObjectCollectionTypeForOverloadResolution(Type type)
1395+
{
1396+
if (typeof(System.Collections.IEnumerable).IsAssignableFrom(type) &&
1397+
!type.IsArray && type != typeof(string))
1398+
{
1399+
if (TypeImplementsGenericInterface(type, typeof(IDictionary<,>)) ||
1400+
TypeImplementsGenericInterface(type, typeof(IReadOnlyDictionary<,>)) ||
1401+
typeof(System.Collections.IDictionary).IsAssignableFrom(type))
1402+
{
1403+
type = typeof(IDictionary<object, object>);
1404+
}
1405+
else if (TypeImplementsGenericInterface(type, typeof(IList<>)) ||
1406+
TypeImplementsGenericInterface(type, typeof(IReadOnlyList<>)) ||
1407+
typeof(System.Collections.IList).IsAssignableFrom(type))
1408+
{
1409+
type = typeof(IList<object>);
1410+
}
1411+
else if (TypeImplementsGenericInterface(type, typeof(ISet<>))
1412+
#if READONLY_SET
1413+
|| TypeImplementsGenericInterface(type, typeof(IReadOnlySet<>))
1414+
#endif
1415+
)
1416+
{
1417+
type = typeof(ISet<object>);
1418+
}
1419+
else if (TypeImplementsGenericInterface(type, typeof(ICollection<>)) ||
1420+
TypeImplementsGenericInterface(type, typeof(IReadOnlyCollection<>)))
1421+
{
1422+
type = typeof(ICollection<object>);
1423+
}
1424+
else
1425+
{
1426+
type = typeof(IEnumerable<object>);
1427+
}
1428+
}
1429+
else if (TypeImplementsGenericInterface(type, typeof(IAsyncEnumerable<>)))
1430+
{
1431+
type = typeof(IAsyncEnumerable<object>);
1432+
}
1433+
1434+
return type;
1435+
}
1436+
1437+
private static bool TypeImplementsGenericInterface(Type type, Type genericInterface)
1438+
{
1439+
if (type.IsInterface && type.IsGenericType &&
1440+
type.GetGenericTypeDefinition() == genericInterface)
1441+
{
1442+
return true;
1443+
}
1444+
1445+
foreach (Type interfaceType in type.GetInterfaces())
1446+
{
1447+
if (interfaceType.IsGenericType &&
1448+
interfaceType.GetGenericTypeDefinition() == genericInterface)
1449+
{
1450+
return true;
1451+
}
1452+
}
1453+
1454+
return false;
1455+
}
1456+
13841457
/// <summary>
13851458
/// Gets overload information for a set of overloaded methods.
13861459
/// </summary>
@@ -2968,71 +3041,114 @@ private IEnumerable<Expression> BuildFromJSToCollectionInterfaceExpressions(
29683041
Type elementType = toType.GenericTypeArguments[0];
29693042
Type typeDefinition = toType.GetGenericTypeDefinition();
29703043

2971-
if (typeDefinition == typeof(IList<>) ||
2972-
typeDefinition == typeof(ICollection<>) ||
3044+
if (typeDefinition == typeof(IEnumerable<>) ||
3045+
typeDefinition == typeof(IAsyncEnumerable<>) ||
3046+
typeDefinition == typeof(ISet<>) ||
29733047
#if READONLY_SET
29743048
typeDefinition == typeof(IReadOnlySet<>) ||
29753049
#else
29763050
// TODO: Support IReadOnlySet on .NET Framework / .NET Standard 2.0.
29773051
#endif
2978-
typeDefinition == typeof(ISet<>))
3052+
typeDefinition == typeof(IList<>) ||
3053+
typeDefinition == typeof(IReadOnlyList<>))
29793054
{
29803055
/*
2981-
* value.TryUnwrap() as ICollection<T> ??
2982-
* ((JSArray)value).AsCollection<T>(
2983-
* (value) => (T)value,
2984-
* (value) => (JSValue)value);
3056+
* value.TryUnwrap() as IList<T> ??
3057+
* ((JSArray)value).AsList<T>((value) => (T)value, (value) => (JSValue)value);
29853058
*/
2986-
Type jsCollectionType = typeDefinition.Name.Contains("Set") ?
2987-
typeof(JSSet) : typeof(JSArray);
3059+
Type jsCollectionType =
3060+
typeDefinition == typeof(IEnumerable<>) ? typeof(JSIterable) :
3061+
typeDefinition == typeof(IAsyncEnumerable<>) ? typeof(JSAsyncIterable) :
3062+
typeDefinition.Name.Contains("Set") ? typeof(JSSet) :
3063+
typeof(JSArray);
3064+
bool isBidirectional = (typeDefinition == typeof(IList<>) ||
3065+
#if READONLY_SET
3066+
typeDefinition == typeof(IReadOnlySet<>) ||
3067+
#endif
3068+
typeDefinition == typeof(ISet<>));
29883069
MethodInfo asCollectionMethod = typeof(JSCollectionExtensions).GetStaticMethod(
29893070
#if !STRING_AS_SPAN
29903071
"As" + typeDefinition.Name.Substring(1, typeDefinition.Name.IndexOf('`') - 1),
29913072
#else
29923073
string.Concat("As",
29933074
typeDefinition.Name.AsSpan(1, typeDefinition.Name.IndexOf('`') - 1)),
29943075
#endif
2995-
new[] { jsCollectionType, typeof(JSValue.To<>), typeof(JSValue.From<>) },
3076+
isBidirectional ?
3077+
new[] { jsCollectionType, typeof(JSValue.To<>), typeof(JSValue.From<>) } :
3078+
new[] { jsCollectionType, typeof(JSValue.To<>) },
29963079
elementType);
29973080
MethodInfo asJSCollectionMethod = jsCollectionType.GetExplicitConversion(
29983081
typeof(JSValue), jsCollectionType);
3082+
Expression valueAsCollectionExpression =
3083+
Expression.Convert(valueExpression, jsCollectionType, asJSCollectionMethod);
3084+
Expression fromJSExpression = GetFromJSValueExpression(elementType);
3085+
Expression toJSExpression = GetToJSValueExpression(elementType);
29993086
yield return Expression.Coalesce(
30003087
Expression.TypeAs(Expression.Call(valueExpression, s_tryUnwrap), toType),
3001-
Expression.Call(
3002-
asCollectionMethod,
3003-
Expression.Convert(valueExpression, jsCollectionType, asJSCollectionMethod),
3004-
GetFromJSValueExpression(elementType),
3005-
GetToJSValueExpression(elementType)));
3088+
isBidirectional ?
3089+
Expression.Call(
3090+
asCollectionMethod,
3091+
valueAsCollectionExpression,
3092+
fromJSExpression,
3093+
toJSExpression) :
3094+
Expression.Call(
3095+
asCollectionMethod,
3096+
valueAsCollectionExpression,
3097+
fromJSExpression));
30063098
}
3007-
else if (typeDefinition == typeof(IReadOnlyList<>) ||
3008-
typeDefinition == typeof(IReadOnlyCollection<>) ||
3009-
typeDefinition == typeof(IEnumerable<>) ||
3010-
typeDefinition == typeof(IAsyncEnumerable<>))
3099+
else if (typeDefinition == typeof(ICollection<>) ||
3100+
typeDefinition == typeof(IReadOnlyCollection<>))
30113101
{
30123102
/*
3013-
* value.TryUnwrap() as IReadOnlyCollection<T> ??
3014-
* ((JSArray)value).AsReadOnlyCollection<T>((value) => (T)value);
3103+
* value.TryUnwrap() as ICollection<T> ?? value.IsArray() ?
3104+
* ((JSArray)value).AsCollection<T>((value) => (T)value) :
3105+
* ((JSSet)value).AsCollection<T>((value) => (T)value, (value) => (JSValue)value);
30153106
*/
3016-
Type jsCollectionType = typeDefinition == typeof(IEnumerable<>) ?
3017-
typeof(JSIterable) : typeDefinition == typeof(IAsyncEnumerable<>) ?
3018-
typeof(JSAsyncIterable) : typeof(JSArray);
3019-
MethodInfo asCollectionMethod = typeof(JSCollectionExtensions).GetStaticMethod(
3020-
#if !STRING_AS_SPAN
3021-
"As" + typeDefinition.Name.Substring(1, typeDefinition.Name.IndexOf('`') - 1),
3022-
#else
3023-
string.Concat("As",
3024-
typeDefinition.Name.AsSpan(1, typeDefinition.Name.IndexOf('`') - 1)),
3025-
#endif
3026-
new[] { jsCollectionType, typeof(JSValue.To<>) },
3107+
MethodInfo isArrayMethod = typeof(JSValue).GetInstanceMethod(nameof(JSValue.IsArray));
3108+
bool isReadOnlyCollection = typeDefinition == typeof(IReadOnlyCollection<>);
3109+
string asCollectionMethodName = isReadOnlyCollection ?
3110+
nameof(JSCollectionExtensions.AsReadOnlyCollection) :
3111+
nameof(JSCollectionExtensions.AsCollection);
3112+
MethodInfo arrayAsCollectionMethod = typeof(JSCollectionExtensions).GetStaticMethod(
3113+
asCollectionMethodName,
3114+
isReadOnlyCollection ?
3115+
new[] { typeof(JSArray), typeof(JSValue.To<>) } :
3116+
new[] { typeof(JSArray), typeof(JSValue.To<>), typeof(JSValue.From<>) },
30273117
elementType);
3028-
MethodInfo asJSCollectionMethod = jsCollectionType.GetExplicitConversion(
3029-
typeof(JSValue), jsCollectionType);
3118+
MethodInfo setAsCollectionMethod = typeof(JSCollectionExtensions).GetStaticMethod(
3119+
asCollectionMethodName,
3120+
isReadOnlyCollection ?
3121+
new[] { typeof(JSSet), typeof(JSValue.To<>) } :
3122+
new[] { typeof(JSSet), typeof(JSValue.To<>), typeof(JSValue.From<>) },
3123+
elementType);
3124+
MethodInfo asJSArrayMethod = typeof(JSArray).GetExplicitConversion(
3125+
typeof(JSValue), typeof(JSArray));
3126+
MethodInfo asJSSetMethod = typeof(JSSet).GetExplicitConversion(
3127+
typeof(JSValue), typeof(JSSet));
30303128
yield return Expression.Coalesce(
30313129
Expression.TypeAs(Expression.Call(valueExpression, s_tryUnwrap), toType),
3032-
Expression.Call(
3033-
asCollectionMethod,
3034-
Expression.Convert(valueExpression, jsCollectionType, asJSCollectionMethod),
3035-
GetFromJSValueExpression(elementType)));
3130+
Expression.Condition(
3131+
Expression.Call(valueExpression, isArrayMethod),
3132+
isReadOnlyCollection ?
3133+
Expression.Call(
3134+
arrayAsCollectionMethod,
3135+
Expression.Convert(valueExpression, typeof(JSArray), asJSArrayMethod),
3136+
GetFromJSValueExpression(elementType)) :
3137+
Expression.Call(
3138+
arrayAsCollectionMethod,
3139+
Expression.Convert(valueExpression, typeof(JSArray), asJSArrayMethod),
3140+
GetFromJSValueExpression(elementType),
3141+
GetToJSValueExpression(elementType)),
3142+
isReadOnlyCollection ?
3143+
Expression.Call(
3144+
setAsCollectionMethod,
3145+
Expression.Convert(valueExpression, typeof(JSSet), asJSSetMethod),
3146+
GetFromJSValueExpression(elementType)) :
3147+
Expression.Call(
3148+
setAsCollectionMethod,
3149+
Expression.Convert(valueExpression, typeof(JSSet), asJSSetMethod),
3150+
GetFromJSValueExpression(elementType),
3151+
GetToJSValueExpression(elementType))));
30363152
}
30373153
else if (typeDefinition == typeof(IDictionary<,>))
30383154
{

src/NodeApi.Generator/ExpressionExtensions.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@ private static string ToCS(
102102
") { " + ToCS(conditional.IfTrue, path, variables) + "; }" +
103103
(conditional.IfFalse is DefaultExpression ? string.Empty :
104104
" else { " + ToCS(conditional.IfFalse, path, variables) + "; }")
105-
: ToCS(conditional.Test, path, variables) + " ?\n" +
105+
: '(' + ToCS(conditional.Test, path, variables) + " ?\n" +
106106
ToCS(conditional.IfTrue, path, variables) + " :\n" +
107-
ToCS(conditional.IfFalse, path, variables),
107+
ToCS(conditional.IfFalse, path, variables) + ')',
108108

109109
MemberExpression { NodeType: ExpressionType.MemberAccess } member =>
110110
member.Expression is ParameterExpression parameterExpression &&
@@ -375,6 +375,10 @@ internal static string FormatType(Type type)
375375
{
376376
return "string";
377377
}
378+
else if (type == typeof(object))
379+
{
380+
return "object";
381+
}
378382
else if (type == typeof(void))
379383
{
380384
return "void";

0 commit comments

Comments
 (0)