Skip to content

Unify Blazor WebAssembly JS interop implementation #24648

@SteveSandersonMS

Description

@SteveSandersonMS

Summary

This is a prerequisite for completing the JS object reference work.

Rather than rely on specific combinations of parameters and invocation semantics being hardcoded in the dotnet.wasm runtime, we should replace the underlying transport with a simpler, more flexible mechanism. This will give us the ability to extend it for JS object reference, and any other enhancements we want in the future, without needing more runtime changes.

Motivation

Today, Blazor WebAssembly JS interop is achieved via two InternalCall methods, whose JavaScript implementations are hardcoded inside the runtime here and here. These two JS methods declare specific combinations of parameters for sync vs async calls and also know about marshalled vs unmarshalled calls.

Having this logic in the runtime limits flexibility for us when working in the aspnetcore repo. For example, as part of the JS object reference work, we'll need to add another parameter such as targetObjectId, but:

  • It's inconvenient to thread this extra param through lots of layers, including working across repos
  • In the case of unmarshalled interop, it's not clear we even can add another parameter. Historically, the Emscripten internal calls were limited to a maximum of 5 params, and mono_wasm_invoke_js_unmarshalled already uses 5 (not sure if the param count limit has been raised since). We could work around this in the marshalled case by repurposing argsJson to hold arbitrary framework-controlled metadata with the true args embedded inside it, but there's no such flexibility for unmarshalled interop.

Rather than fighting this every time we want to extend JS interop, I propose a different and simpler way of transporting the interop calls, which unifies the sync/async cases and the marshalled/unmarshalled cases, plus lets us pass through arbitrary unlimited metadata which can be changed purely within the aspnetcore repo without needing runtime changes. To enable this, I've already made and merged a PR in the runtime repo. What's left is for us to wire this up to Blazor.

Goals

  • Be able to add an extra param for JS object reference in all interop forms (sync/async/unmarshalled)
  • Be free to make other arbitrary changes to what info we provide, and what are the semantics of the calls, in the future without needing runtime changes. Example: to add another flag saying the return value should not be JSON serialized.
  • Exactly retain the semantics of sync/async/unmarshalled interop calls (e.g., parameter formats, async completion flow, error handling), plus no meaningful extra overhead on unmarshalled calls.
  • Don't change any public APIs. This is an internal implementation change only.

Non-goals

  • Retaining back-compat with any code relying on the private InternalCall methods (e.g., through private reflection).

Scenarios

This should work with all the flavors of JS interop: sync, async, unmarshalled.

Risks

  • We might accidentally change some very subtle detail about the semantics of the calls. TBH I think we won't because this should be a pretty mechanical change.

Interactions with other features

Although JS interop is used extensively throughout Blazor and by user code, since we intend to retain the existing semantics exactly, there should be no impact on any other feature.

This does not affect or touch JS interop in Blazor Server in any way at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DoneThis issue has been fixedarea-blazorIncludes: Blazor, Razor Componentsfeature-blazor-wasmThis issue is related to and / or impacts Blazor WebAssembly

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions