Saltar al contenido principal
marzo 19, 2020

Wave Engine funciona en la web gracias a Mono Wasm

Qué significa WebAssembly para .NET

WebAssembly (Wasm) es el no-tan-nuevo estándar para ejecutar código de alto rendimiento en el navegador, en el lado del cliente. Se puede pensar en él como una máquina virtual (VM) optimizada, que traduce el código intermedio, el código de bytes, a la arquitectura de la máquina de destino, todo ello dentro de una caja de arena segura. No es un sustituto de JavaScript, no es la bala de plata, pero nos ha permitido pensar en la Web como otro objetivo de escritorio para .NET, de la misma manera que ya lo son Windows, macOS, Android o iOS, entre otros.

Ahora mismo puedes ejecutar tu aplicación .NET Core Console incrustada en un archivo HTML, hacer peticiones web, acceder al Sistema de Archivos (FS), dibujar cosas con WebGL (la rama web de OpenGL), y un montón de cosas más, simplemente porque es el runtime de .NET el que se ejecuta en Wasm. Todavía quedan pasos que tomar para mejorarlo de cara a los desarrolladores, pero, al mismo tiempo, comienza a ser lo suficientemente maduro para usar junto con los proyectos.

Qué es Mono Wasm y por qué Wave Engine lo necesita

Mono Wasm —como se llamaba hace unas semanas, ahora también es .NET—, proporciona el Common Language Runtime (CLR) de Mono, escrito principalmente en C, compilado en Wasm. Emscripten, un proyecto de código abierto, proporciona la cadena de herramientas necesaria para convertir los backends de LLVM en Wasm, junto con un bootstrap de JavaScript para inicializar el entorno.

Con Wave Engine, nuestro motor 3D multiplataforma centrado en las necesidades industriales, ya intentamos dirigirnos a la Web hace unos años con JSIL, pero simplemente no estaba preparado para nosotros. Pusimos en pausa ese intento hasta que pudiéramos ganar en rendimiento y facilidad para dirigirnos a las API de bajo nivel.

Junto con el desarrollo de Mono Wasm y de plataformas como Uno Platform que utilizan dicha tecnología subyacente, nos replanteamos nuestra posición y tras estudiar como se podía conseguir WebGL, empezamos a habilitar la Web como otra plataforma a la que podemos dirigirnos hoy en día.

Tu primera app con Mono Wasm

Actualmente existen distintos caminos si quieres abordar Wasm en .NET:

  • Blazor: que permite «construir interfaces web interactivas utilizando C# en lugar de JavaScript» y se ejecuta en Mono Wasm por debajo.
  • Uno’s Wasm bootstrap: que permite que Uno Platform se ejecute sobre Wasm, y está construido también sobre Mono Wasm NuGets.
  • Mono Wasm NuGets: descargable a través de sus artefactos Jenkins, son los elementos básicos necesarios para habilitar el escenario Wasm en .NET.

Hy otros proyectos interesantes para convertir los módulos Wasm en ensamblados .NET —como el wasm2cil de Eric Sink—, por ejemplo, que también potencia la comunidad.

Obteniendo la cadena de herramientas

Todos los desarrolladores de .NET están acostumbrados a especificar sus plataformas de destino a través de archivos CSPROJ, pero esto difiere un poco con Wasm hoy en día. La infraestructura aún no está lista, y toda la cadena de herramientas se suministra en forma de NuGets anteriores: Mono runtime, linker, JavaScript interop SDK, etc.

Para empujar en la dirección que ayudaría a Microsoft/Mono a construir la infraestructura mencionada debajo y disfrutar de las últimas partes del repo, deberías:

  1. Navegar hasta la última compilación exitosa y descargar los artefactos:
  1. https://jenkins.mono-project.com/job/test-mono-mainline-wasm/label=ubuntu-1804-amd64/lastSuccessfulBuild/Azure/
  2. Descargar el ZIP que contiene los NuGets: mono-wasm-*.zip
  3. Descomprimirlo y encontrar los NuGets en packages/

Creando el proyecto

Simplemente le decimos al compilador de .NET que apunte a Wasm a través del atributo SDK del proyecto y a parte de esto, no existe ninguna diferencia fundamental con cualquier otro proyecto:

<Project Sdk="Mono.WebAssembly.Sdk/0.2.0">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<None Remove="index.html" />
</ItemGroup>
</Project>

Estructura básica de un archivo .csproj dirigido a Wasm

Para que el compilador detecte donde se encuentra Mono.WebAssembly.Sdk NuGet será necesario suministrar un archivo NuGet.config, estableciento donde se encuentra en el disco duro.

Con el objetivo de simplificar todos estos pasos hemos creado un repo que proporciona todo lo necesario para empezar a escribir tu código: WasmApp1; junto con un artículo sobre cómo servir la página HTML resultante de manera local. Pensamos en ello como si pudieras crear un nuevo proyecto dentro de Visual Studio, simplemente eligiendo una plantilla Wasm.

Interoperabilidad con el DOM

De la misma manera que se actualizan los controles de la interfaz de usuario desde una aplicación de Xamarin.Forms, se hace lo mismo con el DOM de una página web: se puede modificar la interfaz de usuario de la web desde .NET y además invocar cosas de .NET desde la interfaz de usuario -como a través de un clic en un botón, por ejemplo.

Del .NET al DOM

La plantilla WasmApp1 viene con un botón HTML que dispara algún código en el lado .NET. Este botón se crea dinámicamente cuando se inicia la aplicación, y nos suscribimos a su evento onclick justo después:

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);
}

Un trozo de Program.cs, de WasmApp1

El Runtime class estático, ubicado en WebAssembly namespace, proporciona algunos métodos útiles para obtener una referencia de un objeto JavaScript y, envuelto a través de la fachada JSObject, interactuar con él.

Puedes preguntarte por qué el documento, el cuerpo y el botón tienen un ámbito de uso: en realidad hay dos referencias para cada uno de ellos que se ejecutan al mismo tiempo en el navegador: la que está en JavaScript puro, y la que está en .NET, y es una buena práctica no mantener la segunda viva más tiempo del que se necesita -de hecho, se almacena en la capa de Emscripten, y puede recogerse a través de BINDING.mono_wasm_object_registry en la Consola del navegador. Cuantos menos elementos contenga, mejor para el rendimiento.

WasmApp1 funcionando en Firefox

Lo ideal es que la comunidad proporcione NuGets para que JSObject no sea visible a un nivel superior, como ya está haciendo Kenneth Puncey con su proyecto wasm-dom —compruebalo en la carpeta de muestras!

Desde Plain Concepts mantenemos los enlaces de bajo nivel de WebGL, WebGL.NET (un nombre muy creativo, eh?), pensados para Wave Engine, que pueden ser utilizados simplemente añadiendo el paquete NuGet al proyecto anterior. En nuestro caso, partiendo de los archivos IDL de Khronos, donde se definen las versiones 1.0 y 2.0 de WebGL, construimos una Console tool que analiza dichos archivos y produce un backend de C#, abstrayendo la interoperabilidad de JSObject. De esta manera escribimos código WebGL con un sabor similar al de cualquier JavaScript pero sintiéndonos en casa con C#. Dimos una charla durante la DotNet 2019 donde explicamos más a fondo como llegamos a todo esto.

Hay múltiples opciones para generar bindings para las APIs de JavaScript, como buscar sus mapeos en TypeScript y trabajar a través de ellos para obtener el equivalente en C#. O incluso simplemente hacerlo manualmente, si el origen es lo suficientemente pequeño.

Del DOM a .NET

Siguiendo con WasmApp1, cuando un usuario hace clic en el botón esto sucede en el lado del DOM, ya que el botón activa su evento onclick. Como muestra la plantilla, dicho evento se suscribe desde .NET:

button.SetObjectProperty(
    "onclick",
    new Action(_ =>
    {
        using (var window = (JSObject)Runtime.GetGlobalObject())
        {
            window.Invoke("alert", "Hello, Wasm!");
        }
    }));

La parte de Program.cs donde ocurre la suscripción onclick del botón

Si vemos la firma en JavaScript para el onclick de un botón, un objeto MouseEvent es enviado de vuelta, con información relacionada. Lo envolvemos en C# diciéndole a nuestra Acción que recibirá un JSObject, con lo que WebAssembly.Runtime ya se encarga del cast de JavaScript a ese tipo .NET. En nuestro caso, no nos interesa leer su información, pero podríamos acceder a sus propiedades utilizando métodos de JSObject como GetObjectProperty(), por ejemplo.

Hasta aquí, la comunicación se ha disparado desde la interacción del usuario con el DOM, donde ya se ha establecido una suscripción en .NET; sin embargo, ¿podríamos simplemente invocar el código de .NET desde cualquier parte de JavaScript? La respuesta es sí, y volvemos al objeto BINDINGS, disponible siempre durante la ejecución de la página, que proporciona una función para llamar a métodos estáticos desde un ensamblaje conocido:

BINDING.call_static_method("[WasmApp1] WasmApp1.Program:Main", []);

Código de inicio en index.html para ejecutar el punto de entrada de nuestra aplicación

En la sintaxis del tipo «[AssemblyName] FullQualifiedClassName:StaticMethodName» podemos indicar al .NET Runtime que ejecute inmediatamente dicho método y espere su valor de retorno. El segundo parámetro proporcionado es un array para pasar argumentos.

Visor glTF de Wave Engine

Hace unas semanas, hicimos público nuestro primer experimento con Wave Engine dirigido a la web: un visor de modelos 3D.

Esto ha supuesto un hito para nosotros, ya que demostramos que estamos preparados para crear experiencias 3D dentro del navegador, aunque para las necesidades industriales: Herramientas CAD, visores 3D, computación basada en GPU, etc. Llegar a Wasm con nuestra base de código existente en C# ha significado un enorme valor para nosotros y seguro que lo mismo se aplica a otras empresas y personas de todo el mundo.

Si quieres ver en profundidad cómo hemos desarrollado esta solución, sigue leyendo aquí.

Mirando más allá

Aunque la mayor parte de la API de .NET Core 3 es compatible, nos hemos topado con escenarios específicos que todavía tienen algunas advertencias:

  1. Puedes consumir el espacio de nombres System.IO, pero sólo a través de su API de sincronización: tiene que ver con la virtualización subyacente de Emscripten FS, pero sería genial tener alguna documentación sobre lo que los usuarios pueden hacer hoy en día, como las mejores prácticas.
  2. No puedes usar el multi-threading ampliamente: ya hay soporte de Emscripten y .NET, pero sólo algunos navegadores lo soportan, ya que Wasm está madurando también al mismo tiempo.

Si pudiéramos escribir una carta de amor a .NET Wasm, sería así:

  1. Una sola cadena de herramientas para gobernarlas todas: actualmente, no hay posibilidad de obtener bits de .NET Wasm de nuget.org (los mismos nombres de paquetes también son tomados por otros usuarios), lo que frustra a los principiantes en el momento de mpezar a jugar con él. Uno los distribuye internamente; nosotros, Wave Engine, tenemos un feed privado con tales subidas.
  2. Mejores herramientas de debugging y profiling: hoy en día existen opciones para soportar el debugging de C# desde las herramientas de Chrome Developer, pero soñamos con algo soportado desde el propio Visual Studio, como el debugging de JavaScript dentro de una aplicación ASP.NET Core. Seguramente por nuestra naturaleza, impulsada por Wave Engine, estamos obsesionados con ganar algunos ciclos de CPU en nuestras apps. Sería fantástico también, como ejemplo, abrir las trazas de tiempo de ejecución en Xamarin Profiler, y reutilizar -aquí estoy hablando a título personal- mis conocimientos existentes al usarlos para las aplicaciones de Xamarin.
  3. Documentación: Blazor está haciendo un buen trabajo en este aspecto, pero se centra en el punto de vista de ASP.NET desde nuestra perspectiva: realmente nos gustaría una documentación sencilla y limpia sobre el propio WebAssembly Runtime, que animara a los usuarios a pensar en la Web como una plataforma en sí misma, de la que surgen iniciativas como Wave Engine o Uno Platform.

Mono Wasm, o mejor dicho .NET Wasm, ya que el módulo Wasm y el archivo JavaScript que lo acompaña fueron renombrados hace unas semanas, formarán parte de .NET 5. Nos gustaría mucho que esto ocurriera, no sólo por el mensaje que transmite, sino principalmente por las mejoras que obtendrá la cadena de herramientas como parte del proceso de facilitar el consumo a los desarrolladores.

Al estar desarrollado en Open Source, tu o tu empresa, podeis colaborar para que Wasm tenga un mejor soporte. Todo el trabajo se realiza en GitHub bajo la ruta sdks/wasm. Puedes empezar clonando el repo en una máquina Linux -o simplemente en Windows 10 con WSL instalado.

Pasando por su raíz README.md, le permite con todo lo necesario para construir y ejecutar la suite de pruebas utilizadas. Simplemente informando de los errores -donde hemos encontrado que las ramas del repo de WasmApp1 son una vía rápida para ello- aprovecha la necesidad de prestar aún más atención a esta prometedora, y bastante bonita, plataforma.

Si quieres descubrir nuestro último motor gráfico, ¡no te pierdas Evergine!

Autor
Marcos Cobeña Morián
Plain Concepts Mobile