Skip to content

Commit dd1bcef

Browse files
authored
Switch to main thread for interface callback (microsoft#144)
1 parent ddca056 commit dd1bcef

File tree

8 files changed

+216
-78
lines changed

8 files changed

+216
-78
lines changed

src/NodeApi.DotNetHost/JSInterfaceMarshaller.cs

+23-53
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,7 @@ FieldBuilder CreateDelegateField(MethodInfo method)
160160

161161
FieldInfo delegateField = implementationType.GetField(
162162
fieldName, BindingFlags.NonPublic | BindingFlags.Static)!;
163-
delegateField.SetValue(
164-
null, marshaller.BuildToJSMethodExpression(method).Compile());
163+
delegateField.SetValue(null, marshaller.GetToJSMethodDelegate(method));
165164
}
166165

167166
return implementationType;
@@ -232,28 +231,21 @@ private static void BuildPropertyImplementation(
232231
ILGenerator il = getMethodBuilder.GetILGenerator();
233232

234233
/*
235-
* return this._get_property.DynamicInvoke(new object[] { Value });
234+
* return this.DynamicInvoke._get_property, new object[] { Value });
236235
*/
237236

237+
il.Emit(OpCodes.Ldarg_0); // this
238+
238239
// Load the static field for the delegate that implements the method by marshalling to JS.
239240
il.Emit(OpCodes.Ldsfld, getDelegateField!);
240241

241242
// Create an array to hold the arguments passed to the delegate invocation.
242243
il.Emit(OpCodes.Ldc_I4_1);
243244
il.Emit(OpCodes.Newarr, typeof(object));
244245

245-
// Store the value from the Value property in the first array slot.
246-
il.Emit(OpCodes.Dup); // Duplicate the array reference on the stack.
247-
il.Emit(OpCodes.Ldc_I4_0);
248-
il.Emit(OpCodes.Ldarg_0); // this
249-
PropertyInfo valueProperty = typeof(JSInterface).GetProperty(
250-
"Value", BindingFlags.NonPublic | BindingFlags.Instance)!;
251-
il.Emit(OpCodes.Call, valueProperty.GetMethod!);
252-
il.Emit(OpCodes.Box, typeof(JSValue));
253-
il.Emit(OpCodes.Stelem_Ref);
254-
255246
// Invoke the delegate.
256-
il.Emit(OpCodes.Callvirt, typeof(Delegate).GetMethod(nameof(Delegate.DynamicInvoke))!);
247+
il.Emit(OpCodes.Callvirt, typeof(JSInterface).GetMethod(
248+
nameof(Delegate.DynamicInvoke), BindingFlags.NonPublic | BindingFlags.Instance)!);
257249

258250
// Return the result, casting to the return type.
259251
if (property.PropertyType.IsValueType)
@@ -284,26 +276,18 @@ private static void BuildPropertyImplementation(
284276
ILGenerator il = setMethodBuilder.GetILGenerator();
285277

286278
/*
287-
* return this._set_property.DynamicInvoke(new object[] { Value, value });
279+
* return this.DynamicInvoke(_set_property, new object[] { Value, value });
288280
*/
289281

282+
il.Emit(OpCodes.Ldarg_0); // this
283+
290284
// Load the static field for the delegate that implements the method by marshalling to JS.
291285
il.Emit(OpCodes.Ldsfld, setDelegateField!);
292286

293287
// Create an array to hold the arguments passed to the delegate invocation.
294288
il.Emit(OpCodes.Ldc_I4_2);
295289
il.Emit(OpCodes.Newarr, typeof(object));
296290

297-
// Store the value from the Value property in the first array slot.
298-
il.Emit(OpCodes.Dup); // Duplicate the array reference on the stack.
299-
il.Emit(OpCodes.Ldc_I4_0);
300-
il.Emit(OpCodes.Ldarg_0); // this
301-
PropertyInfo valueProperty = typeof(JSInterface).GetProperty(
302-
"Value", BindingFlags.NonPublic | BindingFlags.Instance)!;
303-
il.Emit(OpCodes.Call, valueProperty.GetMethod!);
304-
il.Emit(OpCodes.Box, typeof(JSValue));
305-
il.Emit(OpCodes.Stelem_Ref);
306-
307291
// Store the set argument "value" in the second array slot.
308292
il.Emit(OpCodes.Dup); // Duplicate the array reference on the stack.
309293
il.Emit(OpCodes.Ldc_I4_1);
@@ -312,7 +296,8 @@ private static void BuildPropertyImplementation(
312296
il.Emit(OpCodes.Stelem_Ref);
313297

314298
// Invoke the delegate.
315-
il.Emit(OpCodes.Callvirt, typeof(Delegate).GetMethod(nameof(Delegate.DynamicInvoke))!);
299+
il.Emit(OpCodes.Callvirt, typeof(JSInterface).GetMethod(
300+
nameof(Delegate.DynamicInvoke), BindingFlags.NonPublic | BindingFlags.Instance)!);
316301

317302
// Remove unused return value from the stack.
318303
il.Emit(OpCodes.Pop);
@@ -357,17 +342,7 @@ void EmitArgs()
357342
il.Emit(OpCodes.Ldc_I4, 1 + parameters.Length);
358343
il.Emit(OpCodes.Newarr, typeof(object));
359344

360-
// Store the value from the Value property in the first array slot.
361-
il.Emit(OpCodes.Dup); // Duplicate the array reference on the stack.
362-
il.Emit(OpCodes.Ldc_I4_0);
363-
il.Emit(OpCodes.Ldarg_0); // this
364-
PropertyInfo valueProperty = typeof(JSInterface).GetProperty(
365-
"Value", BindingFlags.NonPublic | BindingFlags.Instance)!;
366-
il.Emit(OpCodes.Call, valueProperty.GetMethod!);
367-
il.Emit(OpCodes.Box, typeof(JSValue));
368-
il.Emit(OpCodes.Stelem_Ref);
369-
370-
// Store the arguments in the remaining array slots.
345+
// Store the arguments in the array, leaving index 0 for the JS `this` value.
371346
for (int i = 0; i < parameters.Length; i++)
372347
{
373348
il.Emit(OpCodes.Dup); // Duplicate the array reference on the stack.
@@ -384,12 +359,14 @@ void EmitArgs()
384359
}
385360
}
386361

362+
il.Emit(OpCodes.Ldarg_0); // this
363+
387364
if (method.IsGenericMethodDefinition)
388365
{
389366
/*
390-
* return JSMarshaller.Current.GetToJSMethodDelegate(
391-
* MethodBase.GetCurrentMethod().MakeGenericMethod(new Type[] { typeArgs... }))
392-
* .DynamicInvoke(new object[] { Value, args... });
367+
* return this.DynamicInvoke(JSMarshaller.Current.GetToJSMethodDelegate(
368+
* MethodBase.GetCurrentMethod().MakeGenericMethod(new Type[] { typeArgs... })),
369+
* new object[] { Value, args... });
393370
*/
394371

395372
il.Emit(
@@ -422,32 +399,25 @@ void EmitArgs()
422399
il.Emit(
423400
OpCodes.Callvirt,
424401
typeof(JSMarshaller).GetInstanceMethod(nameof(JSMarshaller.GetToJSMethodDelegate)));
425-
426-
// Dynamically invoke the JS method delegate.
427-
EmitArgs();
428-
il.Emit(OpCodes.Call,
429-
typeof(Delegate).GetInstanceMethod(nameof(Delegate.DynamicInvoke)));
430402
}
431403
else
432404
{
433405
/*
434-
* return this._method.DynamicInvoke(new object[] { Value, args... });
406+
* return this.DynamicInvoke(_method, new object[] { Value, args... });
435407
*/
436408

437409
// TODO: Consider defining delegate types as needed for method signatures so the
438410
// delegate invocations are not "dynamic". It would avoid boxing value types.
439411

440412
// Load the static field for the delegate that implements the method by marshalling to JS.
441413
il.Emit(OpCodes.Ldsfld, delegateField);
442-
443-
EmitArgs();
444-
445-
// Invoke the delegate.
446-
il.Emit(
447-
OpCodes.Callvirt,
448-
typeof(Delegate).GetInstanceMethod(nameof(Delegate.DynamicInvoke)));
449414
}
450415

416+
EmitArgs();
417+
418+
// Invoke the delegate.
419+
il.Emit(OpCodes.Callvirt, typeof(JSInterface).GetMethod(
420+
nameof(Delegate.DynamicInvoke), BindingFlags.NonPublic | BindingFlags.Instance)!);
451421

452422
// Return the result, casting to the return type if necessary.
453423
if (method.ReturnType == typeof(void))

src/NodeApi.DotNetHost/JSMarshaller.cs

+63-1
Original file line numberDiff line numberDiff line change
@@ -2517,7 +2517,7 @@ private IEnumerable<Expression> BuildFromJSToStructExpressions(
25172517
/*
25182518
* StructName obj = default;
25192519
* obj.Property0 = (Property0Type)value["property0"];
2520-
* ...
2520+
* ...
25212521
* return obj;
25222522
*/
25232523
ParameterExpression objVariable = Expression.Variable(toType, "obj");
@@ -3047,4 +3047,66 @@ protected override Expression VisitParameter(ParameterExpression node) =>
30473047
// Do not recursively visit nested lambdas.
30483048
protected override Expression VisitLambda<T>(Expression<T> node) => node;
30493049
}
3050+
3051+
/// <summary>
3052+
/// Converts a lambda expression that takes a <see cref="JSValue"/> as the first parameter
3053+
/// to a lambda expression that references the the member value of a <see cref="JSInterface"/>
3054+
/// and automatically switches to the JS thread.
3055+
/// </summary>
3056+
public LambdaExpression MakeInterfaceExpression(LambdaExpression methodExpression)
3057+
{
3058+
ParameterExpression thisVariable = Expression.Variable(typeof(JSInterface), "this");
3059+
3060+
if (methodExpression.Parameters.Any((p) => p.IsByRef))
3061+
{
3062+
PropertyInfo valueProperty = typeof(JSInterface).GetProperty(
3063+
"Value", BindingFlags.Instance | BindingFlags.NonPublic)!;
3064+
3065+
// Ref/out parameters cannot be used within a lambda expression. So interface
3066+
// methods with ref/out parameters will not automatically switch to the JS thread.
3067+
// (The caller must be already on the JS thread.)
3068+
// TODO: Use temporary variables to avoid this limitation.
3069+
return Expression.Lambda(
3070+
Expression.Block(
3071+
methodExpression.Body.Type,
3072+
new Expression[]
3073+
{
3074+
Expression.Assign(
3075+
methodExpression.Parameters.First(),
3076+
Expression.Property(thisVariable, valueProperty)),
3077+
}.Concat(((BlockExpression)methodExpression.Body).Expressions)),
3078+
methodExpression.Name,
3079+
methodExpression.Parameters.Skip(1));
3080+
}
3081+
3082+
PropertyInfo valueReferenceProperty = typeof(JSInterface).GetProperty(
3083+
"ValueReference", BindingFlags.Instance | BindingFlags.NonPublic)!;
3084+
3085+
// Use the JSReference.Run() method to switch to the JS thread when operating on the value.
3086+
MethodInfo runMethod;
3087+
if (methodExpression.Body.Type == typeof(void))
3088+
{
3089+
runMethod = typeof(JSReference).GetInstanceMethod(
3090+
nameof(JSReference.Run), new[] { typeof(Action<JSValue>) });
3091+
}
3092+
else
3093+
{
3094+
runMethod = typeof(JSReference)
3095+
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
3096+
.Single((m) => m.Name == nameof(JSReference.Run) && m.IsGenericMethodDefinition)
3097+
.MakeGenericMethod(methodExpression.Body.Type);
3098+
}
3099+
3100+
// Build a lambda expression that moves the __this parameter from the original lambda
3101+
// to the inner lambda where the parameter is supplied by the Run() callback.
3102+
return Expression.Lambda(
3103+
Expression.Block(methodExpression.Body.Type, Expression.Call(
3104+
Expression.Property(thisVariable, valueReferenceProperty),
3105+
runMethod,
3106+
Expression.Lambda(
3107+
methodExpression.Body,
3108+
methodExpression.Parameters.Take(1)))),
3109+
methodExpression.Name,
3110+
methodExpression.Parameters.Skip(1));
3111+
}
30503112
}

src/NodeApi.Generator/ExpressionExtensions.cs

+2
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ private static string ToCS(
106106
ToCS(conditional.IfFalse, path, variables),
107107

108108
MemberExpression { NodeType: ExpressionType.MemberAccess } member =>
109+
member.Expression is ParameterExpression parameterExpression &&
110+
parameterExpression.Name == "this" ? member.Member.Name :
109111
(member.Expression != null ? WithParentheses(member.Expression, path, variables) :
110112
member.Member.DeclaringType!.FullName) + "." + member.Member.Name,
111113

src/NodeApi.Generator/ModuleGenerator.cs

+14-9
Original file line numberDiff line numberDiff line change
@@ -808,8 +808,6 @@ private static void GenerateInterfaceAdapter(
808808

809809
static string ReplaceMethodVariables(string cs) =>
810810
cs.Replace(typeof(JSValue).Namespace + ".", "")
811-
.Replace("JSValue __this, ", "")
812-
.Replace("__this", "Value")
813811
.Replace("__value", "value");
814812

815813
/*
@@ -836,7 +834,8 @@ static string ReplaceMethodVariables(string cs) =>
836834
LambdaExpression getterAdapter =
837835
_marshaller.BuildToJSPropertyGetExpression(property.AsPropertyInfo());
838836
s += "get";
839-
string cs = ReplaceMethodVariables(getterAdapter.ToCS());
837+
string cs = ReplaceMethodVariables(
838+
_marshaller.MakeInterfaceExpression(getterAdapter).ToCS());
840839
s += string.Join("\n", cs.Split('\n').Skip(1));
841840
}
842841

@@ -845,7 +844,8 @@ static string ReplaceMethodVariables(string cs) =>
845844
LambdaExpression setterAdapter =
846845
_marshaller.BuildToJSPropertySetExpression(property.AsPropertyInfo());
847846
s += "set";
848-
string cs = ReplaceMethodVariables(setterAdapter.ToCS());
847+
string cs = ReplaceMethodVariables(
848+
_marshaller.MakeInterfaceExpression(setterAdapter).ToCS());
849849
s += string.Join("\n", cs.Split('\n').Skip(1));
850850
}
851851

@@ -863,8 +863,8 @@ static string ReplaceMethodVariables(string cs) =>
863863
// ahead of time. This does not work in an AOT-compiled executable.
864864

865865
MethodInfo methodInfo = method.AsMethodInfo();
866-
s += $"public {ExpressionExtensions.FormatType(methodInfo.ReturnType)} " +
867-
$"{method.Name}<" +
866+
s += $"{ExpressionExtensions.FormatType(methodInfo.ReturnType)} " +
867+
$"{GetFullName(method)}<" +
868868
string.Join(", ", method.TypeParameters.Select((t) => t.Name)) +
869869
">(" + string.Join(", ", methodInfo.GetParameters().Select((p) =>
870870
$"{ExpressionExtensions.FormatType(p.ParameterType)} {p.Name}")) + ")";
@@ -879,17 +879,22 @@ static string ReplaceMethodVariables(string cs) =>
879879
string.Join(", ", typeArgs.Select((t) => $"typeof({t.Name})")) + ");";
880880
s += $"var jsMarshaller = {typeof(JSMarshaller).Namespace}." +
881881
$"{nameof(JSMarshaller)}.{nameof(JSMarshaller.Current)};";
882+
883+
s += $"return ValueReference.Run((__this) => {{";
884+
882885
s += $"return ({GetFullName(method.ReturnType)})" +
883886
$"jsMarshaller.{nameof(JSMarshaller.GetToJSMethodDelegate)}" +
884-
$"(currentMethod).DynamicInvoke(Value, " +
887+
$"(currentMethod).DynamicInvoke(__this, " +
885888
string.Join(", ", method.Parameters.Select((p) => p.Name)) + ");";
886889

890+
s += "});";
891+
887892
s += "}";
888893
}
889894
else
890895
{
891-
LambdaExpression methodAdapter =
892-
_marshaller.BuildToJSMethodExpression(method.AsMethodInfo());
896+
LambdaExpression methodAdapter = _marshaller.MakeInterfaceExpression(
897+
_marshaller.BuildToJSMethodExpression(method.AsMethodInfo()));
893898
s += ReplaceMethodVariables(methodAdapter.ToCS());
894899
}
895900
}

src/NodeApi/Interop/JSInterface.cs

+34-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System;
5+
46
namespace Microsoft.JavaScript.NodeApi.Interop;
57

68
/// <summary>
@@ -9,19 +11,43 @@ namespace Microsoft.JavaScript.NodeApi.Interop;
911
/// </summary>
1012
public abstract class JSInterface
1113
{
12-
private readonly JSReference _jsReference;
13-
1414
protected JSInterface(JSValue value)
1515
{
16-
_jsReference = new JSReference(value, isWeak: false);
16+
ValueReference = new JSReference(value, isWeak: false);
1717
}
1818

19-
public static JSValue? GetJSValue(object obj)
20-
=> (obj as JSInterface)?.Value;
19+
/// <summary>
20+
/// Gets the JS value for a .NET object, if the object is a proxy to a JS object that
21+
/// implements a .NET interface.
22+
/// </summary>
23+
public static JSValue? GetJSValue(object obj) => (obj as JSInterface)?.Value;
24+
25+
/// <summary>
26+
/// Gets a reference to the underlying JS value.
27+
/// </summary>
28+
protected JSReference ValueReference { get; }
29+
30+
/// <summary>
31+
/// Gets the underlying JS value.
32+
/// </summary>
33+
protected JSValue Value => ValueReference.GetValue()!.Value;
2134

2235
/// <summary>
23-
/// Gets the underlying JS value. (The property name is prefixed with `__` to avoid
24-
/// possible conflicts with interface
36+
/// Dynamically invokes an interface method JS adapter delegate after obtaining the JS `this`
37+
/// value from the reference.
2538
/// </summary>
26-
protected internal JSValue Value => _jsReference.GetValue()!.Value;
39+
/// <param name="interfaceMethod">Interface method JS adapter delegate.</param>
40+
/// <param name="args">Array of method arguments starting at index 1. Index 0 is reserved
41+
/// for the JS `this` value.</param>
42+
/// <remarks>
43+
/// This method is used by the dynamically-emitted interface marshalling code.
44+
/// </remarks>
45+
protected object? DynamicInvoke(Delegate interfaceMethod, object?[] args)
46+
{
47+
return ValueReference.Run((value) =>
48+
{
49+
args[0] = value;
50+
return interfaceMethod.DynamicInvoke(args);
51+
});
52+
}
2753
}

0 commit comments

Comments
 (0)