24

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:

  • Visual Studio 2022
  • the workload "wasm-tools" (.NET WebAssembly build tools)

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.

4 Answers 4

22

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:

  1. Create the library project:
dotnet new classlib
  1. Create the library code, we'll use something simple that avoids any problems marshalling things like javascript strings, so in the 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

  1. Add a nuget.config
dotnet new nugetconfig
  1. In the nuget.config add the reference to allow the experimental package to be downloaded. You can also change the package download location to avoid it adding experimental packages to your global nuget location. Your 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>
  1. Add the package references for the compiler to your project's 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>
  1. Publish your project to wasm:
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 enter image description here

Sign up to request clarification or add additional context in comments.

10 Comments

They have a instruction markdown on how to do this now: github.com/dotnet/runtimelab/blob/…
18 MB WASM file being generated for same code above returning integer. Seems like it is because of other functions being exported, does anyone know?
There's a few things that contribute to the size. 1) this is a debug build so includes debug information and the Wasm is not optimized. Note that the LLVM backend for RyuJIT used in the NativeAOT-LLVM project does not support the release mode of RyuJIT which would also reduce the size. 2) Reflection meta data is included to support reflection. You can remove that with reflection free mode. 3) The required parts of the dotnet runtime are included - there is a "zerosharp" way to get a tiny wasm file, but there is no runtime that way.
thanks, i am pure JS guy and new to C# and web assembly world. Any link you can suggest to get better understanding of what you mentioned above?
@GrumpyRodriguez I contribute to the NativeAOT-LLVM project and there is some documentation there github.com/dotnet/runtimelab/blob/feature/NativeAOT-LLVM/docs/…:
|
1

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

Comments

0

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.

2 Comments

How do you call C# methods from JS with this method?
@Qwertie You can use JSExport (learn.microsoft.com/en-us/aspnet/core/client-side/…) to expose a C# method for JS to call. Or you can have the C# subscribe to an event or custom event that is triggered from JS. You would JSImport a javascript function exposing addEventListener, call that from C# passing a C# action method, and then trigger the event using dispatchEvent from JS which will effectively call that C# action method: github.com/SerratedSharp/CSharpWasmRecipes#events
0

C / Emscripten small example

I'm a C# developer and also wanted to write a small WASM library. I tried using C# with NativeAOT LLVM but the WASM file was very large even with all of the trimming options set.

And so I decided to try writing a C program and use Emscripten to create a WASM file and a javascript file that loads it and runs it. Below is a minimal working example of code that does some basic things that others may want to do (when starting from zero knowledge like myself).

I have assumed that you have followed the install instructions for Emscipten and Ruby etc. I used Visual Studio for coding and if you also do this, then just be aware that you will get errors showing in VS if you compile the code in VS, but when you compile it via Emsctipten, then all is well. You can set some VS configuration to point to the additional c files of Emscripten which helps with some errors, but dont get too hung up on them.

What the example does
an html web page will load the wasm via some js and then call a wasm function to get a string. This string is then displayed in the web page.

The string is made up of

  • some fixed text

  • the domain name (comes from wasm calling out to js to get the domain name)

  • the current time (epoch time). (This comes from using a standard emscripten js library to get the js Date.Now and return it to wasm)

So it covers both getting strings from js to wasm and returning strings from wasm to js, plus shows the use of built-in emscripten functions.

C Code

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <emscripten/html5.h>
#include <emscripten.h>

//------------------------------------------------------------------------
// Function to return a pointer to the string that has the domain name from a js call
//------------------------------------------------------------------------
char* GetDomainName() {

    return (char*)EM_ASM_PTR({
        var jsString = window.location.hostname;
        //var jsString = Module['currentURL'];
        var lengthBytes = lengthBytesUTF8(jsString) + 1;
        var stringOnHeap = _malloc(lengthBytes);
        stringToUTF8(jsString, stringOnHeap, lengthBytes);
        return stringOnHeap;
        });
}


//------------------------------------------------------------------------
// Function to return a pointer to the string 
//------------------------------------------------------------------------
char* get_string() {

    char* url = GetDomainName();

    // built-in function to get the current time in milliseconds (epoch time)
    double now = emscripten_date_now();
    char output[100];

    snprintf(output, 100, "Hello, its a great day! %s %f",url, now);

    // Free the memory allocated by GetDomainName
    free(url);                                                  

    // strdup is a standard C function that allocates memory for the string and copies it to the heap
    char* duplicate = strdup(output);
    return duplicate;                                           
}

Compiling the C code with Emscripten (from windows)

NB. this command is the windows version. To use from linux needs ./emcc syntax

emcc -oz -sEXPORTED_FUNCTIONS=_get_string,_free -sENVIRONMENT=web -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='$stringToNewUTF8' generator.c -o generator.js

The EXPORTED_FUNCTIONS make the C function(s) available to call in the wasm file. They are the C function name prefixed with an underscore. _free is a built-in function from emscripten so you dont need to write one yourself to free memory (no garage collection here :-) )

This will generate 2 files

  • Generator.wasm (25KB)

  • Generator.js (54KB)

NB. The Generator.js file will have js code that is both standard load and run code and also has additional js functions that come from our C code example above

The web page

<!DOCTYPE html>
<html>
<head>
    <title>Wasm String Example</title>

</head>
<body>
    <div id="output"></div>
    <script>
        var Module = {};

        Module.onRuntimeInitialized = () => {
            
            let ptr = Module._get_string();         // call wasm function to load the string into memory and retrun a pointer to it
            var str = UTF8ToString(ptr);
            Module._free(ptr);                      // Free the allocated memory

            // Display the string in the HTML
            document.getElementById('output').textContent = str;
        }
    </script>
    <script async="" type="text/javascript" src="Generator.js"></script>

</body>
</html>

How to run it

CORS will block the wasm file in the browser unless run via a web server. the emscripten install requires a python install and it comes with a simple web server. so you can run it with

(from a command prompt)
>python -m http.server

Then from the browser to to the web page. e.g.
http://localhost:8000/WasmTestPage.html

and you should see something like

Hello, its a great day! localhost 1745985924336.000000

and the time will change every time you refresh the page.

Summary
remember that strings are not natively available in wasm so we have to pass arrays of chars to and from wasm memory and then return a pointer the memory location, for the caller to then go and access the bytes from the memory location. Once finished with the memory, then you must free the memory to avoid memory leaks.
For C to put string data into wasm memory, then they need to be on the heap as anything on the stack will not be there as the function ends. This is why any string constants (arrays and const *) must be copied to heap memory for use.

You can use your own C free function or use the built-in emscripten _free function as per the example. All of the js interop is added to the Module variable in js. You can write your own C code to convert the pointer based string into a local variable, but its easier to use the emscipten built-in functions such as stringToUTF8 and UTF8ToString as they handle a few complexities.

Don't forget to free your heap memory appropriately and I hope this helps someone else. Remember this is my first C code as I am a C# developer.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.