Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bypasses deserialization of return value when 'InvokeVoidAsync' is used. #22056

Closed
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal static class InternalCalls
// We're passing asyncHandle by ref not because we want it to be writable, but so it gets
// passed as a pointer (4 bytes). We can pass 4-byte values, but not 8-byte ones.
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern string InvokeJSMarshalled(out string exception, ref long asyncHandle, string functionIdentifier, string argsJson);
public static extern string InvokeJSMarshalled(out string exception, ref long asyncHandle, string functionIdentifier, string argsJson, bool treatReturnAsVoid);

[MethodImpl(MethodImplOptions.InternalCall)]
public static extern TRes InvokeJSUnmarshalled<T0, T1, T2, TRes>(out string exception, string functionIdentifier, [AllowNull] T0 arg0, [AllowNull] T1 arg1, [AllowNull] T2 arg2);
Expand Down
10 changes: 5 additions & 5 deletions src/Components/WebAssembly/JSInterop/src/WebAssemblyJSRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ namespace Microsoft.JSInterop.WebAssembly
public abstract class WebAssemblyJSRuntime : JSInProcessRuntime
{
/// <inheritdoc />
protected override string InvokeJS(string identifier, string argsJson)
protected override string InvokeJS(string identifier, string argsJson, bool treatReturnAsVoid)
{
var noAsyncHandle = default(long);
var result = InternalCalls.InvokeJSMarshalled(out var exception, ref noAsyncHandle, identifier, argsJson);
var result = InternalCalls.InvokeJSMarshalled(out var exception, ref noAsyncHandle, identifier, argsJson, treatReturnAsVoid);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like from the comments in the InvokeJSMarshalled method that there's some magic that happens here that passes this new boolean arg to JS? Seems like it should just work? Or do we need to be passing the treatReturnAsVoid parameter as part of the argsJson and somehow extracting it on the JS side?

return exception != null
? throw new JSException(exception)
: result;
}

/// <inheritdoc />
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson, bool treatReturnAsVoid)
{
InternalCalls.InvokeJSMarshalled(out _, ref asyncHandle, identifier, argsJson);
InternalCalls.InvokeJSMarshalled(out _, ref asyncHandle, identifier, argsJson, treatReturnAsVoid);
}

protected override void EndInvokeDotNet(DotNetInvocationInfo callInfo, in DotNetInvocationResult dispatchResult)
Expand All @@ -39,7 +39,7 @@ protected override void EndInvokeDotNet(DotNetInvocationInfo callInfo, in DotNet
// We pass 0 as the async handle because we don't want the JS-side code to
// send back any notification (we're just providing a result for an existing async call)
var args = JsonSerializer.Serialize(new[] { callInfo.CallId, dispatchResult.Success, resultOrError }, JsonSerializerOptions);
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args);
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args, false);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,12 @@ export module DotNet {
*
* @param identifier Identifies the globally-reachable function to invoke.
* @param argsJson JSON representation of arguments to be passed to the function.
* @param treatReturnAsVoid Ignore any result that is returned and treat as Void.
* @returns JSON representation of the invocation result.
*/
invokeJSFromDotNet: (identifier: string, argsJson: string) => {
invokeJSFromDotNet: (identifier: string, argsJson: string, treatReturnAsVoid: boolean) => {
const result = findJSFunction(identifier).apply(null, parseJsonWithRevivers(argsJson));
return result === null || result === undefined
return result === null || result === undefined || treatReturnAsVoid === true
? null
: JSON.stringify(result, argReplacer);
},
Expand Down
5 changes: 3 additions & 2 deletions src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public abstract class JSInProcessRuntime : JSRuntime, IJSInProcessRuntime
[return: MaybeNull]
public TValue Invoke<TValue>(string identifier, params object[] args)
{
var resultJson = InvokeJS(identifier, JsonSerializer.Serialize(args, JsonSerializerOptions));
var resultJson = InvokeJS(identifier, JsonSerializer.Serialize(args, JsonSerializerOptions), typeof(TValue) == typeof(VoidReturn));
if (resultJson is null)
{
return default;
Expand All @@ -35,7 +35,8 @@ public TValue Invoke<TValue>(string identifier, params object[] args)
/// </summary>
/// <param name="identifier">The identifier for the function to invoke.</param>
/// <param name="argsJson">A JSON representation of the arguments.</param>
/// <param name="treatReturnAsVoid">Ignore any result that is returned and treat as Void.</param>
/// <returns>A JSON representation of the result.</returns>
protected abstract string? InvokeJS(string identifier, string? argsJson);
protected abstract string? InvokeJS(string identifier, string? argsJson, bool treatReturnAsVoid);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static void InvokeVoid(this IJSInProcessRuntime jsRuntime, string identif
throw new ArgumentNullException(nameof(jsRuntime));
}

jsRuntime.Invoke<object>(identifier, args);
jsRuntime.Invoke<VoidReturn>(identifier, args);
}
}
}
8 changes: 4 additions & 4 deletions src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToke
var argsJson = args?.Any() == true ?
JsonSerializer.Serialize(args, JsonSerializerOptions) :
null;
BeginInvokeJS(taskId, identifier, argsJson);
BeginInvokeJS(taskId, identifier, argsJson, typeof(TValue) == typeof(VoidReturn));

return new ValueTask<TValue>(tcs.Task);
}
Expand All @@ -138,7 +138,8 @@ private void CleanupTasksAndRegistrations(long taskId)
/// <param name="taskId">The identifier for the function invocation, or zero if no async callback is required.</param>
/// <param name="identifier">The identifier for the function to invoke.</param>
/// <param name="argsJson">A JSON representation of the arguments.</param>
protected abstract void BeginInvokeJS(long taskId, string identifier, string? argsJson);
/// <param name="treatReturnAsVoid">Ignore any result that is returned and treat as Void.</param>
protected abstract void BeginInvokeJS(long taskId, string identifier, string? argsJson, bool treatReturnAsVoid);

/// <summary>
/// Completes an async JS interop call from JavaScript to .NET
Expand All @@ -165,8 +166,7 @@ internal void EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonRe
if (succeeded)
{
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);

var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions);
var result = resultType != typeof(VoidReturn) ? JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions) : null;
TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result);
}
else
Expand Down
6 changes: 3 additions & 3 deletions src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string
throw new ArgumentNullException(nameof(jsRuntime));
}

await jsRuntime.InvokeAsync<object>(identifier, args);
await jsRuntime.InvokeAsync<VoidReturn>(identifier, args);
}

/// <summary>
Expand Down Expand Up @@ -91,7 +91,7 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string
throw new ArgumentNullException(nameof(jsRuntime));
}

await jsRuntime.InvokeAsync<object>(identifier, cancellationToken, args);
await jsRuntime.InvokeAsync<VoidReturn>(identifier, cancellationToken, args);
}

/// <summary>
Expand Down Expand Up @@ -134,7 +134,7 @@ public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string
using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout);
var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None;

await jsRuntime.InvokeAsync<object>(identifier, cancellationToken, args);
await jsRuntime.InvokeAsync<VoidReturn>(identifier, cancellationToken, args);
}
}
}
11 changes: 11 additions & 0 deletions src/JSInterop/Microsoft.JSInterop/src/VoidReturn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Threading.Tasks;

namespace Microsoft.JSInterop
{
/// <summary>
/// Used as a type for the <see cref="TaskCompletionSource{TResult}"/> that represents nothing should be returned.
/// </summary>
internal sealed class VoidReturn
{
}
}