Développement WebAssembly: véritable rùteau et exemples



L'annonce de WebAssembly a eu lieu en 2015 - mais maintenant, aprĂšs des annĂ©es, il y en a encore peu qui peuvent s'en vanter en production. Les documents sur une telle expĂ©rience sont d’autant plus prĂ©cieux: des informations de premiĂšre main sur la maniĂšre de vivre avec elle dans la pratique sont encore rares.

Lors de la conférence HolyJS, un rapport sur l'expérience de l'utilisation de WebAssembly a reçu des notes élevées du public, et maintenant une version texte de ce rapport a été préparée spécialement pour Habr (une vidéo est également jointe).



Je m'appelle Andrey, je vais vous parler de WebAssembly. On peut dire que j'ai commencĂ© Ă  m'engager sur le web au siĂšcle dernier, mais je suis modeste, donc je ne le dirai pas. Pendant ce temps, j'ai rĂ©ussi Ă  travailler Ă  la fois sur le backend et le frontend, et j'ai mĂȘme dessinĂ© un peu de design. Aujourd'hui, je m'intĂ©resse Ă  des choses comme WebAssembly, C ++ et d'autres choses natives. J'aime aussi beaucoup la typographie et collectionne les anciennes technologies.

Tout d'abord, je parlerai de la façon dont l'Ă©quipe et moi avons implĂ©mentĂ© WebAssembly dans notre projet, puis nous verrons si vous avez besoin de quelque chose de WebAssembly, et terminerons avec quelques conseils au cas oĂč vous souhaiteriez l'implĂ©menter vous-mĂȘme.

Comment nous avons implémenté WebAssembly


Je travaille pour Inetra, nous sommes situés à Novossibirsk et réalisons certains de nos propres projets. L'un d'eux est ByteFog. Il s'agit d'une technologie peer-to-peer pour fournir des vidéos aux utilisateurs. Nos clients sont des services qui distribuent une énorme quantité de vidéos. Ils ont un problÚme: quand un événement populaire se produit, par exemple, une conférence de presse ou un événement sportif, comment ne pas s'y préparer, un tas de clients arrivent, s'appuient sur le serveur et le serveur est triste. Les clients reçoivent actuellement une trÚs mauvaise qualité vidéo.

Mais tout le monde regarde le mĂȘme contenu. Demandons aux appareils voisins des utilisateurs de partager des morceaux de vidĂ©o, puis nous dĂ©chargerons le serveur, Ă©conomiserons la bande passante et les utilisateurs recevront la vidĂ©o en meilleure qualitĂ©. Ces nuages ​​sont notre technologie, notre serveur proxy ByteFog.



Nous devons ĂȘtre installĂ©s sur tous les appareils capables d'afficher de la vidĂ©o, c'est pourquoi nous prenons en charge une trĂšs large gamme de plates-formes: Windows, Linux, Android, iOS, Web, Tizen. Quelle langue choisir pour avoir une base de code unique sur toutes ces plateformes? Nous avons choisi C ++ car il s'est avĂ©rĂ© avoir le plus d'avantages :-D Plus sĂ©rieusement, nous avons une bonne expertise en C ++, c'est vraiment un langage rapide, et en portabilitĂ© c'est probablement juste derriĂšre C.

Nous avons obtenu une assez grosse application (900 classes), mais cela fonctionne bien. Sous Windows et Linux, nous compilons en code natif. Pour Android et iOS, nous construisons une bibliothĂšque que nous connectons Ă  l'application. Nous parlerons de Tizen une autre fois, mais sur le Web, nous travaillions comme plug-in de navigateur.

Il s'agit de la technologie d'API Netscape Plugin. Comme son nom l'indique, il est assez ancien et présente également un inconvénient: il donne un accÚs trÚs large au systÚme, donc le code utilisateur peut poser un problÚme de sécurité. C'est probablement pourquoi Chrome a désactivé la prise en charge de cette technologie en 2015, puis tous les navigateurs ont rejoint ce flash mob. Nous nous sommes donc retrouvés sans version Web pendant prÚs de deux ans.

En 2017, un nouvel espoir est venu. Comme vous pouvez l'imaginer, il s'agit de WebAssembly. En consĂ©quence, nous nous sommes fixĂ© pour tĂąche de porter notre application sur un navigateur. Étant donnĂ© que la prise en charge de Firefox et Chrome est dĂ©jĂ  apparue au printemps, et Ă  l'automne 2017, Edge et Safari se sont relevĂ©s.

Il Ă©tait important pour nous d'utiliser le code prĂȘt Ă  l'emploi, car nous avons beaucoup de logique mĂ©tier que nous ne voulions pas doubler, afin de ne pas doubler le nombre de bogues. Prenez le compilateur Emscripten. Il fait ce dont nous avons besoin - compile l'application positive dans le navigateur et recrĂ©e l'environnement familier de l'application native dans le navigateur. Nous pouvons dire que Emscripten est un tel code Browserify pour C ++. Il vous permet Ă©galement de transfĂ©rer des objets de C ++ vers JavaScript et vice versa. Notre premiĂšre pensĂ©e a Ă©tĂ©: prenons maintenant Emscripten, compilons simplement, et tout fonctionnera. Bien sĂ»r que non. De lĂ  a commencĂ© notre voyage le long du rĂąteau.

La premiÚre chose que nous avons rencontrée était la dépendance. Il y avait plusieurs bibliothÚques dans notre base de code. Maintenant, cela n'a aucun sens de les énumérer, mais pour ceux qui comprennent, nous avons Boost. Il s'agit d'une grande bibliothÚque qui vous permet d'écrire du code multiplateforme, mais il est trÚs difficile de configurer la compilation avec. Je voulais faire glisser le moins de code possible dans le navigateur.

Architecture Bytefog


En consĂ©quence, nous avons identifiĂ© le noyau: nous pouvons dire qu'il s'agit d'un serveur proxy qui contient la logique mĂ©tier principale. Ce serveur proxy prend les donnĂ©es de deux sources. Le premier et le principal est HTTP, c'est-Ă -dire un canal vers le serveur de distribution vidĂ©o, le second est notre rĂ©seau P2P, c'est-Ă -dire un canal vers un autre mĂȘme proxy d'un autre utilisateur. Nous donnons les donnĂ©es principalement au joueur, car notre tĂąche est de montrer un contenu de haute qualitĂ© Ă  l'utilisateur. S'il reste des ressources, nous distribuons le contenu au rĂ©seau P2P afin que d'autres utilisateurs puissent le tĂ©lĂ©charger. À l'intĂ©rieur, il y a une cache intelligente qui fait toute la magie.



AprĂšs avoir compilĂ© tout cela, nous sommes confrontĂ©s au fait que WebAssembly est exĂ©cutĂ© dans le sandbox du navigateur. Cela signifie qu'il ne peut pas faire plus que ce que JavaScript donne. Alors que les applications natives utilisent beaucoup de choses spĂ©cifiques Ă  la plate-forme, comme un systĂšme de fichiers, un rĂ©seau ou des nombres alĂ©atoires. Toutes ces fonctionnalitĂ©s devront ĂȘtre implĂ©mentĂ©es en JavaScript en utilisant ce que le navigateur nous donne. Cette plaque rĂ©pertorie les remplacements assez Ă©vidents rĂ©pertoriĂ©s.



Pour rendre cela possible, il est nĂ©cessaire de scier l'implĂ©mentation des capacitĂ©s natives dans une application native et d'y insĂ©rer une interface, c'est-Ă -dire tracer une certaine frontiĂšre. Ensuite, vous implĂ©mentez cela en JavaScript et quittez l'implĂ©mentation native, et dĂ©jĂ  pendant l'assemblage, celle nĂ©cessaire est sĂ©lectionnĂ©e. Nous avons donc regardĂ© notre architecture et trouvĂ© tous les endroits oĂč cette frontiĂšre peut ĂȘtre tracĂ©e. Par coĂŻncidence, il s'agit d'un sous-systĂšme de transport.



Pour chacun de ces endroits, nous avons défini une spécification, c'est-à-dire que nous avons fixé un contrat: quelles méthodes seront, quels paramÚtres ils auront, quels types de données. Une fois que vous avez fait cela, vous pouvez travailler en parallÚle, chaque développeur de son cÎté.

Quel est le résultat? Nous avons remplacé le principal canal de diffusion vidéo du fournisseur par l'AJAX habituel. Nous transmettons des données au lecteur via la bibliothÚque populaire HLS.js, mais il y a une possibilité fondamentale de s'intégrer avec d'autres joueurs, si nécessaire. Nous avons remplacé la totalité de la couche P2P par WebRTC.



À la suite de la compilation, plusieurs fichiers sont obtenus. Le plus important est le .wasm binaire. Il contient le bytecode compilĂ© que le navigateur exĂ©cutera et qui contient tout votre hĂ©ritage C ++. Mais en soi, cela ne fonctionne pas, le soi-disant «code de colle» est nĂ©cessaire, il est Ă©galement gĂ©nĂ©rĂ© par le compilateur. Le code de colle tĂ©lĂ©charge un fichier binaire et vous tĂ©lĂ©chargez ces deux fichiers en production. À des fins de dĂ©bogage, vous pouvez gĂ©nĂ©rer une reprĂ©sentation textuelle de l'assembleur - un fichier .wast et une carte source. Vous devez comprendre qu'ils peuvent ĂȘtre trĂšs volumineux. Dans notre cas, ils ont atteint 100 mĂ©gaoctets ou plus.

Collecte du bundle


Examinons de plus prĂšs le code de la colle. C'est le bon vieux ES5 habituel, assemblĂ© en un seul fichier. Lorsque nous le connectons Ă  une page Web, nous avons une variable globale qui contient l'ensemble de notre module wasm instanciĂ©, qui est prĂȘt Ă  accepter les demandes Ă  son API.

Mais l'inclusion d'un fichier séparé est une complication assez sérieuse pour la bibliothÚque que les utilisateurs utiliseront. Nous aimerions tout mettre dans un seul paquet. Pour cela, nous utilisons Webpack et une option de compilation spéciale MODULARIZE.

Il enveloppe le code adhĂ©sif dans le motif «Module», et nous pouvons le rĂ©cupĂ©rer: importez ou utilisez le besoin si nous Ă©crivons sur ES5 - Webpack comprend calmement cette dĂ©pendance. Il y avait un problĂšme avec Babel - il n'aimait pas la grande quantitĂ© de code, mais c'est un code ES5, il n'a pas besoin d'ĂȘtre transposĂ©, nous l'ajoutons juste pour l'ignorer.

À la recherche du nombre de fichiers, j'ai dĂ©cidĂ© d'utiliser l'option SINGLE_FILE. Il traduit tous les fichiers binaires rĂ©sultant de la compilation dans le formulaire Base64 et les pousse dans le code adhĂ©sif sous forme de chaĂźne. Cela semble ĂȘtre une excellente idĂ©e, mais aprĂšs cela, le bundle est devenu de 100 mĂ©gaoctets. Ni Webpack, ni Babel, ni mĂȘme le navigateur ne fonctionnent sur un tel volume. Quoi qu'il en soit, nous ne forcerons pas l'utilisateur Ă  charger 100 mĂ©gaoctets?!

Si vous y rĂ©flĂ©chissez, cette option n'est pas nĂ©cessaire. Le code adhĂ©sif tĂ©lĂ©charge les fichiers binaires de lui-mĂȘme. Il le fait via HTTP, donc nous obtenons la mise en cache prĂȘte Ă  l'emploi, nous pouvons dĂ©finir tous les en-tĂȘtes que nous voulons, par exemple, activer la compression, et les fichiers WebAssembly sont parfaitement compressĂ©s.

Mais la technologie la plus cool est la compilation en streaming. Autrement dit, le fichier WebAssembly, lors du tĂ©lĂ©chargement Ă  partir du serveur, peut dĂ©jĂ  ĂȘtre compilĂ© dans le navigateur Ă  mesure que les donnĂ©es arrivent, ce qui accĂ©lĂšre considĂ©rablement le chargement de votre application. En gĂ©nĂ©ral, toute la technologie WebAssembly met l'accent sur le dĂ©marrage rapide d'une grande base de code.

Thématique


Un autre problĂšme avec le module est qu'il s'agit d'un objet Thenable, c'est-Ă -dire qu'il a une mĂ©thode .then (). Cette fonction vous permet de suspendre un rappel au dĂ©marrage du module, et c'est trĂšs pratique. Mais je voudrais que l'interface corresponde Ă  Promise. ThĂ©rapeutique n'est pas une promesse, mais ça va, terminons nous-mĂȘmes. Écrivons un code aussi simple:

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

Nous crĂ©ons Promise, dĂ©marrons notre module et, en tant que rappel, nous appelons la fonction de rĂ©solution et transmettons le module que nous y avons installĂ©. Tout semble ĂȘtre Ă©vident, tout va bien, nous lançons - quelque chose ne va pas, notre navigateur est gelĂ©, nos DevTools sont suspendus et le processeur chauffe sur l'ordinateur. Nous ne comprenons rien - une sorte de rĂ©cursivitĂ© ou une boucle infinie. Le dĂ©bogage est assez difficile, et lorsque nous avons interrompu JavaScript, nous nous sommes retrouvĂ©s dans la fonction Then du module Emscripten.

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

Examinons-le plus en détail. Terrain

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

responsable de suspendre un rappel. Tout est clair ici: une fonction asynchrone qui appelle notre rappel. Tout ce que nous voulons. Il y a une autre partie à cette fonctionnalité.

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

Il est appelĂ© lorsque le module a dĂ©jĂ  dĂ©marrĂ©. Ensuite, le rappel est appelĂ© de maniĂšre synchrone immĂ©diatement et le module lui est transmis dans le paramĂštre. Cela imite le comportement de Promise, et cela semble ĂȘtre ce que nous attendons. Mais alors qu'est-ce qui ne va pas?

Si vous lisez attentivement la documentation, il s'avĂšre qu'il y a un point trĂšs subtil Ă  propos de Promise. Lorsque nous rĂ©solvons la promesse Ă  l'aide d'un Thenable, le navigateur dĂ©ballera les valeurs de ce Thenable et pour ce faire, il appellera la mĂ©thode .then (). En consĂ©quence, nous rĂ©solvons la promesse, lui passons le module. Le navigateur demande: est-ce donc un objet? Oui, c'est un Thenable. Ensuite, la fonction .then () est appelĂ©e sur le module, et la fonction de rĂ©solution elle-mĂȘme est passĂ©e en tant que rappel.

Le module vĂ©rifie s'il est en cours d'exĂ©cution. Il est dĂ©jĂ  en cours d'exĂ©cution, donc le rappel est appelĂ© immĂ©diatement et le mĂȘme module lui est Ă  nouveau transmis. En rappel, nous avons la fonction de rĂ©solution, et le navigateur demande: est-ce un objet Thenable? Oui, c'est un Thenable. Et tout recommence. En consĂ©quence, nous tombons dans un cycle sans fin dont le navigateur ne revient jamais.



Je n'ai pas trouvé de solution élégante à ce problÚme. Par conséquent, je supprime simplement la méthode .then () avant de résoudre, et cela fonctionne.

Emscripten


Nous avons donc compilé le module, assemblé JS, mais il manque quelque chose. Nous devons probablement faire un travail utile. Pour ce faire, transférez des données et connectez les deux mondes - JS et C ++. Comment faire Emscripten propose trois options:

  • Le premier est les fonctions ccall et cwrap. Le plus souvent, vous les rencontrerez dans certains didacticiels sur WebAssembly, mais ils ne conviennent pas au travail rĂ©el, car ils ne prennent pas en charge les capacitĂ©s de C ++.
  • Le second est WebIDL Binder. Il prend dĂ©jĂ  en charge les fonctions C ++, vous pouvez dĂ©jĂ  travailler avec. Il s'agit d'un langage de description d'interface sĂ©rieux utilisĂ©, par exemple, par le W3C pour leur documentation. Mais nous ne voulions pas l'intĂ©grer dans notre projet et avons utilisĂ© la troisiĂšme option
  • Embind. Nous pouvons dire que c'est une façon native de connecter des objets pour Emscripten, elle est basĂ©e sur des modĂšles C ++ et vous permet de faire beaucoup de choses en transmettant diffĂ©rentes entitĂ©s de C ++ Ă  JS et vice versa.


Embind vous permet de:

  • Appeler des fonctions C ++ Ă  partir du code JavaScript
  • CrĂ©er des objets JS Ă  partir d'une classe C ++
  • A partir du code C ++, tournez-vous vers l'API du navigateur (si pour une raison quelconque vous le souhaitez, vous pouvez, par exemple, Ă©crire l'intĂ©gralitĂ© du framework frontal en C ++).
  • L'essentiel pour nous: implĂ©menter l'interface JavaScript dĂ©crite en C ++.


Échange de donnĂ©es


Le dernier point est important, car c'est exactement l'action que vous effectuerez constamment lors du portage de l'application. Je voudrais donc m'y attarder plus en détail. Maintenant, il y aura du code C ++, mais n'ayez pas peur, c'est presque comme TypeScript :-D

Le schéma est le suivant:



Du cĂŽtĂ© C ++, il y a un noyau auquel nous voulons donner accĂšs, par exemple, Ă  un rĂ©seau externe - pour tĂ©lĂ©charger la vidĂ©o. Il le faisait Ă  l'aide de sockets natifs, il y avait une sorte de client HTTP qui faisait cela, mais il n'y a pas de sockets natifs dans WebAssembly. Nous devons en quelque sorte sortir, donc nous avons coupĂ© l'ancien client HTTP, insĂ©rĂ© l'interface Ă  cet endroit et implĂ©mentĂ© cette interface en JavaScript en utilisant AJAX normal, de quelque maniĂšre que ce soit. AprĂšs cela, nous transmettrons l'objet rĂ©sultant Ă  C ++, oĂč le noyau l'utilisera.

Faisons le client HTTP le plus simple qui ne puisse faire que des requĂȘtes get:

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

À l'entrĂ©e, il reçoit une chaĂźne avec l'URL Ă  tĂ©lĂ©charger et Ă  la sortie
une chaßne avec le résultat de la demande. En C ++, les chaßnes peuvent avoir des données binaires, donc cela convient à la vidéo. Emscripten nous fait écrire ici
un Wrapper effrayant:



Dans ce document, la chose principale est deux choses - le nom de la fonction du cÎté C ++ (je les ai marqués en vert) et les noms correspondants du cÎté JavaScript (je les ai marqués en bleu). En conséquence, nous rédigeons une déclaration de communication:



Il fonctionne comme des blocs Lego, Ă  partir desquels nous l'assemblons. Nous avons une classe, cette classe a une mĂ©thode, et nous voulons hĂ©riter de cette classe pour implĂ©menter l'interface. C’est tout. Nous allons Ă  JavaScript et hĂ©ritons. Cela peut se faire de deux maniĂšres. Le premier est d'Ă©tendre. Ceci est trĂšs similaire Ă  la bonne vieille extension de Backbone.



Le module contient tout ce que Emscripten a compilé, et il a une propriété avec une interface exportée. Nous appelons la méthode extend et y passons un objet avec l'implémentation de cette méthode, c'est-à-dire qu'une méthode sera implémentée dans la fonction get
Obtenez des informations en utilisant AJAX.

En sortie, extend nous donne un constructeur JavaScript standard. Nous pouvons l'appeler autant de fois que nĂ©cessaire et gĂ©nĂ©rer des objets dans la quantitĂ© dont nous avons besoin. Mais il y a une situation oĂč nous avons un objet, et nous voulons juste le passer du cĂŽtĂ© C ++.



Pour ce faire, liez en quelque sorte cet objet Ă  un type que C ++ comprendra. C'est ce que fait la fonction d'implĂ©mentation. En sortie, il ne donne pas un constructeur, mais un objet prĂȘt Ă  l'emploi, notre client, que nous pouvons redonner au C ++. Vous pouvez le faire, par exemple, comme ceci:

 var app = Module.makeApp(client, 
) 

Supposons que nous ayons une fabrique qui crée notre application et qui prend ses dépendances en paramÚtres, par exemple, client et autre chose. Lorsque cette fonction fonctionne, nous obtenons l'objet de notre application, qui contient déjà l'API dont nous avons besoin. Vous pouvez faire le contraire:

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

Directement à partir de C ++, sortez notre client de la portée globale du navigateur. De plus, à la place du client, il peut y avoir n'importe quelle API de navigateur, à partir de la console, se terminant par l'API DOM, WebRTC - tout ce que vous voulez. Ensuite, nous appelons les méthodes de cet objet, et nous encapsulons toutes les valeurs dans la classe magique val, qu'Emscripten nous fournit.

Erreurs contraignantes


En général, c'est tout, mais lorsque vous démarrez le développement, des erreurs de liaison vous attendent. Ils ressemblent à ceci:



Emscripten essaie de nous aider et d'expliquer ce qui ne va pas. Si tout cela est résumé, vous devez vous assurer qu'elles coïncident (il est facile de sceller et d'obtenir une erreur de liaison):

  • Noms
  • Les types
  • Nombre de paramĂštres

La syntaxe Embind est inhabituelle non seulement pour les fournisseurs frontaux, mais aussi pour les personnes qui traitent avec C ++. Il s'agit d'une sorte de DSL dans laquelle il est facile de se tromper, il faut suivre cela. En parlant d'interfaces, lorsque vous implémentez une sorte d'interface en JavaScript, il est nécessaire qu'elle corresponde exactement à ce que vous décrivez dans votre contrat.

Nous avons eu un cas intéressant. Mon collÚgue Jura, qui était impliqué dans le projet cÎté C ++, a utilisé Extend pour tester ses modules. Ils travaillaient parfaitement pour lui, alors il les a commis et me les a transmis. J'ai utilisé un outil pour intégrer ces modules dans un projet JS. Et ils ont cessé de travailler pour moi. Lorsque nous l'avons compris, il s'est avéré que lors de la liaison dans les noms des fonctions, nous avons obtenu une faute de frappe.

Comme son nom l'indique, Extend est une extension de l'interface, donc si vous l'avez scellée quelque part, Extend ne générera pas d'erreur, il décidera que vous venez d'ajouter une nouvelle méthode, et c'est trÚs bien.

Autrement dit, il masque les erreurs de liaison jusqu'Ă  ce que la mĂ©thode elle-mĂȘme soit appelĂ©e. Je suggĂšre d'utiliser Implement dans tous les cas oĂč cela vous convient, car il vĂ©rifie immĂ©diatement l'exactitude de l'interface transmise. Mais si vous avez besoin d'Extend, vous devez couvrir avec des tests l'appel de chaque mĂ©thode afin de ne pas le gĂącher.

Étendre et ES6


Un autre problÚme avec Extend est qu'il ne prend pas en charge les classes ES6. Lorsque vous héritez d'un objet dérivé d'une classe ES6, Extend s'attend à ce que toutes les propriétés soient énumérables, mais pas avec ES6. Les méthodes sont dans le prototype et elles ont une énumération: false. J'utilise une béquille comme celle-ci, dans laquelle je passe en revue le prototype et allume énumérable: true:

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

J'espÚre qu'un jour je pourrai m'en débarrasser, car il est question dans la communauté Emscripten d'améliorer le support pour ES6.

RAM


En parlant de C ++, on ne peut s'empĂȘcher de mentionner la mĂ©moire. Lorsque nous avons tout vĂ©rifiĂ© sur une vidĂ©o de qualitĂ© SD, tout allait bien avec nous, cela a fonctionnĂ© parfaitement! DĂšs que nous avons fait le test FullHD, il y avait un manque d'erreur de mĂ©moire. Peu importe, il y a l'option TOTAL_MEMORY, qui dĂ©finit la valeur de mĂ©moire de dĂ©part pour le module. Nous avons fait un demi-gigaoctet, tout va bien, mais c'est en quelque sorte inhumain pour les utilisateurs, car nous rĂ©servons la mĂ©moire Ă  tout le monde, mais tout le monde n'a pas d'abonnement au contenu FullHD.

Il existe une autre option - ALLOW_MEMORY_GROWTH. Il vous permet de développer la mémoire
progressivement au besoin. Cela fonctionne comme ceci: Emscripten par dĂ©faut donne au module 16 mĂ©gaoctets pour le fonctionnement. Lorsque vous les avez tous utilisĂ©s, une nouvelle mĂ©moire est allouĂ©e. Toutes les anciennes donnĂ©es y sont copiĂ©es et vous avez toujours la mĂȘme quantitĂ© d'espace pour les nouvelles. Cela se produit jusqu'Ă  ce que vous atteigniez 4 Go.

Supposons que vous ayez alloué 256 mégaoctets de mémoire, mais vous savez avec certitude que vous pensiez que votre application en a suffisamment 192. Le reste de la mémoire sera alors utilisé de maniÚre inefficace. Vous l'avez mis en surbrillance, vous l'avez pris à l'utilisateur, mais vous n'en faites rien. Je voudrais en quelque sorte éviter cela. Il y a une petite astuce: nous commençons à travailler avec la mémoire augmentée d'une fois et demie. Ensuite, dans la troisiÚme étape, nous atteignons 192 mégaoctets, et c'est exactement ce dont nous avons besoin. Nous avons réduit la consommation de mémoire par ce reste et économisé l'allocation de mémoire inutile, et plus, plus cela prend de temps. Par conséquent, je recommande d'utiliser ces deux options ensemble.

Injection de dépendance


Il semblerait que c'était tout, mais le rùteau est allé un peu plus. Il y a un problÚme avec l'injection de dépendance. Nous écrivons la classe la plus simple dans laquelle une dépendance est nécessaire.

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

Par exemple, nous transmettons notre client HTTP à notre application. Nous économisons dans la propriété de classe. Il semblerait que tout fonctionnera bien.

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

Nous hĂ©ritons de l'interface C ++, crĂ©ons d'abord notre objet, lui transmettons la dĂ©pendance, puis hĂ©ritons. Au moment de l'hĂ©ritage, Emscripten fait quelque chose d'incroyable avec l'objet. Il est plus facile de penser qu'il tue un ancien objet, en crĂ©e un nouveau basĂ© sur son modĂšle et y fait glisser toutes les mĂ©thodes publiques. Mais en mĂȘme temps, l'Ă©tat de l'objet est perdu et vous obtenez un objet qui n'est pas formĂ© et ne fonctionne pas correctement. La rĂ©solution de ce problĂšme est assez simple. Il est nĂ©cessaire d'utiliser un constructeur qui fonctionne aprĂšs la phase d'hĂ©ritage.

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

Nous faisons presque la mĂȘme chose: nous stockons la dĂ©pendance dans le champ de l'objet, mais c'est l'objet qui s'est avĂ©rĂ© aprĂšs l'hĂ©ritage. Nous ne devons pas oublier de transmettre l'appel du constructeur Ă  l'objet parent, qui se trouve du cĂŽtĂ© C ++. La derniĂšre ligne est un analogue de la mĂ©thode super () dans ES6. Voici comment l'hĂ©ritage se produit dans ce cas:

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

Tout d'abord, nous héritons, puis créons un nouvel objet dans lequel la dépendance est déjà passée, et cela fonctionne.

Truc de pointeur


Un autre problÚme est de passer des objets par pointeur de C ++ vers JavaScript. Nous avons déjà fait un client HTTP. Par souci de simplicité, nous avons manqué un détail important.

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

La mĂ©thode renvoie immĂ©diatement la valeur, c'est-Ă -dire qu'il s'avĂšre que la demande doit ĂȘtre synchrone. Mais aprĂšs tout, AJAX demande AJAX et qu'ils sont asynchrones, donc dans la vraie vie, la mĂ©thode ne retournera rien, ou nous pouvons retourner l'ID de la demande. Mais afin d'avoir quelqu'un pour renvoyer la rĂ©ponse, nous passons l'Ă©couteur comme deuxiĂšme paramĂštre, dans lequel il y aura des rappels Ă  partir de C ++.

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

Dans JS, cela ressemble Ă  ceci:

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

Nous avons une fonction get qui prend cet objet écouteur. Nous commençons le téléchargement du fichier et raccrochons le rappel. Lorsque le fichier est téléchargé, nous extrayons la fonction souhaitée de l'écouteur et lui transmettons le résultat.

Il semblerait que le plan soit bon, mais lorsque la fonction get se terminera, toutes les variables locales seront détruites et avec eux les paramÚtres de la fonction, c'est-à-dire que le pointeur sera détruit et que runtime emscripten détruira l'objet du cÎté C ++.

Par conséquent, lorsqu'il s'agit d'appeler la ligne listener.onResult (result), l'écouteur n'existera plus et lors de l'accÚs, une erreur d'accÚs à la mémoire se produira et entraßnera le blocage de l'application.

Je voudrais éviter cela, et il y a une solution, mais il a fallu plusieurs semaines pour la trouver.

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

Il s'avÚre qu'il existe une méthode pour cloner un pointeur. Pour une raison quelconque, il n'est pas documenté, mais il fonctionne correctement et vous permet d'augmenter le nombre de références dans le pointeur Emscripten. Cela nous permet de le suspendre dans une fermeture, puis, lorsque nous lançons notre rappel, notre écouteur sera accessible par ce pointeur et nous pourrons travailler selon nos besoins.

La chose la plus importante est de ne pas oublier de supprimer ce pointeur, sinon cela entraßnera une erreur de fuite de mémoire, ce qui est trÚs mauvais.

Écriture rapide dans la mĂ©moire


Lorsque nous téléchargeons des vidéos, ce sont des quantités relativement importantes d'informations, et je voudrais réduire la quantité de données de copie dans les deux sens afin d'économiser de la mémoire et du temps. Il existe une astuce pour écrire une grande quantité d'informations directement dans la mémoire de WebAssembly à partir de 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 est nos données sous forme de tableau typé. Nous pouvons prendre sa longueur et demander l'allocation de mémoire de la taille dont nous avons besoin depuis le module WebAssembly. La fonction malloc nous renverra un pointeur, qui est juste l'index du tableau qui contient toute la mémoire dans WebAssembly. Du cÎté de JavaScript, il ressemble à un ArrayBuffer.

À l'Ă©tape suivante, nous allons couper une fenĂȘtre dans ce ArrayBuffer de la bonne taille Ă  partir d'un certain endroit et y copier nos donnĂ©es. MalgrĂ© le fait que l'opĂ©ration set ait une sĂ©mantique de copie, quand j'ai regardĂ© cette section dans le profileur, je n'ai pas vu un long processus. Je pense que le navigateur optimise cette opĂ©ration Ă  l'aide de la sĂ©mantique de mouvement, c'est-Ă -dire transfĂšre la propriĂ©tĂ© de la mĂ©moire d'un objet Ă  un autre.

Et dans notre application, nous comptons également sur la sémantique de déplacement pour économiser la copie de mémoire.

Adblock


Un problÚme intéressant, plutÎt, sur le changement, avec Adblock. Il s'avÚre qu'en Russie, tous les bloqueurs populaires reçoivent un abonnement à RU Adlist, et il a une telle rÚgle merveilleuse qui interdit de télécharger WebAssembly à partir de sites tiers. Par exemple, avec un CDN.



La solution n'est pas d'utiliser le CDN, mais de tout stocker sur votre domaine (cela ne nous convient pas). Ou renommez le fichier .wasm pour qu'il ne corresponde pas à cette rÚgle. Vous pouvez toujours aller sur le forum de ces camarades et essayer de les convaincre de supprimer cette rÚgle. Je pense qu'ils se justifient en combattant les mineurs de cette façon, bien que je ne sache pas pourquoi les mineurs ne peuvent pas deviner de renommer le fichier.

La production


En consĂ©quence, nous sommes entrĂ©s en production. Oui, ce n'Ă©tait pas facile, cela a pris 8 mois et je veux me demander si ça valait le coup. À mon avis - cela valait la peine:

Pas besoin d'installer


Nous avons obtenu que notre code soit livrĂ© Ă  l'utilisateur sans installer de programme. Lorsque nous avions un plug-in de navigateur, l'utilisateur devait le tĂ©lĂ©charger et l'installer, et c'est un Ă©norme filtre pour la distribution de technologies. Maintenant, l'utilisateur regarde simplement la vidĂ©o sur le site et ne comprend mĂȘme pas que toute une machine fonctionne sous le capot, et que tout y est compliquĂ©. Le navigateur tĂ©lĂ©charge simplement un fichier supplĂ©mentaire avec le code, comme une image ou .css.

Base de code unifiée et débogage sur différentes plateformes


Dans le mĂȘme temps, nous avons pu maintenir notre base de code unique. Nous pouvons tordre le mĂȘme code sur diffĂ©rentes plates-formes et il est arrivĂ© Ă  plusieurs reprises que des bogues invisibles sur l'une des plates-formes apparaissent sur l'autre. Et ainsi, nous pouvons dĂ©tecter des bugs cachĂ©s avec diffĂ©rents outils sur diffĂ©rentes plateformes.

Libération rapide


Nous avons obtenu une version rapide, car nous pouvons ĂȘtre publiĂ©s comme une simple application Web et mettre Ă  jour le code C ++ Ă  chaque nouvelle version. Cela ne se compare pas Ă  la façon de publier de nouveaux plugins, une application mobile ou une application SmartTV. La sortie ne dĂ©pend que de nous: quand on veut, alors elle sortira.

Rétroaction rapide


Et cela signifie une rétroaction rapide: si quelque chose ne va pas, nous pouvons découvrir pendant la journée qu'il y a un problÚme et y répondre.

Je pense que tous ces problĂšmes valaient ces avantages. Tout le monde n'a pas d'application C ++, mais si vous en avez une et que vous voulez qu'elle soit dans le navigateur - WebAssembly est un cas d'utilisation Ă  100% pour vous.

OĂč postuler


Tout le monde n'Ă©crit pas en C ++. Mais non seulement C ++ est disponible pour WebAssembly. Oui, il s'agit historiquement de la toute premiĂšre plate-forme encore disponible dans asm.js, une des premiĂšres technologies de Mozilla. Soit dit en passant, il a donc de trĂšs bons outils, comme ils sont plus anciens que la technologie elle-mĂȘme.

Rouille


Le nouveau langage Rust, également développé par Mozilla, rattrape et dépasse désormais le C ++ en termes d'outils. Tout va au point qu'ils feront le processus de développement le plus cool pour WebAssembly.

Lua, Perl, Python, PHP, etc.


Presque tous les langages interprétés sont également disponibles dans WebAssembly, car leurs interprÚtes sont écrits en C ++, ils ont simplement été compilés dans WebAssembly et vous pouvez maintenant tordre PHP dans un navigateur.

Allez


Dans la version 1.11, ils ont fait une version bĂȘta de la compilation dans WebAssembly, dans 2.0, ils promettent un support de publication. Leur prise en charge est apparue plus tard, car WebAssembly ne prend pas en charge le garbage collector et Go est un langage Ă  mĂ©moire gĂ©rĂ©e. Ils ont donc dĂ» faire glisser leur garbage collector sous WebAssembly.

Kotlin / Native


À propos de la mĂȘme histoire avec Kotlin. Leur compilateur a un support expĂ©rimental, mais ils devront Ă©galement faire quelque chose avec le garbage collector. Je ne sais pas quel est le statut.

Graphiques 3D


À quoi d'autre pouvez-vous penser? La premiĂšre chose qui tourne dans le langage, ce sont les applications 3D. Et, en effet, historiquement, asm.js et WebAssembly ont commencĂ© avec le portage de jeux sur les navigateurs. Et il n'est pas surprenant que tous les moteurs populaires soient dĂ©sormais exportĂ©s vers WebAssembly.



Traitement des données localement


Vous pouvez Ă©galement penser Ă  traiter les donnĂ©es des utilisateurs directement dans son navigateur, sur son ordinateur: prendre une image tĂ©lĂ©chargĂ©e ou Ă  partir d'un appareil photo, enregistrer du son, traiter une vidĂ©o. Lisez l'archive tĂ©lĂ©chargĂ©e par l'utilisateur ou rĂ©cupĂ©rez-la vous-mĂȘme Ă  partir d'un tas de fichiers et tĂ©lĂ©chargez-la sur le serveur en une seule demande.

Réseaux de neurones





. , , , , . , , ; — .



, 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.


Merci de votre attention! .



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

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


All Articles