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 鈥嬧媓acer 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(鈥砪lient鈥); client.call<std::string>(鈥砱et鈥, 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 鈥嬧媔mplementar 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( 鈥矨pp鈥, 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( 鈥矨pp鈥, 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