Reputation: 1781
I need to compile a C# project to WebAssembly and be able to call some methods from JavaScript.
I want to use it in an old ASP.NET MVC 4 application that needs to add some new features and I prefer to use C# instead JavaScript/TypeScript.
Ideally I would like to compile to WebAssembly using .Net 6 but I can use any other alternative.
I'm running .Net 6 on Windows 10 Version 21H1 (OS Build 19043.1415)
I've installed:
But every time I search for a tutorial, example, etc, about how to use the .NET WebAssembly build tools the results are about Blazor.
I've read this tutorial but I can't find the mono-wasm compiler (and like I said above I would like to use .Net 6 to compile whenever possible.)
Can anyone please help me with this?
Thank you.
Upvotes: 24
Views: 13498
Reputation: 38392
I have been using Uno.Wasm.Bootstrap for awhile as a straightforward way to compile a C# assembly to a WASM package. You add the nuget package to a console project and make a couple minor changes: https://github.com/unoplatform/Uno.Wasm.Bootstrap/blob/main/doc/using-the-bootstrapper.md
This is separate from the larger Uno Platform and can be used without using their UI platform. It is just the tooling needed to compile your assembly to a simple WASM package and the JavaScript needed to load it (or "bootstrap" it) into the browser. The nice thing is because they leverage this on the mature Uno platform, the tooling has been exercised pretty thoroughly.
It produces a folder containing the WASM assembly and all the JavaScript/etc. static files that do the work to retrieve and load the WASM assembly. So all the code in your console app runs client side in the browser. The area is still evolving and there's a few different ways to call JavaScript from C# and vice versa. My jQuery WASM wrapper uses alot of WebAssemblyRuntime.InvokeJS(@$
to call JavaScript and ultimately interact with the DOM, but depending what you are trying to accomplish there are easier ways to do this in .NET 7 using import and export attributes and n C# methods to expose them to JavaScript.
I use MSBUILD tasks to copy the WASM distribution folder into my MVC project to be served as additional static files. Alternatively you would just host the static files as a separate static site. Then reference them from my layout.cshtml, the same way you'd reference a JavaScript file.
They also document an Embedded Mode that packages it slightly differently to simplify loading the WASM package in some scenarios. I have not tested this yet since it's newer, but seems like it may be more appropriate where it's being integrated into an existing site.
Alot of the use cases for WASM seem to be SPA's but like you I just wanted to use it as client side logic for my traditional MVC web app, instead of JavaScript. At some levels you usually need to generate or call JavaScript to interop with the DOM.
There are now some WASM libraries out there using this technology that act as C# wrappers for things like jQuery so that you can skip past dealing with the JavaScript interop layer and work in C# to access and manipulate the DOM/HTML. Of course the JS interop layer is still there, but the wrapper is already built out so you don't have to write any JS.
Upvotes: 0
Reputation: 839
There is the experimental NativeAOT-LLVM (https://github.com/dotnet/runtimelab/tree/feature/NativeAOT-LLVM). It is not an official Microsoft WebAssembly compiler, its supported by the community, but .Net 6 is available. First, and this only works on Windows, you need to install and activate emscripten. I wont cover installing emscripten here, but you can read https://emscripten.org/docs/getting_started/downloads.html.
To create and run a dotnet c# library:
dotnet new classlib
Class1.cs
file add[System.Runtime.InteropServices.UnmanagedCallersOnly(EntryPoint = "Answer")]
public static int Answer()
{
return 41;
}
This will create a function, Answer
that can be called from outside managed code, i.e. from Javascript
dotnet new nugetconfig
nuget.config
should look like:<?xml version="1.0" encoding="utf-8"?>
<configuration>
<config>
<add key="globalPackagesFolder" value=".packages" />
</config>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
csproj
file so that it ends with this: <ItemGroup>
<PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM" Version="7.0.0-*" />
<PackageReference Include="runtime.win-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="7.0.0-*" />
</ItemGroup>
</Project>
dotnet publish /p:NativeLib=Static /p:SelfContained=true -r browser-wasm -c Debug /p:TargetArchitecture=wasm /p:PlatformTarget=AnyCPU /p:MSBuildEnableWorkloadResolver=false /p:EmccExtraArgs="-s EXPORTED_FUNCTIONS=_Answer%2C_NativeAOT_StaticInitialization -s EXPORTED_RUNTIME_METHODS=cwrap" --self-contained
This will build the project referencing the browser-wasm runtime. MSBuildEnableWorkloadResolver
stops the build process checking for Mono's wasm-tools Visual Studio workload which we are not using here. (Mono is a different compiler and runtime, which I believe is getting similar support for .net 7). EmccExtraArgs
allows us to add parameters to emscripten's emcc
and we need that to export the two function we will call from Javascript: Answer
- this is our library function, and NativeAOT_StaticInitialization
this is called once per lifetime of the wasm module to initialize the runtime. Note the additional underscores in front of the names. The compilation takes a while, but when finished you should have a subfolder bin\x64\Debug\net6.0\browser-wasm\native
where you will find the wasm, some html, and some javascript. In the html file at the end, before the closing body
tag, initialize the runtime and call your function with:
<script>
Module.onRuntimeInitialized = _ => {
const corertInit = Module.cwrap('NativeAOT_StaticInitialization', 'number', []);
corertInit();
const answer = Module.cwrap('Answer', 'number', []);
console.log(answer());
};
</script>
Then server that up with the web server of your choosing, browse to the page and check the console where if everything has gone to plan, and the stars align (this is experimental), you should see
Upvotes: 22
Reputation: 519
I recently came across : https://github.com/Elringus/DotNetJS
Looks very interesting and closer to how I would want to use c# in the web browser. I will be looking at this in the next few months
Upvotes: 1