Elena Canorea
Communications Lead
WebAssembly (Wasm) is the not-so-new standard to run high-performance code in the browser, on the client side. You can think of it as an optimized Virtual Machine (VM), which translates intermediate code, bytecode, into the target machine architecture, all of it within a secured sandbox. It is not a replacement for JavaScript, it is not the silver bullet, but has allowed us to think of the Web as another desktop target for .NET, in the same way, Windows, macOS, Android or iOS, among other ones, already are.
You can right now run your .NET Core Console app embedded in an HTML file, make web requests, access the File System (FS), draw stuff with WebGL (the Web branch for OpenGL), and a lot of more things, just because it is the .NET runtime being executed in Wasm. There are still steps to be taken in order to make things smoother for us the Developers but, at the same time, begins to be mature enough for using along with projects.
Mono Wasm —as it was named some weeks ago, now it is .NET too—, provides Mono’s .NET Common Language Runtime (CLR), written mostly in C, compiled into Wasm. Emscripten, an Open Source project, provides the toolchain needed to turn LLVM backends into Wasm, along with a JavaScript bootstrap to initialize the environment.
With Wave Engine, our cross-platform 3D engine focused on industrial needs, we already tried to target the Web a few years ago, back with JSIL, but the state of art was simply not ready for us. We paused such an attempt until we could gain on performance and easiness to target the low-level drawing APIs.
Along with the development of Mono Wasm, and parties like Uno Platform using such as their underlying technology, we rethought our position and, after studying how WebGL could be achieved, we started enabling the Web as another platform we can, nowadays, target.
There are currently different paths if you want to target Wasm in .NET:
There are some other cool projects to turn Wasm modules into .NET assemblies —like Eric Sink’s wasm2cil—, for instance, which empower the community too.
Every .NET Developer is used to specify their target platform/s through CSPROJ files, but this differs a little bit with Wasm nowadays. The infrastructure is still not ready, and the entire toolchain is supplied in the shape of above NuGets: Mono runtime it-self, linker, JavaScript interop SDK, etc.
In order to push in the direction which would help Microsoft/Mono to build the below mentioned infrastructure, and enjoy the latest bits from the repo, you should:
We simply tell .NET compiler to target Wasm through the Project’s SDK attribute and, apart from this, there is no other fundamental difference with any other one:
<Project Sdk="Mono.WebAssembly.Sdk/0.2.0">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<None Remove="index.html" />
</ItemGroup>
</Project>
Basic structure of a .csproj file targeting Wasm
In order to the compiler detect where Mono.WebAssembly.Sdk NuGet is you will need to supply a NuGet.config file, establishing where such is in the hard drive.
With the goal of simplifying all these steps, we have set up a repo that provides everything ready to start writing your code: WasmApp1; along with an article on how to serve the resulting HTML page locally. We thought of it as if you could create a new project within Visual Studio, just choosing a Wasm template.
In the same way, you update your UI controls from a Xamarin.Forms app, you make the same with the DOM in a web page: you can modify the web UI from .NET and, also, invoke .NET stuff from the UI —like through a button click, for example.
WasmApp1 template comes with an HTML button that fires some code on the .NET side. Such button is created dynamically when the app starts, and we subscribe to its onclick event just after:
using (var document = (JSObject)Runtime.GetGlobalObject("document"))
using (var body = (JSObject)document.GetObjectProperty("body"))
using (var button = (JSObject)document.Invoke("createElement", "button"))
{
button.SetObjectProperty("innerHTML", "Click me!");
[…]
body.Invoke("appendChild", button);
}
The Runtime static class, located at WebAssembly namespace, provides some handy methods to get a reference from a JavaScript object and, wrapped through the JSObject facade, interact with it.
You may ask why document, body, and button are scoped with usings: there are actually two references for each one of them running at the same time in the browser: the one in pure JavaScript, and the one in .NET, and it is a good practice to not keep the second one alive more time than it is needed —it is indeed stored at the Emscripten layer, and can be gathered through BINDING.mono_wasm_object_registry at the browser’s Console. The fewer elements such contains, the better for performance.
Ideally, the community will provide NuGets so JSObject may not be visible at a higher level, as Kenneth Pouncey is already doing with his wasm-dom project —see the samples folder!
From Plain Concepts we maintain the low-level WebGL bindings, WebGL.NET (really creative naming heh?), thought for Wave Engine, which can be used by just adding the NuGet package to the above project. In our case, starting from Khronos’ IDL files, where WebGL 1.0 and 2.0 versions are defined, we built a Console tool that parses such files and outputs a C# backend, abstracting JSObject interop. This way we write WebGL code in a similar flavor as any JavaScript would do but feeling at home with C#. We spent a talk at past dotNet 2019 were explained more in-depth how we reached everything.
There are multiple options to generate bindings for JavaScript APIs, like looking for their TypeScript mappings and working through such to obtain the C# equivalent. Or even simply doing such manually, if the origin itself is small enough.
Following with WasmApp1, when a user clicks the button such happens on the DOM side, as the button triggers its onclick event. As the template showcases, such event is subscribed from .NET:
button.SetObjectProperty(
"onclick",
new
Action(_ =>
{
using
(
var
window = (JSObject)Runtime.GetGlobalObject())
{
window.Invoke("alert", "Hello, Wasm!");
}
}));
The Program.cs portion where the button’s onclick subscription happens
If we see the signature in JavaScript for a button’s onclick, a MouseEvent object is sent back, with information related. We wrap such in C# by telling our Action will receive a JSObject, thus WebAssembly.Runtime already handles the cast from JavaScript to that .NET type. In our case, we are not interested in reading its information but could access its properties by using JSObject methods like GetObjectProperty(), for instance.
Until here, the communication has been fired from user interaction with DOM, where a subscription in .NET is already set; however, could we simply invoke .NET code from anywhere in JavaScript? The answer is yes, and we go back to BINDINGS object, available always during the page execution, which provides a function to call static methods from a well-known assembly:
BINDING.call_static_method("[WasmApp1] WasmApp1.Program:Main", []);
Init code at index.html to execute our app’s entry point
In the syntax of type “[AssemblyName] FullQualifiedClassName:StaticMethodName” we can tell the .NET Runtime to immediately execute such method and wait for its return value. The second parameter provided is an array to pass arguments.
A few weeks ago, we made public our first experiment with Wave Engine targeting the Web: a 3D models viewer.
Such supposed a milestone achievement for us, as demoed we are ready to create 3D experiences within the browser though for the industrial needs: CAD tools, 3D viewers, GPU-based computation, etc. Reaching Wasm with our C# existing codebase has meant a tremendous value for us and sure the same applies to other companies and individuals around the globe.
If you want to look in-depth at how we developed this solution please keep reading here.
Although most of the .NET Core 3 API is supported, we have stumbled upon specific scenarios which still have some caveats:
If we could write a love letter to .NET Wasm, it would be like this:
Mono Wasm, or better said .NET Wasm, as the Wasm module and companion JavaScript file were renamed a few weeks ago, will be part of .NET 5. We would really like this to happen, not only because of the message it spends, but mainly for the improvements, the toolchain will obtain as part of the process of making it easy for developers to consume.
As being developed in Open Source, you, or your company, can collaborate to make Wasm support better. All the work is made at GitHub under sdks/wasm path. You can start by cloning the repo into a Linux machine —or simply under Windows 10 with WSL installed.
Going through its root README.md, enables you with everything needed to build and run the test suite used. Simply reporting bugs —where we have found the WasmApp1 repo branches to be a quick path for such— leverage the need for even more attention in this promising, and quite beautiful, platform.
Want to know all the secrets of our graphics engine? Here you have all our posts about Wave Engine!
And if you want to know more about our newest graphic engine, discover Evergine!
Elena Canorea
Communications Lead
Cookie | Duration | Description |
---|---|---|
__cfduid | 1 year | The cookie is used by cdn services like CloudFare to identify individual clients behind a shared IP address and apply security settings on a per-client basis. It does not correspond to any user ID in the web application and does not store any personally identifiable information. |
__cfduid | 29 days 23 hours 59 minutes | The cookie is used by cdn services like CloudFare to identify individual clients behind a shared IP address and apply security settings on a per-client basis. It does not correspond to any user ID in the web application and does not store any personally identifiable information. |
__cfduid | 1 year | The cookie is used by cdn services like CloudFare to identify individual clients behind a shared IP address and apply security settings on a per-client basis. It does not correspond to any user ID in the web application and does not store any personally identifiable information. |
__cfduid | 29 days 23 hours 59 minutes | The cookie is used by cdn services like CloudFare to identify individual clients behind a shared IP address and apply security settings on a per-client basis. It does not correspond to any user ID in the web application and does not store any personally identifiable information. |
_ga | 1 year | This cookie is installed by Google Analytics. The cookie is used to calculate visitor, session, campaign data and keep track of site usage for the site's analytics report. The cookies store information anonymously and assign a randomly generated number to identify unique visitors. |
_ga | 1 year | This cookie is installed by Google Analytics. The cookie is used to calculate visitor, session, campaign data and keep track of site usage for the site's analytics report. The cookies store information anonymously and assign a randomly generated number to identify unique visitors. |
_ga | 1 year | This cookie is installed by Google Analytics. The cookie is used to calculate visitor, session, campaign data and keep track of site usage for the site's analytics report. The cookies store information anonymously and assign a randomly generated number to identify unique visitors. |
_ga | 1 year | This cookie is installed by Google Analytics. The cookie is used to calculate visitor, session, campaign data and keep track of site usage for the site's analytics report. The cookies store information anonymously and assign a randomly generated number to identify unique visitors. |
_gat_UA-326213-2 | 1 year | No description |
_gat_UA-326213-2 | 1 year | No description |
_gat_UA-326213-2 | 1 year | No description |
_gat_UA-326213-2 | 1 year | No description |
_gid | 1 year | This cookie is installed by Google Analytics. The cookie is used to store information of how visitors use a website and helps in creating an analytics report of how the wbsite is doing. The data collected including the number visitors, the source where they have come from, and the pages viisted in an anonymous form. |
_gid | 1 year | This cookie is installed by Google Analytics. The cookie is used to store information of how visitors use a website and helps in creating an analytics report of how the wbsite is doing. The data collected including the number visitors, the source where they have come from, and the pages viisted in an anonymous form. |
_gid | 1 year | This cookie is installed by Google Analytics. The cookie is used to store information of how visitors use a website and helps in creating an analytics report of how the wbsite is doing. The data collected including the number visitors, the source where they have come from, and the pages viisted in an anonymous form. |
_gid | 1 year | This cookie is installed by Google Analytics. The cookie is used to store information of how visitors use a website and helps in creating an analytics report of how the wbsite is doing. The data collected including the number visitors, the source where they have come from, and the pages viisted in an anonymous form. |
attributionCookie | session | No description |
cookielawinfo-checkbox-analytics | 1 year | Set by the GDPR Cookie Consent plugin, this cookie is used to record the user consent for the cookies in the "Analytics" category . |
cookielawinfo-checkbox-necessary | 1 year | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-necessary | 1 year | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-non-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Non Necessary". |
cookielawinfo-checkbox-non-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Non Necessary". |
cookielawinfo-checkbox-non-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Non Necessary". |
cookielawinfo-checkbox-non-necessary | 1 year | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Non Necessary". |
cookielawinfo-checkbox-performance | 1 year | Set by the GDPR Cookie Consent plugin, this cookie is used to store the user consent for cookies in the category "Performance". |
cppro-ft | 1 year | No description |
cppro-ft | 7 years 1 months 12 days 23 hours 59 minutes | No description |
cppro-ft | 7 years 1 months 12 days 23 hours 59 minutes | No description |
cppro-ft | 1 year | No description |
cppro-ft-style | 1 year | No description |
cppro-ft-style | 1 year | No description |
cppro-ft-style | session | No description |
cppro-ft-style | session | No description |
cppro-ft-style-temp | 23 hours 59 minutes | No description |
cppro-ft-style-temp | 23 hours 59 minutes | No description |
cppro-ft-style-temp | 23 hours 59 minutes | No description |
cppro-ft-style-temp | 1 year | No description |
i18n | 10 years | No description available. |
IE-jwt | 62 years 6 months 9 days 9 hours | No description |
IE-LANG_CODE | 62 years 6 months 9 days 9 hours | No description |
IE-set_country | 62 years 6 months 9 days 9 hours | No description |
JSESSIONID | session | The JSESSIONID cookie is used by New Relic to store a session identifier so that New Relic can monitor session counts for an application. |
viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
viewed_cookie_policy | 1 year | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
viewed_cookie_policy | 1 year | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
VISITOR_INFO1_LIVE | 5 months 27 days | A cookie set by YouTube to measure bandwidth that determines whether the user gets the new or old player interface. |
wmc | 9 years 11 months 30 days 11 hours 59 minutes | No description |
Cookie | Duration | Description |
---|---|---|
__cf_bm | 30 minutes | This cookie, set by Cloudflare, is used to support Cloudflare Bot Management. |
sp_landing | 1 day | The sp_landing is set by Spotify to implement audio content from Spotify on the website and also registers information on user interaction related to the audio content. |
sp_t | 1 year | The sp_t cookie is set by Spotify to implement audio content from Spotify on the website and also registers information on user interaction related to the audio content. |
Cookie | Duration | Description |
---|---|---|
_hjAbsoluteSessionInProgress | 1 year | No description |
_hjAbsoluteSessionInProgress | 1 year | No description |
_hjAbsoluteSessionInProgress | 1 year | No description |
_hjAbsoluteSessionInProgress | 1 year | No description |
_hjFirstSeen | 29 minutes | No description |
_hjFirstSeen | 29 minutes | No description |
_hjFirstSeen | 29 minutes | No description |
_hjFirstSeen | 1 year | No description |
_hjid | 11 months 29 days 23 hours 59 minutes | This cookie is set by Hotjar. This cookie is set when the customer first lands on a page with the Hotjar script. It is used to persist the random user ID, unique to that site on the browser. This ensures that behavior in subsequent visits to the same site will be attributed to the same user ID. |
_hjid | 11 months 29 days 23 hours 59 minutes | This cookie is set by Hotjar. This cookie is set when the customer first lands on a page with the Hotjar script. It is used to persist the random user ID, unique to that site on the browser. This ensures that behavior in subsequent visits to the same site will be attributed to the same user ID. |
_hjid | 1 year | This cookie is set by Hotjar. This cookie is set when the customer first lands on a page with the Hotjar script. It is used to persist the random user ID, unique to that site on the browser. This ensures that behavior in subsequent visits to the same site will be attributed to the same user ID. |
_hjid | 1 year | This cookie is set by Hotjar. This cookie is set when the customer first lands on a page with the Hotjar script. It is used to persist the random user ID, unique to that site on the browser. This ensures that behavior in subsequent visits to the same site will be attributed to the same user ID. |
_hjIncludedInPageviewSample | 1 year | No description |
_hjIncludedInPageviewSample | 1 year | No description |
_hjIncludedInPageviewSample | 1 year | No description |
_hjIncludedInPageviewSample | 1 year | No description |
_hjSession_1776154 | session | No description |
_hjSessionUser_1776154 | session | No description |
_hjTLDTest | 1 year | No description |
_hjTLDTest | 1 year | No description |
_hjTLDTest | session | No description |
_hjTLDTest | session | No description |
_lfa_test_cookie_stored | past | No description |
Cookie | Duration | Description |
---|---|---|
loglevel | never | No description available. |
prism_90878714 | 1 month | No description |
redirectFacebook | 2 minutes | No description |
YSC | session | YSC cookie is set by Youtube and is used to track the views of embedded videos on Youtube pages. |
yt-remote-connected-devices | never | YouTube sets this cookie to store the video preferences of the user using embedded YouTube video. |
yt-remote-device-id | never | YouTube sets this cookie to store the video preferences of the user using embedded YouTube video. |
yt.innertube::nextId | never | This cookie, set by YouTube, registers a unique ID to store data on what videos from YouTube the user has seen. |
yt.innertube::requests | never | This cookie, set by YouTube, registers a unique ID to store data on what videos from YouTube the user has seen. |