Desarrollo de WebAssembly: rastrillo real y ejemplos



El anuncio de WebAssembly tuvo lugar en 2015, pero ahora, después de años, todavía hay pocos que puedan presumir de él en producción. Los materiales sobre dicha experiencia son aún más valiosos: la información de primera mano sobre cómo vivir con ella en la práctica aún es escasa.

En la conferencia HolyJS, un informe sobre la experiencia de usar WebAssembly recibió altas calificaciones de la audiencia, y ahora se ha preparado especialmente una versión de texto de este informe para Habr (también se adjunta un video).



Mi nombre es Andrey, te contaré sobre WebAssembly. Podemos decir que comencé a involucrarme en la web en el siglo pasado, pero soy modesto, así que no diré eso. Durante este tiempo, logré trabajar tanto en el backend como en la interfaz, e incluso dibujé un pequeño diseño. Hoy estoy interesado en cosas como WebAssembly, C ++ y otras cosas nativas. También me encanta la tipografía y coleccionar tecnología antigua.

Primero, hablaré sobre cómo el equipo y yo implementamos WebAssembly en nuestro proyecto, luego discutiremos si necesita algo de WebAssembly y terminaremos con algunos consejos en caso de que quiera implementarlo por su cuenta.

Cómo implementamos WebAssembly


Trabajo para Inetra, estamos ubicados en Novosibirsk y estamos haciendo algunos de nuestros propios proyectos. Uno de ellos es ByteFog. Esta es una tecnología punto a punto para entregar video a los usuarios. Nuestros clientes son servicios que distribuyen una gran cantidad de video. Tienen un problema: cuando ocurre un evento popular, por ejemplo, la conferencia de prensa de alguien o algún evento deportivo, cómo no prepararse para él, un grupo de clientes viene, apoyándose en el servidor, y el servidor está triste. Los clientes reciben una calidad de video muy pobre en este momento.

Pero todos están viendo el mismo contenido. Pidamos a los dispositivos vecinos de los usuarios que compartan piezas de video, y luego descargaremos el servidor, ahorraremos ancho de banda y los usuarios recibirán video en mejor calidad. Estas nubes son nuestra tecnología, nuestro servidor proxy ByteFog.



Debemos estar instalados en todos los dispositivos que puedan mostrar video, por lo tanto, admitimos una amplia gama de plataformas: Windows, Linux, Android, iOS, Web, Tizen. ¿Qué idioma elegir para tener una única base de código en todas estas plataformas? Elegimos C ++ porque resultó tener las mayores ventajas: - D Más en serio, tenemos una buena experiencia en C ++, es realmente un lenguaje rápido, y en portabilidad es probablemente solo superado por C.

Tenemos una aplicación bastante grande (900 clases), pero funciona bien. Bajo Windows y Linux, compilamos en código nativo. Para Android e iOS, creamos una biblioteca que conectamos a la aplicación. Hablaremos de Tizen en otra ocasión, pero en la Web solíamos trabajar como complemento del navegador.

Esta es la tecnología API Netscape Plugin. Como su nombre lo indica, es bastante antiguo y también tiene un inconveniente: proporciona un acceso muy amplio al sistema, por lo que el código de usuario puede causar un problema de seguridad. Esta es probablemente la razón por la cual Chrome desactivó el soporte para esta tecnología en 2015, y luego todos los navegadores se unieron a este flash mob. Así que nos quedamos sin una versión web durante casi dos años.

En 2017, surgió una nueva esperanza. Como te puedes imaginar, esto es WebAssembly. Como resultado, nos propusimos portar nuestra aplicación a un navegador. Dado que el soporte para Firefox y Chrome ya apareció en la primavera, y para el otoño de 2017, Edge y Safari se retiraron.

Era importante para nosotros usar el código listo, ya que tenemos mucha lógica de negocios que no queríamos duplicar, para no duplicar la cantidad de errores. Tome el compilador Emscripten. Él hace lo que necesitamos: compila la aplicación positiva en el navegador y recrea el entorno familiar para la aplicación nativa en el navegador. Podemos decir que Emscripten es un Browserify para el código C ++. También le permite reenviar objetos de C ++ a JavaScript y viceversa. Nuestro primer pensamiento fue: ahora tomemos Emscripten, solo compilemos y todo funcionará. Por supuesto que no. A partir de esto comenzó nuestro viaje a lo largo del rastrillo.

Lo primero con lo que nos encontramos fue la adicción. Había varias bibliotecas en nuestra base de código. Ahora no tiene sentido enumerarlos, pero para aquellos que entienden, tenemos Boost. Esta es una gran biblioteca que le permite escribir código multiplataforma, pero es muy difícil configurar la compilación con ella. Quería arrastrar el menor código posible al navegador.

Bytefog Architecture


Como resultado, identificamos el núcleo: podemos decir que este es un servidor proxy que contiene la lógica comercial principal. Este servidor proxy toma datos de dos fuentes. El primero y principal es HTTP, es decir, un canal al servidor de distribución de video, el segundo es nuestra red P2P, es decir, un canal a otro mismo proxy de otro usuario. Damos los datos principalmente al reproductor, ya que nuestra tarea es mostrar contenido de alta calidad al usuario. Si quedan recursos, distribuimos el contenido a la red P2P para que otros usuarios puedan descargarlo. En el interior hay un caché inteligente que hace toda la magia.



Una vez compilado todo esto, nos enfrentamos al hecho de que WebAssembly se ejecuta en el entorno limitado del navegador. Eso significa que no puede hacer más de lo que JavaScript proporciona. Mientras que las aplicaciones nativas usan muchas cosas específicas de la plataforma, como un sistema de archivos, una red o números aleatorios. Todas estas características deberán implementarse en JavaScript utilizando lo que el navegador nos brinda. Esta placa enumera los reemplazos bastante obvios enumerados.



Para hacer esto posible, es necesario eliminar la implementación de capacidades nativas en una aplicación nativa e insertar una interfaz allí, es decir, dibujar un cierto borde. Luego implementa esto en JavaScript y deja la implementación nativa, y ya durante el ensamblaje se selecciona la necesaria. Entonces, miramos nuestra arquitectura y encontramos todos los lugares donde se puede dibujar este borde. Casualmente, este es un subsistema de transporte.



Para cada lugar, definimos una especificación, es decir, arreglamos un contrato: qué métodos serán, qué parámetros tendrán, qué tipos de datos. Una vez que haya hecho esto, puede trabajar en paralelo, cada desarrollador de su lado.

Cual es el resultado? Reemplazamos el canal principal de entrega de video del proveedor con el AJAX habitual. Emitimos datos al jugador a través de la popular biblioteca HLS.js, pero existe una posibilidad fundamental de integrarse con otros jugadores, si es necesario. Reemplazamos toda la capa P2P con WebRTC.



Como resultado de la compilación, se obtienen varios archivos. Lo más importante es el binario .wasm. Contiene el código de bytes compilado que ejecutará el navegador y que contiene todo el legado de C ++. Pero por sí mismo no funciona, el llamado "código de pegamento" es necesario, también lo genera el compilador. El código de pegamento está descargando un archivo binario, y usted carga ambos archivos a producción. Para fines de depuración, puede generar una representación textual del ensamblador: un archivo .wast y un mapa fuente. Debe comprender que pueden ser muy grandes. En nuestro caso, alcanzaron los 100 megabytes o más.

Recolectando el paquete


Echemos un vistazo más de cerca al código de pegamento. Este es el buen ES5 de siempre, ensamblado en un solo archivo. Cuando lo conectamos a una página web, tenemos una variable global que contiene todo nuestro módulo wasm instanciado, que está listo para aceptar solicitudes a su API.

Pero incluir un archivo separado es una complicación bastante grave para la biblioteca que usarán los usuarios. Nos gustaría poner todo en un solo paquete. Para esto utilizamos Webpack y una opción especial de compilación MODULARIZE.

Envuelve el código adhesivo en el patrón "Módulo", y podemos recogerlo: importar o usar require si escribimos en ES5 - Webpack entiende con calma esta dependencia. Hubo un problema con Babel: no le gustó la gran cantidad de código, pero este es un código ES5, no es necesario transponerlo, solo lo agregamos para ignorarlo.

En busca del número de archivos, decidí usar la opción SINGLE_FILE. Traduce todos los archivos binarios resultantes de la compilación en el formulario Base64 y lo inserta en el código adhesivo como una cadena. Suena como una gran idea, pero después de eso el paquete se convirtió en 100 megabytes de tamaño. Ni Webpack, ni Babel, ni siquiera el navegador funcionan en ese volumen. De todos modos, ¿no forzaremos al usuario a cargar 100 megabytes?

Si lo piensa, esta opción no es necesaria. El código adhesivo descarga archivos binarios por sí solo. Lo hace a través de HTTP, por lo que sacamos el caché de la caja, podemos configurar los encabezados que queramos, por ejemplo, habilitar la compresión, y los archivos de WebAssembly están perfectamente comprimidos.

Pero la tecnología más genial es la compilación de transmisión. Es decir, el archivo WebAssembly, mientras se descarga desde el servidor, ya se puede compilar en el navegador a medida que llegan los datos, y esto acelera enormemente la carga de su aplicación. En general, toda la tecnología de WebAssembly se centra en el inicio rápido de una base de código grande.

Thenable


Otro problema con el módulo es que es un objeto Thenable, es decir, tiene un método .then (). Esta función le permite colgar una devolución de llamada en el momento en que se inicia el módulo, y es muy conveniente. Pero me gustaría que la interfaz coincida con Promise. Thenable no es Promesa, pero está bien, terminemos nosotros mismos. Escribamos un código tan simple:

return new Promise((resolve, reject) => { Module(config).then((module) => { resolve(module); }); }); 

Creamos Promise, iniciamos nuestro módulo y, como devolución de llamada, llamamos a la función de resolución y pasamos el módulo que instalamos allí. Todo parece ser obvio, todo está bien, estamos iniciando: algo está mal, nuestro navegador está congelado, nuestras DevTools están suspendidas y el procesador se está calentando en la computadora. No entendemos nada: algún tipo de recursión o un bucle infinito. La depuración es bastante difícil, y cuando interrumpimos JavaScript, terminamos en la función Then del módulo Emscripten.

 Module['then'] = function(func) { if (Module['calledRun']) { func(Module); } else { Module['onRuntimeInitialized'] = function() { func(Module); }; }; return Module; }; 

Miremos con más detalle. Parcela

 Module['onRuntimeInitialized'] = function() { func(Module); }; 

responsable de colgar una devolución de llamada. Aquí todo está claro: una función asincrónica que llama a nuestra devolución de llamada. Todo como queramos. Hay otra parte de esta característica.

 if (Module['calledRun']) { func(Module); 

Se llama cuando el módulo ya se ha iniciado. Luego, la devolución de llamada se llama sincrónicamente de inmediato, y el módulo se le pasa en el parámetro. Esto imita el comportamiento de Promise, y parece ser lo que esperamos. Pero entonces, ¿qué está mal?

Si lees cuidadosamente la documentación, resulta que hay un punto muy sutil sobre Promise. Cuando resolvemos la Promesa usando un Thenable, el navegador desenvolverá los valores de ese Thenable, y para hacer esto, llamará al método .then (). Como resultado, resolvemos la Promesa, le pasamos el módulo. El navegador pregunta: ¿Entonces esto es un objeto? Sí, este es un Thenable. Luego se llama a la función .then () en el módulo, y la función de resolución en sí misma se pasa como una devolución de llamada.

El módulo verifica si se está ejecutando. Ya se está ejecutando, por lo que se llama a la devolución de llamada inmediatamente y se le pasa el mismo módulo nuevamente. Como devolución de llamada, tenemos la función de resolución, y el navegador pregunta: ¿es este un objeto Thenable? Sí, este es un Thenable. Y todo comienza de nuevo. Como resultado, caemos en un ciclo sin fin del cual el navegador nunca regresa.



No encontré una solución elegante para este problema. Como resultado, simplemente elimino el método .then () antes de resolverlo, y esto funciona.

Emscripten


Entonces, compilamos el módulo, ensamblamos JS, pero falta algo. Probablemente necesitemos hacer un trabajo útil. Para hacer esto, transfiera datos y conecte los dos mundos: JS y C ++. Como hacerlo Emscripten ofrece tres opciones:

  • El primero es las funciones ccall y cwrap. En la mayoría de los casos, los encontrará en algunos tutoriales sobre WebAssembly, pero no son adecuados para un trabajo real, ya que no admiten las capacidades de C ++.
  • El segundo es WebIDL Binder. Ya es compatible con las funciones de C ++, ya puede trabajar con él. Este es un lenguaje de descripción de interfaz serio utilizado, por ejemplo, por W3C para su documentación. Pero no queríamos llevarlo a nuestro proyecto y utilizamos la tercera opción
  • Embind. Podemos decir que esta es una forma nativa de conectar objetos para Emscripten, se basa en plantillas de C ++ y le permite hacer muchas cosas reenviando diferentes entidades de C ++ a JS y viceversa.


Embind te permite:

  • Llamar a funciones de C ++ desde código JavaScript
  • Crear objetos JS a partir de una clase C ++
  • Desde el código C ++, recurra a la API del navegador (si por alguna razón desea esto, puede, por ejemplo, escribir todo el marco front-end en C ++).
  • Lo principal para nosotros: implementar la interfaz JavaScript descrita en C ++.


Intercambio de datos


El último punto es importante, ya que esta es exactamente la acción que harás constantemente al portar la aplicación. Por lo tanto, me gustaría profundizar en ello con más detalle. Ahora habrá código C ++, pero no tengas miedo, es casi como TypeScript :-D

El esquema es el siguiente:



En el lado de C ++, hay un núcleo al que queremos dar acceso, por ejemplo, a una red externa, para cargar video. Solía ​​hacer esto con sockets nativos, había algún tipo de cliente HTTP que hacía esto, pero no hay sockets nativos en WebAssembly. Necesitamos salir de alguna manera, así que cortamos el antiguo cliente HTTP, insertamos la interfaz en este lugar e implementamos esta interfaz en JavaScript usando AJAX normal, de cualquier manera. Después de eso, volveremos a pasar el objeto resultante a C ++, donde el núcleo lo usará.

Hagamos el cliente HTTP más simple que solo puede realizar solicitudes de obtención:

 class HTTPClient { public: virtual std::string get(std::string url) = 0; }; 

A la entrada, recibe una cadena con la URL que se descargará, y a la salida
una cadena con el resultado de la solicitud. En C ++, las cadenas pueden tener datos binarios, por lo que esto es adecuado para video. Emscripten nos hace escribir aquí
un envoltorio tan aterrador:



En él, lo principal son dos cosas: el nombre de la función en el lado de C ++ (los marqué en verde) y los nombres correspondientes en el lado de JavaScript (los marqué en azul). Como resultado, escribimos una declaración de comunicación:



Funciona como bloques de Lego, desde donde lo ensamblamos. Tenemos una clase, esta clase tiene un método y queremos heredar de esta clase para implementar la interfaz. Eso es todo Vamos a JavaScript y heredamos. Esto se puede hacer de dos maneras. El primero es extender. Esto es muy similar a la buena extensión de Backbone.



El módulo contiene todo lo que compiló Emscripten y tiene una propiedad con una interfaz exportada. Llamamos al método extendido y pasamos un objeto allí con la implementación de este método, es decir, algún método se implementará en la función get
Obtenga información utilizando AJAX.

En la salida, extender nos da un constructor de JavaScript regular. Podemos llamarlo tantas veces como sea necesario y generar objetos en la cantidad que necesitamos. Pero hay una situación en la que tenemos un objeto y solo queremos pasarlo al lado de C ++.



Para hacer esto, de alguna manera, asocie este objeto a un tipo que C ++ comprenderá. Esto es lo que hace la función de implementación. En la salida, no proporciona un constructor, sino un objeto listo para usar, nuestro cliente, que podemos devolver a C ++. Puede hacer esto, por ejemplo, así:

 var app = Module.makeApp(client, …) 

Supongamos que tenemos una fábrica que crea nuestra aplicación y toma sus dependencias en parámetros, por ejemplo, cliente y algo más. Cuando esta función funciona, obtenemos el objeto de nuestra aplicación, que ya contiene la API que necesitamos. Puedes hacer lo contrario:

 val client = val::global(″client″); client.call<std::string>(″get″, val(...) ); 

Directamente desde C ++, tome a nuestro cliente desde el alcance global del navegador. Además, en lugar del cliente, puede haber cualquier API de navegador, comenzando desde la consola, terminando con la API DOM, WebRTC, lo que desee. A continuación, llamamos a los métodos que tiene este objeto y ajustamos todos los valores en la clase mágica val, que Emscripten nos proporciona.

Errores vinculantes


En general, eso es todo, pero cuando comienzas el desarrollo, te esperan errores de enlace. Se parecen a esto:



Emscripten trata de ayudarnos y explicar qué está yendo mal. Si todo esto se resume, entonces debe asegurarse de que coincidan (es fácil sellar y obtener un error vinculante):

  • Nombres
  • Tipos
  • Número de parámetros

La sintaxis de incrustación es inusual no solo para los proveedores de front-end, sino también para las personas que trabajan con C ++. Este es un tipo de DSL en el que es fácil cometer un error, debe seguir esto. Hablando de interfaces, cuando implementa algún tipo de interfaz en JavaScript, es necesario que coincida exactamente con lo que describió en su contrato.

Tuvimos un caso interesante. Mi colega Jura, que estuvo involucrado en el proyecto en el lado de C ++, usó Extend para probar sus módulos. Funcionaron perfectamente para él, así que los cometió y me los pasó. Solía ​​implementar para integrar estos módulos en un proyecto JS. Y dejaron de trabajar para mí. Cuando lo descubrimos, resultó que al vincular los nombres de las funciones, obtuvimos un error tipográfico.

Como su nombre lo indica, Extend es una extensión de la interfaz, por lo que si la ha sellado en algún lugar, Extend no arrojará un error, decidirá que acaba de agregar un nuevo método, y eso está bien.

Es decir, oculta los errores de enlace hasta que se llama al método en sí. Sugiero usar Implementar en todos los casos en los que le convenga, ya que verifica inmediatamente la corrección de la interfaz reenviada. Pero si necesita extender, debe cubrir con pruebas la llamada de cada método para no estropearlo.

Extender y ES6


Otro problema con Extend es que no admite clases ES6. Cuando hereda un objeto derivado de una clase ES6, Extend espera que todas las propiedades sean enumerables en él, pero con ES6 no lo es. Los métodos están en el prototipo y tienen enumerables: falso. Utilizo una muleta como esta, en la que reviso el prototipo y enciendo enumerable: verdadero:

 function enumerateProto(obj) { Object.getOwnPropertyNames(obj.prototype) .forEach(prop => Object.defineProperty(obj.prototype, prop, {enumerable: true}) ) } 

Espero algún día poder deshacerme de él, ya que se habla en la comunidad Emscripten sobre mejorar el soporte para ES6.

RAM


Hablando de C ++, uno no puede evitar mencionar la memoria. Cuando verificamos todo en video con calidad SD, todo estuvo bien con nosotros, ¡funcionó perfectamente! Tan pronto como hicimos la prueba FullHD, hubo una falta de error de memoria. No importa, existe la opción TOTAL_MEMORY, que establece el valor de memoria inicial para el módulo. Hicimos medio gigabyte, todo está bien, pero de alguna manera es inhumano para los usuarios, porque reservamos la memoria para todos, pero no todos tienen una suscripción al contenido FullHD.

Hay otra opción: ALLOW_MEMORY_GROWTH. Te permite hacer crecer la memoria.
gradualmente según sea necesario. Funciona así: Emscripten por defecto le da al módulo 16 megabytes para su operación. Cuando todos los usó, se asigna una nueva pieza de memoria. Todos los datos antiguos se copian allí, y aún tiene la misma cantidad de espacio para los nuevos. Esto sucede hasta que alcanzas los 4 GB.

Suponga que asignó 256 megabytes de memoria, pero sabe con certeza que pensó que su aplicación tiene suficiente 192. Entonces, el resto de la memoria se usará de manera ineficiente. Lo resaltó, lo tomó del usuario, pero no haga nada con él. Me gustaría evitar esto de alguna manera. Hay un pequeño truco: comenzamos a trabajar con la memoria aumentada una vez y media. Luego, en el tercer paso, alcanzamos 192 megabytes, y esto es exactamente lo que necesitamos. Hemos reducido el consumo de memoria en ese resto y hemos guardado la asignación de memoria innecesaria, y cuanto más tiempo tardan más. Por lo tanto, recomiendo usar ambas opciones juntas.

Inyección de dependencia


Parece que eso fue todo, pero luego el rastrillo fue un poco más. Hay un problema con la inyección de dependencia. Escribimos la clase más simple en la que se necesita una dependencia.

 class App { constructor(httpClient) { this.httpClient = httpClient } } 

Por ejemplo, pasamos nuestro cliente HTTP a nuestra aplicación. Ahorramos en la propiedad de clase. Parece que todo funcionará bien.

 Module.App.extend( ″App″, new App(client) ) 

Heredamos de la interfaz de C ++, primero creamos nuestro objeto, le pasamos la dependencia y luego heredamos. En el momento de la herencia, Emscripten hace algo increíble con el objeto. Es más fácil pensar que mata un objeto antiguo, crea uno nuevo basado en su plantilla y arrastra todos los métodos públicos allí. Pero al mismo tiempo, se pierde el estado del objeto y se obtiene un objeto que no está formado y no funciona correctamente. Resolver este problema es bastante simple. Debemos usar un constructor que funcione después de la etapa de herencia.

 class App { _construct(httpClient) { this.httpClient = httpClient this._parent._construct.call(this) } } 

Hacemos casi lo mismo: almacenamos la dependencia en el campo del objeto, pero este es el objeto que resultó después de la herencia. No debemos olvidar reenviar la llamada del constructor al objeto padre, que se encuentra en el lado de C ++. La última línea es un análogo del método super () en ES6. Así es como ocurre la herencia en este caso:

 const appConstr = Module.App.extend( ″App″, new App() ) const app = new appConstr(client) 

Primero, heredamos, luego creamos un nuevo objeto en el que ya se pasó la dependencia, y esto funciona.

Truco puntero


Otro problema es pasar objetos por puntero de C ++ a JavaScript. Ya hicimos un cliente HTTP. Por simplicidad, nos hemos perdido un detalle importante.

 std::string get(std::string url) 

El método devuelve el valor inmediatamente, es decir, resulta que la solicitud debe ser sincrónica. Pero después de todo, AJAX solicita AJAX y que son asíncronos, por lo que en la vida real el método no devolverá nada, o podemos devolver el ID de la solicitud. Pero para que alguien devuelva la respuesta, pasamos al oyente como el segundo parámetro, en el que habrá devoluciones de llamada de C ++.

 void get(std::string url, Listener listener) 

En JS, se ve así:

 function get(url, listener) { fetch(url).then(result) => { listener.onResult(result) }) } 

Tenemos una función get que toma este objeto de escucha. Comenzamos la descarga del archivo y colgamos la devolución de llamada. Cuando se descarga el archivo, extraemos la función deseada del oyente y le pasamos el resultado.

Parece que el plan es bueno, pero cuando se complete la función get, se destruirán todas las variables locales y, junto con ellas, los parámetros de la función, es decir, se destruirá el puntero y el tiempo de ejecución emscripten destruirá el objeto en el lado de C ++.

Como resultado, cuando se trata de llamar a la línea listener.onResult (resultado), el oyente ya no existirá, y al acceder a él, se producirá un error de acceso a la memoria que provocará el bloqueo de la aplicación.

Me gustaría evitar esto, y hay una solución, pero tardó varias semanas en encontrarla.

 function get(url, listener) { const listenerCopy = listener.clone() fetch(url).then((result) => { listenerCopy.onResult(result) listenerCopy.delete() }) } 

Resulta que hay un método para clonar un puntero. Por alguna razón, no está documentado, pero funciona bien y le permite aumentar el recuento de referencias en el puntero Emscripten. Esto nos permite suspenderlo en un cierre, y luego, cuando lanzamos nuestra devolución de llamada, nuestro puntero podrá acceder a nuestro oyente y podremos trabajar como lo necesitemos.

Lo más importante es no olvidar eliminar este puntero, de lo contrario, se producirá un error de pérdida de memoria, lo cual es muy malo.

Escritura rápida en memoria


Cuando descargamos videos, se trata de cantidades relativamente grandes de información, y me gustaría reducir la cantidad de copia de datos de un lado a otro para ahorrar memoria y tiempo. Hay un truco sobre cómo escribir una gran cantidad de información directamente en la memoria de WebAssembly desde JavaScript.

 var newData = new Uint8Array(…); var size = newData.byteLength; var ptr = Module._malloc(size); var memory = new Uint8Array( Module.buffer, ptr, size ); memory.set(newData); 

newData son nuestros datos como una matriz escrita. Podemos tomar su longitud y solicitar la asignación de memoria del tamaño que necesitamos del módulo WebAssembly. La función malloc nos devolverá un puntero, que es solo el índice de la matriz que contiene toda la memoria en WebAssembly. Desde el lado de JavaScript, solo se ve como un ArrayBuffer.

En el siguiente paso, cortaremos una ventana en este ArrayBuffer del tamaño correcto desde cierto lugar y copiaremos nuestros datos allí. A pesar de que la operación de configuración tiene una semántica de copia, cuando miré esta sección en el generador de perfiles, no vi un proceso largo. Creo que el navegador optimiza esta operación con la ayuda de la semántica de movimiento, es decir, transfiere la propiedad de la memoria de un objeto a otro.

Y en nuestra aplicación, también confiamos en la semántica de movimiento para guardar la copia de la memoria.

Adblock


Un problema interesante, más bien, sobre el cambio, con Adblock. Resulta que en Rusia todos los bloqueadores populares reciben una suscripción a la Lista de anuncios de RU, y tiene una regla tan maravillosa que prohíbe descargar WebAssembly de sitios de terceros. Por ejemplo, con un CDN.



La salida no es usar el CDN, sino almacenar todo en su dominio (esto no nos conviene). O cambie el nombre del archivo .wasm para que no se ajuste a esta regla. Todavía puede ir al foro de estos camaradas e intentar convencerlos de que eliminen esta regla. Creo que se justifican luchando contra los mineros de esta manera, aunque no sé por qué los mineros no pueden adivinar cambiar el nombre del archivo.

Producción


Como resultado, entramos en producción. Sí, no fue fácil, tomó 8 meses y quiero preguntarme si valió la pena. En mi opinión, valió la pena:

No es necesario instalar


Tenemos que entregar nuestro código al usuario sin instalar ningún programa. Cuando teníamos un complemento de navegador, el usuario tenía que descargarlo e instalarlo, y este es un filtro enorme para la distribución de tecnología. Ahora el usuario solo mira el video en el sitio y ni siquiera comprende que toda una maquinaria funciona debajo del capó, y que todo es complicado allí. El navegador simplemente descarga un archivo adicional con el código, como una imagen o .css.

Base de código unificado y depuración en diferentes plataformas


Al mismo tiempo, pudimos mantener nuestra base de código único. Podemos torcer el mismo código en diferentes plataformas y ha sucedido repetidamente que los errores que eran invisibles en una de las plataformas aparecieron en la otra. Y así, podemos detectar errores ocultos con diferentes herramientas en diferentes plataformas.

Liberación rápida


Obtuvimos un lanzamiento rápido, ya que podemos lanzarlo como una simple aplicación web y actualizar el código C ++ con cada nuevo lanzamiento. No se compara con cómo lanzar nuevos complementos, una aplicación móvil o una aplicación SmartTV. El lanzamiento depende solo de nosotros: cuando queramos, será lanzado.

Retroalimentación rápida


Y eso significa una respuesta rápida: si algo sale mal, podemos descubrir durante el día que hay un problema y responderlo.

Creo que todos estos problemas valieron estas ventajas. No todos tienen una aplicación C ++, pero si tiene una y desea que esté en el navegador, WebAssembly es un caso de uso 100% para usted.

Donde aplicar


No todos escriben en C ++. Pero no solo C ++ está disponible para WebAssembly. Sí, esta es históricamente la primera plataforma que todavía estaba disponible en asm.js, una de las primeras tecnologías de Mozilla. Por cierto, por lo tanto, tiene herramientas bastante buenas, como son más antiguos que la tecnología en sí.

Herrumbre


El nuevo lenguaje Rust, que también está siendo desarrollado por Mozilla, ahora está alcanzando y superando a C ++ en términos de herramientas. Todo va al punto de que harán el mejor proceso de desarrollo para WebAssembly.

Lua, Perl, Python, PHP, etc.


Casi todos los lenguajes que se interpretan también están disponibles en WebAssembly, ya que sus intérpretes están escritos en C ++, simplemente se compilaron en WebAssembly y ahora puede girar PHP en un navegador.

Ir


En la versión 1.11 hicieron una versión beta de compilación en WebAssembly, en 2.0 prometen soporte de lanzamiento. Su soporte apareció más tarde, porque WebAssembly no es compatible con el recolector de basura, y Go es un lenguaje de memoria administrada. Entonces tuvieron que arrastrar su recolector de basura bajo WebAssembly.

Kotlin / Nativo


Sobre la misma historia con Kotlin. Su compilador tiene soporte experimental, pero también tendrán que hacer algo con el recolector de basura. No sé qué estado hay.

3D-


? , — 3D-. , , asm.js WebAssembly . , WebAssembly.




, : , , . , .





. , , , , . , , ; — .



, Google Chrome, , WebAssembly-. npm- , Wasm, JS. , ++ - — .

HunSpell — Wasm .


— « ». , - , — OpenSSL. WebAssembly. OpenSSL — , , .


use case wotinspector.com. World of Tanks. , , , , , .

— . , , . , , - ++, WebAssembly, ( , ).

. , , . . , , , , . . .


, , ++. , FFmpeg, . , ffmpeg. . , , , , .



— . OpenCV — , WebAssembly, . PDF. SQLite, SQL. SQLite WebAssembly Emscripten, .

Node.js





WebAssembly, Node.js. , Sass — css. Ruby, ++ ( libsass). , Webpack', Node.js. node-sass , JS- .

, , . . :



, node-sass 100 . , ( ) . WebAssembly : , WebAssembly .

Node. , WebAssembly libsass-asm . , . WebAssembly …


Figma — web-. - Sketch, , . ++ ( ), asm.js. , .



WebAssembly, , 3 . , .

Visual Studio Code, , Electron, , , Node-sass. , Node, . , , , WebAssembly.





— AutoCAD. 30 , ++, . , , - JavaScript, , . WebAssembly AutoCAD - , 5 .

, , , , , , , , . FFMpeg — , — QEMU. , , KVM, .



2011 QEMU . , . , Linux , Linux-, , - .

, . bash, , Linux. — GUI . . , , …



, , - . Windows 2000 , , 18 , . , Chrome ( FireFox).

, WebAssembly , , , , .


, WebAssembly. , — , . — , .



, C++ web-. , , — . — , , , .

, . , C++, JavaScript, . , C++. , JS C++, .

— .



CI Pipeline


? JS- , Webpack. , , ( ), JS. webpack watch, , .




, . , , .

Chrome DevTools, Sources wasm-. ( - ), , , .



, , : «, , , , , !». , embedded-, , - .

: -g4 wast- , .



, 100 ( FAR). — , Chrome. E:/_work/bfg/bytefrog/… — . , ++ . , SourceMap!

SourceMap


, .
  • Firefox.
  • --sourcemap-base=http://localhost , SourceMap -, .
  • HTTP.
  • .
  • Windows «:» . .


. CMake , URL -. : wast- , . , .

, :



++ . ! , , stack trace, . , wasm- stack trace, , , , , .



, — SourceMap . , , . , .



«var0».



, . , SourceMap, , .


. Chrome, Firefox. Firefox — «» , , .



Chrome ( , , Mangled ), , , , .




. , :

  • . runtime, . ++ Rust Go.
  • JS — Wasm. , JS Wasm. -, , . , .
  • . , , , .
  • Wasm . Wasm , JS. WebAssembly , .
  • JS.


: .

  • wasp_cpp_bench
  • Chrome 65.0.3325.181 (64-bit)
  • Core i5-4690
  • 24gb ram
  • 5 ; max min;


. JS — , .



++, , - . Grayscale. C++ , . ( ), , JS. , , , ++, .


Sentry, — wasm. , traceKit, Sentry — Raven, — , , wasm . , , , pull request, npm install JS-.



. production, , . debug-, , :




  • WebAssembly , .
  • — . 8 , C++, , .
  • , , WebAssembly — .
  • — JS. JS- , «» , , .


, :
  • Emscripten Embind. .
  • - Emscripten — . , , 3000 Emscripten.
  • Sentry.
  • Firefox.


Gracias por su atencion! .



HolyJS, : 24-25 HolyJS . (, Node.js Ryan Dahl!), — 1 .

Source: https://habr.com/ru/post/441140/


All Articles