unoplatform / Uno.Wasm.Bootstrap

A simple nuget package to run C# code in a WASM-compatible browser
Other
375 stars 57 forks source link

Blazor/Microsoft.JSInterop compatibility #710

Open mmarinchenko opened 1 year ago

mmarinchenko commented 1 year ago

Issue background

We are experimenting with cross-platform 3D visualization running inside Uno.WinUI shell. This issue covers WebAssebly case, so if we forget about other platforms, some kind of JavaScript 3D engine, such as Babylon.js, could be used as a workaround. But ideally, we would like to go Uno-way as much as possible, so the 3D engine should be cross-platform and run on .NET.

One potentially possible third-party option is Plain Concepts' Evergine. Like most Blazor-related libraries, Evergine uses Microsoft.AspNetCore.Components.WebAssembly package, which, in turn, relies on Microsoft.JSInterop to interact with JavaScript API. As a result, we basically need a JavaScript infrastructure that would be compatible with both Uno.Wasm.Bootstrap and Microsoft.JSInterop.

Please note that Evergine is just an example to provide the issue background. We are not part of Evergine team or any other Plain Concepts affiliate. Feel free to provide us with any other cross-platform .NET 3D visualization solution compatible with Uno, if you have one.

Why might this be important for Uno?

Number of .NET libraries, ported to WebAssembly, is constantly growing.

Some of them require interaction with JavaScript API. Microsoft.JSInterop, for obvious reasons, is used by default unless they assume compatibility specifically with Uno.

Some of them have a proprietary license (like Evergine still has) and cannot be modified by third party. The lack of compatibility between the two technologies may slow down Uno/.NET adoption in WebAssembly world.

Our investigations

dotnet.js bundled with Uno has the following functions (see 0004-restore-jsinterop-invokejsunmarshalled.patch):

function mono_wasm_invoke_js_blazor(e, t, r, n, o) {
    try {
        const e = globalThis.Blazor;
        if (!e)
            throw new Error("The blazor.webassembly.js library is not loaded.");
        return e._internal.invokeJSFromDotNet(t, r, n, o)
    } catch (t) {
        const r = t.message + "\n" + t.stack, n = mono_wasm_new_root();
        return js_string_to_mono_string_root(r, n), n.copy_to_address(e), n.release(), 0
    }
}

function mono_wasm_invoke_js_unmarshalled(e, t, r, n, o) {
    try {
        const e = conv_string(t), s = globalThis.DotNet;
        if (!s)
            throw new Error("The Microsoft.JSInterop.js library is not loaded.");
        const _ = undefined;
        return s.jsCallDispatcher.findJSFunction(e).call(null, r, n, o)
    } catch (t) {
        const r = t.message + "\n" + t.stack, n = wrapped_c_functions.mono_wasm_string_from_js(r);
        return Module.setValue(e, n, "i32"), 0
    }
}

The first mentioned function invokes Blazor._internal.invokeJSFromDotNet(...) which isn't present in Uno. It could be added in an additional JS script as EmbeddedResource, but the problem is that the Blazor._internal object, on the other hand, is present in Uno and written in a non-extensible way: it uses direct assignment of anonymous object, which overwrites any additions.

We've tried to customize this code to add Blazor._internal.invokeJSFromDotNet(...), but it isn't clear to us which implementation should be used here. The ASP.NET Core implementation (as of version 6.0) uses DotNet.jsCallDispatcher object, which is also present in Uno, but has a completely different implementation (including DotNet.jsCallDispatcher.findJSFunction(...) which is invoked by the second mentioned function from dotnet.js bundled with Uno).

Neither blazor.webassembly.js nor Microsoft.JSInterop.js can be used directly with Uno, because this breaks everything. So what should we do?

Additional notes

https://github.com/mmarinchenko/UnoWasmApp repository contains empty Uno application with Wasm head. Last commit adds the Microsoft.AspNetCore.Components.WebAssembly package and the Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder.CreateDefault() call to reproduce the issue. No other changes were made, except for some minor format and package version fixes.

P.S. This could be a success story in the future if we solve this issue!

jeromelaban commented 1 year ago

If I'm not mistaken, this particular set of API is now discouraged as it is not performant, and it is best to use JSImport/JSExport attributes, which Uno supports properly. Are you able to use this approach instead?