Skip to content

Commit 80cb809

Browse files
authored
JSExport method overload support (#221)
1 parent 2f53044 commit 80cb809

File tree

6 files changed

+226
-59
lines changed

6 files changed

+226
-59
lines changed

src/NodeApi.DotNetHost/JSMarshaller.cs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ public class JSMarshaller
4343
/// </summary>
4444
public const string ResultPropertyName = "result";
4545

46+
/// <summary>
47+
/// Keeps track of the names of all generated lambda expressions in order to automatically
48+
/// avoid collisions, which can occur with overloaded methods.
49+
/// </summary>
50+
private readonly HashSet<string> _expressionNames = new();
51+
4652
[ThreadStatic]
4753
private static JSMarshaller? s_current;
4854

@@ -805,7 +811,7 @@ Expression ParameterToJSValue(int index) => InlineOrInvoke(
805811
return Expression.Lambda(
806812
_delegates.Value.GetToJSDelegateType(method.ReturnType, parameters),
807813
Expression.Block(method.ReturnType, new[] { resultVariable }, statements),
808-
$"to_{FullMethodName(method)}",
814+
FullMethodName(method, "to_"),
809815
parameters);
810816
}
811817
catch (Exception ex)
@@ -874,7 +880,7 @@ public LambdaExpression BuildFromJSFunctionExpression(MethodInfo method)
874880
return Expression.Lambda(
875881
JSMarshallerDelegates.GetFromJSDelegateType(method.DeclaringType!),
876882
body: Expression.Block(typeof(JSValue), variables, statements),
877-
$"from_{FullMethodName(method)}",
883+
FullMethodName(method, "from_"),
878884
parameters: new[] { thisParameter, s_argsParameter });
879885
}
880886
catch (Exception ex)
@@ -1265,6 +1271,7 @@ public Expression<Func<JSCallbackDescriptor>> BuildMethodOverloadDescriptorExpre
12651271
* return JSCallbackOverload.CreateDescriptor(methodName, overloads);
12661272
*/
12671273

1274+
string name = FullMethodName(methods[0]);
12681275
ParameterExpression overloadsVariable =
12691276
Expression.Variable(typeof(JSCallbackOverload[]), "overloads");
12701277
var statements = new Expression[methods.Length + 2];
@@ -1304,7 +1311,7 @@ public Expression<Func<JSCallbackDescriptor>> BuildMethodOverloadDescriptorExpre
13041311
typeof(JSCallbackDescriptor),
13051312
new[] { overloadsVariable },
13061313
statements),
1307-
name: FullMethodName(methods[0]),
1314+
name,
13081315
Array.Empty<ParameterExpression>());
13091316
}
13101317

@@ -3015,17 +3022,30 @@ private static bool IsTypedArrayType(Type elementType)
30153022
|| elementType == typeof(double);
30163023
}
30173024

3018-
private static string FullMethodName(MethodInfo method)
3025+
private string FullMethodName(MethodInfo method, string? prefix = null)
30193026
{
3020-
string prefix = string.Empty;
30213027
string name = method.Name;
30223028
if (name.StartsWith("get_") || name.StartsWith("set_"))
30233029
{
3024-
prefix = name.Substring(0, 4);
3030+
prefix ??= name.Substring(0, 4);
30253031
name = name.Substring(4);
30263032
}
3033+
else
3034+
{
3035+
prefix ??= string.Empty;
3036+
}
3037+
3038+
// Ensure the generated name is unique by appending a counter suffix if necessary.
3039+
string fullName = $"{prefix}{FullTypeName(method.DeclaringType!)}_{name}";
3040+
string suffix = string.Empty;
3041+
for (int i = 2; _expressionNames.Contains(fullName + suffix); i++)
3042+
{
3043+
suffix = $"_{i}";
3044+
}
30273045

3028-
return $"{prefix}{FullTypeName(method.DeclaringType!)}_{name}";
3046+
fullName += suffix;
3047+
_expressionNames.Add(fullName);
3048+
return fullName;
30293049
}
30303050

30313051
internal static string FullTypeName(Type type)

src/NodeApi.Generator/ModuleGenerator.cs

Lines changed: 61 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -368,18 +368,20 @@ private void ExportModule(
368368
s += $"exportsValue = new JSModuleBuilder<{ns}.{moduleType.Name}>()";
369369
s.IncreaseIndent();
370370

371-
// Export non-static members of the module class.
372-
foreach (ISymbol? member in moduleType.GetMembers()
373-
.Where((m) => m.DeclaredAccessibility == Accessibility.Public && !m.IsStatic))
371+
// Export public non-static members of the module class.
372+
IEnumerable<ISymbol> members = moduleType.GetMembers()
373+
.Where((m) => m.DeclaredAccessibility == Accessibility.Public && !m.IsStatic);
374+
375+
foreach (IPropertySymbol property in members.OfType<IPropertySymbol>())
374376
{
375-
if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary)
376-
{
377-
ExportMethod(ref s, method);
378-
}
379-
else if (member is IPropertySymbol property)
380-
{
381-
ExportProperty(ref s, property);
382-
}
377+
ExportProperty(ref s, property, GetExportName(property));
378+
}
379+
380+
foreach (IGrouping<string, IMethodSymbol> methodGroup in members.OfType<IMethodSymbol>()
381+
.Where((m) => m.MethodKind == MethodKind.Ordinary)
382+
.GroupBy(GetExportName))
383+
{
384+
ExportMethod(ref s, methodGroup, methodGroup.Key);
383385
}
384386
}
385387
else
@@ -401,18 +403,20 @@ private void ExportModule(
401403
// Export tagged static properties as properties on the module.
402404
ExportProperty(ref s, exportProperty, exportName);
403405
}
404-
else if (exportItem is IMethodSymbol exportMethod)
405-
{
406-
// Export tagged static methods as top-level functions on the module.
407-
ExportMethod(ref s, exportMethod, exportName);
408-
}
409406
else if (exportItem is ITypeSymbol exportDelegate &&
410407
exportDelegate.TypeKind == TypeKind.Delegate)
411408
{
412409
ExportDelegate(exportDelegate);
413410
}
414411
}
415412

413+
// Export tagged static methods as top-level functions on the module.
414+
foreach (IGrouping<string, IMethodSymbol> methodGroup in exportItems.OfType<IMethodSymbol>()
415+
.GroupBy(GetExportName))
416+
{
417+
ExportMethod(ref s, methodGroup, methodGroup.Key);
418+
}
419+
416420
if (moduleType != null)
417421
{
418422
// Construct an instance of the custom module class when the module is initialized.
@@ -434,10 +438,8 @@ private void ExportModule(
434438
private void ExportType(
435439
ref SourceBuilder s,
436440
ITypeSymbol type,
437-
string? exportName = null)
441+
string exportName)
438442
{
439-
exportName ??= type.Name;
440-
441443
string propertyAttributes = string.Empty;
442444
if (type.ContainingType != null)
443445
{
@@ -547,22 +549,15 @@ private void ExportMembers(
547549
{
548550
bool isStreamClass = typeof(System.IO.Stream).IsAssignableFrom(type.AsType());
549551

550-
foreach (ISymbol member in type.GetMembers()
551-
.Where((m) => m.DeclaredAccessibility == Accessibility.Public))
552-
{
553-
if (isStreamClass && !member.IsStatic)
554-
{
555-
// Only static members on stream subclasses are exported to JS.
556-
continue;
557-
}
552+
IEnumerable<ISymbol> members = type.GetMembers()
553+
.Where((m) => m.DeclaredAccessibility == Accessibility.Public)
554+
.Where((m) => !isStreamClass || m.IsStatic);
558555

559-
if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary)
560-
{
561-
ExportMethod(ref s, method);
562-
}
563-
else if (member is IPropertySymbol property)
556+
foreach (ISymbol member in members)
557+
{
558+
if (member is IPropertySymbol property)
564559
{
565-
ExportProperty(ref s, property);
560+
ExportProperty(ref s, property, GetExportName(member));
566561
}
567562
else if (type.TypeKind == TypeKind.Enum && member is IFieldSymbol field)
568563
{
@@ -571,42 +566,61 @@ private void ExportMembers(
571566
}
572567
else if (member is INamedTypeSymbol nestedType)
573568
{
574-
ExportType(ref s, nestedType);
569+
ExportType(ref s, nestedType, GetExportName(member));
575570
}
576571
}
572+
573+
foreach (IGrouping<string, IMethodSymbol> methodGroup in members
574+
.OfType<IMethodSymbol>().Where((m) => m.MethodKind == MethodKind.Ordinary)
575+
.GroupBy(GetExportName))
576+
{
577+
ExportMethod(ref s, methodGroup, methodGroup.Key);
578+
}
577579
}
578580

579581
/// <summary>
580582
/// Generate code for a method exported on a class, struct, or module.
581583
/// </summary>
582584
private void ExportMethod(
583585
ref SourceBuilder s,
584-
IMethodSymbol method,
585-
string? exportName = null)
586+
IEnumerable<IMethodSymbol> methods,
587+
string exportName)
586588
{
587-
exportName ??= ToCamelCase(method.Name);
589+
// TODO: Support exporting generic methods.
590+
methods = methods.Where((m) => !m.IsGenericMethod);
591+
592+
IMethodSymbol? method = methods.FirstOrDefault();
593+
if (method == null)
594+
{
595+
return;
596+
}
588597

589-
// An adapter method may be used to support marshalling arbitrary parameters,
590-
// if the method does not match the `JSCallback` signature.
591598
string attributes = "JSPropertyAttributes.DefaultMethod" +
592599
(method.IsStatic ? " | JSPropertyAttributes.Static" : string.Empty);
593-
if (method.IsGenericMethod)
600+
601+
if (methods.Count() == 1 && !IsMethodCallbackAdapterRequired(method))
594602
{
595-
// TODO: Export generic method.
603+
// No adapter is needed for a method with a JSCallback signature.
604+
string ns = GetNamespace(method);
605+
string className = method.ContainingType.Name;
606+
s += $".AddMethod(\"{exportName}\", " +
607+
$"{ns}.{className}.{method.Name},\n\t{attributes})";
596608
}
597-
else if (IsMethodCallbackAdapterRequired(method))
609+
else if (methods.Count() == 1)
598610
{
611+
// An adapter method supports marshalling arbitrary parameters.
599612
Expression<JSCallback> adapter =
600613
_marshaller.BuildFromJSMethodExpression(method.AsMethodInfo());
601614
_callbackAdapters.Add(adapter.Name!, adapter);
602615
s += $".AddMethod(\"{exportName}\", {adapter.Name},\n\t{attributes})";
603616
}
604617
else
605618
{
606-
string ns = GetNamespace(method);
607-
string className = method.ContainingType.Name;
608-
s += $".AddMethod(\"{exportName}\", " +
609-
$"{ns}.{className}.{method.Name},\n\t{attributes})";
619+
// An adapter method provides overload resolution.
620+
LambdaExpression adapter = _marshaller.BuildMethodOverloadDescriptorExpression(
621+
methods.Select((m) => m.AsMethodInfo()).ToArray());
622+
_callbackAdapters.Add(adapter.Name!, adapter);
623+
s += $".AddMethod(\"{exportName}\", {adapter.Name}(),\n\t{attributes})";
610624
}
611625
}
612626

@@ -616,10 +630,8 @@ private void ExportMethod(
616630
private void ExportProperty(
617631
ref SourceBuilder s,
618632
IPropertySymbol property,
619-
string? exportName = null)
633+
string exportName)
620634
{
621-
exportName ??= ToCamelCase(property.Name);
622-
623635
bool writable = property.SetMethod != null ||
624636
(!property.IsStatic && property.ContainingType.TypeKind == TypeKind.Struct);
625637
string attributes = "JSPropertyAttributes.Enumerable | JSPropertyAttributes.Configurable" +

src/NodeApi.Generator/SymbolExtensions.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,8 @@ private static ConstructorBuilder BuildSymbolicConstructor(
414414
IReadOnlyList<IParameterSymbol> parameters = constructorSymbol.Parameters;
415415
for (int i = 0; i < parameters.Count; i++)
416416
{
417-
constructorBuilder.DefineParameter(i, ParameterAttributes.None, parameters[i].Name);
417+
// The parameter index is offset by 1.
418+
constructorBuilder.DefineParameter(i + 1, ParameterAttributes.None, parameters[i].Name);
418419
}
419420

420421
if (isDelegateConstructor)
@@ -556,8 +557,10 @@ public static ConstructorInfo AsConstructorInfo(this IMethodSymbol methodSymbol)
556557
parameter.Type.AsType(type.GenericTypeArguments, buildType: true);
557558
}
558559

559-
ConstructorInfo? constructorInfo = type.GetConstructor(
560-
methodSymbol.Parameters.Select((p) => p.Type.AsType()).ToArray());
560+
BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance;
561+
ConstructorInfo? constructorInfo = type.GetConstructors(bindingFlags)
562+
.FirstOrDefault((c) => c.GetParameters().Select((p) => p.Name).SequenceEqual(
563+
methodSymbol.Parameters.Select((p) => p.Name)));
561564
return constructorInfo ?? throw new InvalidOperationException(
562565
$"Constructor not found for type: {type.Name}");
563566
}

src/NodeApi/Interop/JSPropertyDescriptorListOfT.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,14 @@ public TDerived AddMethod(
215215
attributes,
216216
data);
217217
}
218+
219+
public TDerived AddMethod(
220+
string name,
221+
JSCallbackDescriptor callbackDescriptor,
222+
JSPropertyAttributes attributes = JSPropertyAttributes.DefaultMethod)
223+
{
224+
Properties.Add(JSPropertyDescriptor.Function(
225+
name, callbackDescriptor.Callback, attributes, callbackDescriptor.Data));
226+
return (TDerived)(object)this;
227+
}
218228
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.JavaScript.NodeApi.TestCases;
5+
6+
[JSExport]
7+
public class Overloads
8+
{
9+
public Overloads()
10+
{
11+
}
12+
13+
public Overloads(int intValue)
14+
{
15+
IntValue = intValue;
16+
}
17+
18+
public Overloads(string stringValue)
19+
{
20+
StringValue = stringValue;
21+
}
22+
23+
public Overloads(int intValue, string stringValue)
24+
{
25+
IntValue = intValue;
26+
StringValue = stringValue;
27+
}
28+
29+
public Overloads(ITestInterface obj)
30+
{
31+
StringValue = obj.Value;
32+
}
33+
34+
public int? IntValue { get; private set; }
35+
36+
public string? StringValue { get; private set; }
37+
38+
public void SetValue(int intValue)
39+
{
40+
IntValue = intValue;
41+
}
42+
43+
public void SetValue(string stringValue)
44+
{
45+
StringValue = stringValue;
46+
}
47+
48+
public void SetValue(int intValue, string stringValue)
49+
{
50+
IntValue = intValue;
51+
StringValue = stringValue;
52+
}
53+
54+
public void SetValue(ITestInterface obj)
55+
{
56+
StringValue = obj.Value;
57+
}
58+
59+
// Method with overloaded name in C# is given a non-overloaded export name.
60+
[JSExport("setDoubleValue")]
61+
public void SetValue(double doubleValue)
62+
{
63+
IntValue = (int)doubleValue;
64+
}
65+
}

0 commit comments

Comments
 (0)