WebAssembly-Entwicklung: echter Rechen und Beispiele



Die Ankündigung von WebAssembly erfolgte im Jahr 2015 - aber jetzt, nach Jahren, gibt es nur noch wenige, die sich in der Produktion damit rühmen können. Die Materialien zu solchen Erfahrungen sind umso wertvoller: Informationen darüber, wie man in der Praxis damit umgeht, sind immer noch Mangelware.

Auf der HolyJS-Konferenz erhielt ein Bericht über die Erfahrungen mit der Verwendung von WebAssembly gute Noten vom Publikum, und jetzt wurde eine Textversion dieses Berichts speziell für Habr erstellt (ein Video ist ebenfalls beigefügt).



Mein Name ist Andrey, ich werde Ihnen von WebAssembly erzählen. Wir können sagen, dass ich im letzten Jahrhundert angefangen habe, mich mit dem Internet zu beschäftigen, aber ich bin bescheiden, also werde ich das nicht sagen. In dieser Zeit konnte ich sowohl am Backend als auch am Frontend arbeiten und sogar ein kleines Design zeichnen. Heute interessiere ich mich für Dinge wie WebAssembly, C ++ und andere native Dinge. Ich liebe auch Typografie und sammle alte Technologie.

Zuerst werde ich darüber sprechen, wie das Team und ich WebAssembly in unserem Projekt implementiert haben, dann werden wir diskutieren, ob Sie etwas von WebAssembly benötigen, und mit ein paar Tipps abschließen, falls Sie es selbst implementieren möchten.

Wie wir WebAssembly implementiert haben


Ich arbeite für Inetra, wir befinden uns in Nowosibirsk und machen einige unserer eigenen Projekte. Einer von ihnen ist ByteFog. Dies ist eine Peer-to-Peer-Technologie zur Bereitstellung von Videos für Benutzer. Unsere Kunden sind Dienste, die eine große Menge an Videos verbreiten. Sie haben ein Problem: Wenn ein beliebtes Ereignis stattfindet, beispielsweise eine Pressekonferenz oder ein Sportereignis, wie man sich nicht darauf vorbereitet, kommen eine Reihe von Kunden herein, stützen sich auf den Server, und der Server ist traurig. Kunden erhalten zu diesem Zeitpunkt eine sehr schlechte Videoqualität.

Aber jeder sieht den gleichen Inhalt. Lassen Sie uns benachbarte Geräte von Benutzern bitten, Videoteile freizugeben. Anschließend werden wir den Server entladen, Bandbreite sparen und Benutzer erhalten Videos in besserer Qualität. Diese Clouds sind unsere Technologie, unser ByteFog-Proxyserver.



Wir müssen auf jedem Gerät installiert sein, das Videos anzeigen kann. Daher unterstützen wir eine Vielzahl von Plattformen: Windows, Linux, Android, iOS, Web, Tizen. Welche Sprache soll gewählt werden, um auf allen diesen Plattformen eine einzige Codebasis zu haben? Wir haben uns für C ++ entschieden, weil es die meisten Vorteile hat :-D Im Ernst, wir haben gute Kenntnisse in C ++, es ist wirklich eine schnelle Sprache und in Bezug auf die Portabilität ist es wahrscheinlich die zweitwichtigste nach C.

Wir haben eine ziemlich große Anwendung (900 Klassen), aber es funktioniert gut. Unter Windows und Linux kompilieren wir in nativen Code. Für Android und iOS erstellen wir eine Bibliothek, die wir mit der Anwendung verbinden. Wir werden ein anderes Mal über Tizen sprechen, aber im Web haben wir früher als Browser-Plugin gearbeitet.

Dies ist die Netscape Plugin API-Technologie. Wie der Name schon sagt, ist es ziemlich alt und hat auch einen Nachteil: Es bietet einen sehr breiten Zugriff auf das System, sodass Benutzercode ein Sicherheitsproblem verursachen kann. Dies ist wahrscheinlich der Grund, warum Chrome die Unterstützung für diese Technologie im Jahr 2015 deaktiviert hat und dann alle Browser diesem Flash-Mob beigetreten sind. So blieben wir fast zwei Jahre ohne Webversion.

2017 kam eine neue Hoffnung. Wie Sie sich vorstellen können, ist dies WebAssembly. Aus diesem Grund haben wir uns die Aufgabe gestellt, unsere Anwendung auf einen Browser zu portieren. Da die Unterstützung für Firefox und Chrome bereits im Frühjahr und im Herbst 2017 erschien, haben sich Edge und Safari hochgezogen.

Für uns war es wichtig, vorgefertigten Code zu verwenden, da wir eine Menge Geschäftslogik haben, die wir nicht verdoppeln wollten, um die Anzahl der Fehler nicht zu verdoppeln. Nimm den Compiler Emscripten. Er tut, was wir brauchen - kompiliert die positive Anwendung in den Browser und erstellt die Umgebung neu, die der nativen Anwendung im Browser vertraut ist. Wir können sagen, dass Emscripten ein solches Browserify für C ++ - Code ist. Außerdem können Sie Objekte von C ++ an JavaScript weiterleiten und umgekehrt. Unser erster Gedanke war: Nehmen wir jetzt Emscripten, kompilieren Sie einfach und alles wird funktionieren. Natürlich nicht. Von hier aus begann unsere Reise entlang des Rechen.

Das erste, was uns begegnete, war Sucht. In unserer Codebasis befanden sich mehrere Bibliotheken. Jetzt macht es keinen Sinn, sie aufzulisten, aber für diejenigen, die verstehen, haben wir Boost. Dies ist eine große Bibliothek, mit der Sie plattformübergreifenden Code schreiben können, aber es ist sehr schwierig, die Kompilierung damit zu konfigurieren. Ich wollte so wenig Code wie möglich in den Browser ziehen.

Bytefog-Architektur


Als Ergebnis haben wir den Kern identifiziert: Wir können sagen, dass dies ein Proxyserver ist, der die Hauptgeschäftslogik enthält. Dieser Proxyserver nimmt Daten aus zwei Quellen auf. Das erste und wichtigste ist HTTP, dh ein Kanal zum Videoverteilungsserver, das zweite ist unser P2P-Netzwerk, dh ein Kanal zu einem anderen gleichen Proxy von einem anderen Benutzer. Wir geben die Daten in erster Linie an den Player weiter, da es unsere Aufgabe ist, dem Benutzer qualitativ hochwertige Inhalte anzuzeigen. Wenn noch Ressourcen vorhanden sind, verteilen wir den Inhalt an das P2P-Netzwerk, damit andere Benutzer ihn herunterladen können. Im Inneren befindet sich ein intelligenter Cache, der die ganze Magie ausübt.



Nachdem wir dies alles kompiliert haben, sehen wir uns mit der Tatsache konfrontiert, dass WebAssembly in der Browser-Sandbox ausgeführt wird. Das heißt, es kann nicht mehr als JavaScript. Während native Anwendungen viele plattformspezifische Dinge verwenden, wie z. B. ein Dateisystem, ein Netzwerk oder Zufallszahlen. Alle diese Funktionen müssen mit dem, was der Browser uns gibt, in JavaScript implementiert werden. Diese Platte listet die ziemlich offensichtlichen Ersetzungen auf, die aufgelistet sind.



Um dies zu ermöglichen, ist es erforderlich, die Implementierung nativer Funktionen in einer nativen Anwendung abzuschneiden und dort eine Schnittstelle einzufügen, dh einen bestimmten Rand zu zeichnen. Dann implementieren Sie dies in JavaScript und verlassen die native Implementierung, und bereits während der Assembly wird die erforderliche ausgewählt. Also haben wir uns unsere Architektur angesehen und alle Orte gefunden, an denen diese Grenze gezogen werden kann. Zufälligerweise ist dies ein Transportsubsystem.



Für jeden dieser Orte haben wir eine Spezifikation definiert, dh wir haben einen Vertrag festgelegt: Welche Methoden werden verwendet, welche Parameter werden sie haben, welche Datentypen. Sobald Sie dies getan haben, können Sie parallel arbeiten, wobei jeder Entwickler auf seiner Seite steht.

Was ist das Ergebnis? Wir haben den Haupt-Videoübertragungskanal des Anbieters durch den üblichen AJAX ersetzt. Wir geben Daten über die beliebte HLS.js-Bibliothek an den Player aus, es besteht jedoch eine grundlegende Möglichkeit, diese bei Bedarf in andere Player zu integrieren. Wir haben die gesamte P2P-Schicht durch WebRTC ersetzt.



Durch die Kompilierung werden mehrere Dateien erhalten. Das wichtigste ist der binäre .wasm. Es enthält den kompilierten Bytecode, den der Browser ausführt und der Ihr gesamtes C ++ - Erbe enthält. Aber an sich funktioniert es nicht, der sogenannte "Klebercode" ist notwendig, er wird auch vom Compiler generiert. Der Klebercode lädt eine Binärdatei herunter, und Sie laden beide Dateien in die Produktion hoch. Zu Debugging-Zwecken können Sie eine Textdarstellung des Assemblers generieren - eine .wast-Datei und eine Quellkarte. Sie müssen verstehen, dass sie sehr groß sein können. In unserem Fall erreichten sie 100 Megabyte oder mehr.

Das Bündel sammeln


Schauen wir uns den Klebercode genauer an. Dies ist das übliche gute alte ES5, das in einer einzigen Datei zusammengefasst ist. Wenn wir es mit einer Webseite verbinden, haben wir eine globale Variable, die unser gesamtes instanziiertes Wasm-Modul enthält, das bereit ist, Anforderungen an seine API anzunehmen.

Das Einfügen einer separaten Datei ist jedoch eine ziemlich schwerwiegende Komplikation für die Bibliothek, die Benutzer verwenden werden. Wir möchten alles in einem einzigen Bündel zusammenfassen. Hierfür verwenden wir Webpack und eine spezielle Kompilierungsoption MODULARIZE.

Es klebt den Klebercode in das "Modul" -Muster und wir können ihn aufgreifen: Importieren oder Verwenden erfordern, wenn wir auf ES5 schreiben - Webpack versteht diese Abhängigkeit ruhig. Es gab ein Problem mit Babel - er mochte die große Menge an Code nicht, aber dies ist ein ES5-Code, er muss nicht transponiert werden, wir fügen ihn einfach hinzu, um ihn zu ignorieren.

Um die Anzahl der Dateien zu ermitteln, habe ich mich für die Option SINGLE_FILE entschieden. Es übersetzt alle aus der Kompilierung resultierenden Binärdateien in das Base64-Formular und verschiebt sie als Zeichenfolge in den Klebecode. Klingt nach einer großartigen Idee, aber danach wurde das Bundle 100 Megabyte groß. Weder Webpack noch Babel noch der Browser funktionieren auf einem solchen Volume. Auf jeden Fall werden wir den Benutzer nicht zwingen, 100 Megabyte zu laden ?!

Wenn Sie darüber nachdenken, wird diese Option nicht benötigt. Adhesive Code lädt Binärdateien selbst herunter. Er macht das über HTTP, damit wir sofort zwischenspeichern können. Wir können alle gewünschten Header festlegen, z. B. die Komprimierung aktivieren, und WebAssembly-Dateien werden perfekt komprimiert.

Die coolste Technologie ist jedoch das Streaming von Kompilierungen. Das heißt, die WebAssembly-Datei kann beim Herunterladen vom Server bereits im Browser kompiliert werden, wenn Daten eintreffen. Dies beschleunigt das Laden Ihrer Anwendung erheblich. Im Allgemeinen konzentriert sich die gesamte WebAssembly-Technologie auf den schnellen Start einer großen Codebasis.

Dann möglich


Ein weiteres Problem mit dem Modul besteht darin, dass es sich um ein Thenable-Objekt handelt, dh über eine .then () -Methode. Diese Funktion ermöglicht es Ihnen, einen Rückruf zum Zeitpunkt des Starts des Moduls aufzuhängen, und ist sehr praktisch. Aber ich möchte, dass die Benutzeroberfläche zu Promise passt. Thenable ist kein Versprechen, aber es ist okay, lassen Sie es uns selbst einpacken. Schreiben wir einen so einfachen Code:

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

Wir erstellen Promise, starten unser Modul und rufen als Rückruf die Auflösungsfunktion auf und übergeben das dort installierte Modul. Alles scheint offensichtlich zu sein, alles ist in Ordnung, wir starten - etwas stimmt nicht, unser Browser ist eingefroren, unsere DevTools hängen und der Prozessor heizt sich auf dem Computer auf. Wir verstehen nichts - eine Art Rekursion oder eine Endlosschleife. Das Debuggen ist ziemlich schwierig, und als wir JavaScript unterbrochen haben, sind wir in der Then-Funktion im Emscripten-Modul gelandet.

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

Schauen wir uns das genauer an. Handlung

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

verantwortlich für das Aufhängen eines Rückrufs. Hier ist alles klar: eine asynchrone Funktion, die unseren Rückruf aufruft. Alles wie wir wollen. Es gibt noch einen weiteren Teil dieser Funktion.

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

Es wird aufgerufen, wenn das Modul bereits gestartet wurde. Dann wird der Rückruf sofort synchron aufgerufen und das Modul im Parameter an ihn übergeben. Dies ahmt das Verhalten von Promise nach und scheint das zu sein, was wir erwarten. Aber was ist dann falsch?

Wenn Sie die Dokumentation sorgfältig lesen, stellt sich heraus, dass Promise einen sehr subtilen Punkt hat. Wenn wir das Versprechen mit einem Thenable auflösen, packt der Browser die Werte aus diesem Thenable aus und ruft dazu die .then () -Methode auf. Infolgedessen lösen wir das Versprechen und übergeben das Modul an es. Der Browser fragt: Ist das dann ein Objekt? Ja, das ist ein Thenable. Dann wird die Funktion .then () für das Modul aufgerufen und die Auflösungsfunktion selbst als Rückruf übergeben.

Das Modul prüft, ob es läuft. Es wird bereits ausgeführt, sodass der Rückruf sofort aufgerufen wird und dasselbe Modul erneut an ihn übergeben wird. Als Rückruf haben wir die Auflösungsfunktion und der Browser fragt: Ist dies ein Thenable-Objekt? Ja, das ist ein Thenable. Und alles beginnt von vorne. Infolgedessen geraten wir in einen endlosen Zyklus, aus dem der Browser niemals zurückkehrt.



Ich habe keine elegante Lösung für dieses Problem gefunden. Infolgedessen lösche ich einfach die .then () -Methode vor dem Auflösen, und dies funktioniert.

Emscripten


Also haben wir das Modul kompiliert, JS zusammengestellt, aber etwas fehlt. Wir müssen wahrscheinlich nützliche Arbeit leisten. Übertragen Sie dazu Daten und verbinden Sie die beiden Welten - JS und C ++. Wie kann man das machen? Emscripten bietet drei Optionen:

  • Die erste ist die Funktionen ccall und cwrap. Meistens werden Sie sie in einigen Tutorials zu WebAssembly kennenlernen, aber sie sind nicht für echte Arbeit geeignet, da sie die Funktionen von C ++ nicht unterstützen.
  • Der zweite ist WebIDL Binder. Es unterstützt bereits C ++ - Funktionen, Sie können bereits damit arbeiten. Dies ist eine seriöse Schnittstellenbeschreibungssprache, die beispielsweise von W3C für ihre Dokumentation verwendet wird. Wir wollten es aber nicht in unser Projekt aufnehmen und nutzten die dritte Option
  • Einbinden. Wir können sagen, dass dies eine native Methode zum Verbinden von Objekten für Emscripten ist. Sie basiert auf C ++ - Vorlagen und ermöglicht es Ihnen, viele Dinge zu tun, indem Sie verschiedene Entitäten von C ++ an JS und umgekehrt weiterleiten.


Mit Embind können Sie:

  • Rufen Sie C ++ - Funktionen aus JavaScript-Code auf
  • Erstellen Sie JS-Objekte aus einer C ++ - Klasse
  • Wenden Sie sich im C ++ - Code an die Browser-API (wenn Sie dies aus irgendeinem Grund möchten, können Sie beispielsweise das gesamte Front-End-Framework in C ++ schreiben).
  • Die Hauptsache für uns: Implementieren Sie die in C ++ beschriebene JavaScript-Schnittstelle.


Datenaustausch


Der letzte Punkt ist wichtig, da dies genau die Aktion ist, die Sie beim Portieren der Anwendung ständig ausführen. Deshalb möchte ich näher darauf eingehen. Jetzt wird es C ++ - Code geben, aber keine Angst, es ist fast wie bei TypeScript :-D

Das Schema ist wie folgt:



Auf der C ++ - Seite gibt es einen Kernel, auf den wir beispielsweise Zugriff auf ein externes Netzwerk gewähren möchten - um Videos hochzuladen. Früher wurde dies mit nativen Sockets durchgeführt. Es gab eine Art HTTP-Client, der dies tat, aber in WebAssembly gibt es keine nativen Sockets. Wir müssen irgendwie raus, also schneiden wir den alten HTTP-Client ab, fügen die Schnittstelle an dieser Stelle ein und implementieren diese Schnittstelle in JavaScript unter Verwendung von regulärem AJAX auf irgendeine Weise. Danach geben wir das resultierende Objekt an C ++ zurück, wo der Kernel es verwendet.

Lassen Sie uns den einfachsten HTTP-Client erstellen, der nur Abrufanforderungen stellen kann:

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

Zur Eingabe erhält es eine Zeichenfolge mit der herunterzuladenden URL und zur Ausgabe
eine Zeichenfolge mit dem Ergebnis der Anforderung. In C ++ können Zeichenfolgen Binärdaten enthalten, daher ist dies für Videos geeignet. Emscripten lässt uns hier schreiben
so ein gruseliger Wrapper:



Die Hauptsache sind zwei Dinge - der Name der Funktion auf der C ++ - Seite (ich habe sie grün markiert) und die entsprechenden Namen auf der JavaScript-Seite (ich habe sie blau markiert). Als Ergebnis schreiben wir eine Kommunikationserklärung:



Es funktioniert wie Legoblöcke, aus denen wir es zusammensetzen. Wir haben eine Klasse, diese Klasse hat eine Methode und wir möchten von dieser Klasse erben, um die Schnittstelle zu implementieren. Das ist alles. Wir gehen zu JavaScript und erben. Dies kann auf zwei Arten erfolgen. Der erste ist verlängern. Dies ist sehr ähnlich zu der guten alten Erweiterung von Backbone.



Das Modul enthält alles, was Emscripten kompiliert hat, und verfügt über eine Eigenschaft mit einer exportierten Schnittstelle. Wir rufen die Extend-Methode auf und übergeben dort ein Objekt mit der Implementierung dieser Methode, dh einige Methoden werden in der get-Funktion implementiert
Informationen mit AJAX abrufen.

Bei der Ausgabe erhalten Sie mit "verlängern" einen regulären JavaScript-Konstruktor. Wir können es so oft wie nötig aufrufen und Objekte in der Menge generieren, die wir benötigen. Es gibt jedoch eine Situation, in der wir ein Objekt haben und es nur an die C ++ - Seite übergeben möchten.



Binden Sie dazu dieses Objekt irgendwie an einen Typ, den C ++ versteht. Dies ist, was die Implementierungsfunktion tut. Bei der Ausgabe wird kein Konstruktor angegeben, sondern ein gebrauchsfertiges Objekt, unser Client, das wir an C ++ zurückgeben können. Sie können dies beispielsweise folgendermaßen tun:

 var app = Module.makeApp(client, …) 

Angenommen, wir haben eine Factory, die unsere Anwendung erstellt und deren Abhängigkeiten in Parameter wie Client und etwas anderes umwandelt. Wenn diese Funktion funktioniert, erhalten wir das Objekt unserer Anwendung, das bereits die API enthält, die wir benötigen. Sie können das Gegenteil tun:

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

Nehmen Sie unseren Client direkt aus C ++ aus dem globalen Browserbereich. Anstelle des Clients kann es außerdem eine beliebige Browser-API geben, die von der Konsole aus beginnt und mit der DOM-API WebRTC endet - was auch immer Sie möchten. Als nächstes rufen wir die Methoden dieses Objekts auf und verpacken alle Werte in die magische Klasse val, die Emscripten uns zur Verfügung stellt.

Bindungsfehler


Im Allgemeinen ist das alles, aber wenn Sie mit der Entwicklung beginnen, erwarten Sie Bindungsfehler. Sie sehen ungefähr so ​​aus:



Emscripten versucht uns zu helfen und zu erklären, was falsch läuft. Wenn dies alles zusammengefasst ist, müssen Sie sicherstellen, dass sie übereinstimmen (es ist leicht zu versiegeln und einen Bindungsfehler zu erhalten):

  • Namen
  • Typen
  • Anzahl der Parameter

Die Embind-Syntax ist nicht nur für Front-End-Anbieter ungewöhnlich, sondern auch für Benutzer von C ++. Dies ist eine Art DSL, bei der es leicht ist, einen Fehler zu machen. Sie müssen dies befolgen. Apropos Schnittstellen: Wenn Sie eine Schnittstelle in JavaScript implementieren, muss diese genau mit der in Ihrem Vertrag beschriebenen übereinstimmen.

Wir hatten einen interessanten Fall. Mein Kollege Jura, der auf der C ++ - Seite an dem Projekt beteiligt war, hat Extend verwendet, um seine Module zu testen. Sie haben perfekt für ihn gearbeitet, also hat er sie begangen und an mich weitergegeben. Ich habe implement verwendet, um diese Module in ein JS-Projekt zu integrieren. Und sie haben aufgehört für mich zu arbeiten. Als wir es herausfanden, stellte sich heraus, dass wir beim Binden der Namen der Funktionen einen Tippfehler bekamen.

Wie der Name schon sagt, ist Extend eine Erweiterung der Schnittstelle. Wenn Sie sie also irgendwo versiegelt haben, gibt Extend keinen Fehler aus, sondern entscheidet, dass Sie gerade eine neue Methode hinzugefügt haben, und das ist in Ordnung.

Das heißt, die Bindungsfehler werden ausgeblendet, bis die Methode selbst aufgerufen wird. Ich schlage vor, Implement in allen Fällen zu verwenden, in denen es Ihnen passt, da es sofort die Richtigkeit der weitergeleiteten Schnittstelle überprüft. Wenn Sie jedoch Extend benötigen, müssen Sie den Aufruf jeder Methode mit Tests abdecken, um ihn nicht zu verfälschen.

Erweitern und ES6


Ein weiteres Problem mit Extend ist, dass ES6-Klassen nicht unterstützt werden. Wenn Sie ein von einer ES6-Klasse abgeleitetes Objekt erben, erwartet Extend, dass alle Eigenschaften darin aufzählbar sind, bei ES6 jedoch nicht. Die Methoden befinden sich im Prototyp und haben folgende Aufzählungen: false. Ich benutze eine Krücke wie diese, in der ich über den Prototyp gehe und aufzählbar einschalte: wahr:

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

Ich hoffe, dass ich es eines Tages loswerden kann, da in der Emscripten-Community über die Verbesserung der Unterstützung für ES6 gesprochen wird.

Rom


Wenn man über C ++ spricht, kann man nicht anders, als den Speicher zu erwähnen. Als wir alles auf Video in SD-Qualität überprüft haben, war bei uns alles in Ordnung, es hat einfach perfekt funktioniert! Sobald wir den FullHD-Test durchgeführt haben, fehlte ein Speicherfehler. Es spielt keine Rolle, es gibt die Option TOTAL_MEMORY, mit der der Startspeicherwert für das Modul festgelegt wird. Wir haben ein halbes Gigabyte gemacht, alles ist in Ordnung, aber irgendwie ist es für Benutzer unmenschlich, weil wir den Speicher für alle reservieren, aber nicht jeder hat ein Abonnement für FullHD-Inhalte.

Es gibt noch eine andere Option - ALLOW_MEMORY_GROWTH. Es ermöglicht Ihnen, das Gedächtnis zu vergrößern
nach Bedarf schrittweise. Das funktioniert so: Emscripten gibt dem Modul standardmäßig 16 Megabyte für den Betrieb. Wenn Sie sie alle verwendet haben, wird ein neuer Speicher zugewiesen. Alle alten Daten werden dort kopiert, und Sie haben immer noch den gleichen Speicherplatz für neue. Dies geschieht, bis Sie 4 GB erreichen.

Angenommen, Sie haben 256 Megabyte Speicher zugewiesen, aber Sie wissen mit Sicherheit, dass Ihre Anwendung über genügend Speicher verfügt. Dann wird der Rest des Speichers ineffizient verwendet. Sie haben es hervorgehoben, dem Benutzer abgenommen, aber nichts damit gemacht. Ich möchte das irgendwie vermeiden. Es gibt einen kleinen Trick: Wir beginnen mit der Arbeit, wobei das Gedächtnis um das Eineinhalbfache erhöht wird. Dann erreichen wir im dritten Schritt 192 Megabyte, und genau das brauchen wir. Wir haben den Speicherverbrauch um diesen Rest reduziert und unnötige Speicherzuweisung gespart. Je weiter, desto länger dauert dies. Daher empfehle ich, beide Optionen zusammen zu verwenden.

Abhängigkeitsinjektion


Es scheint, dass das alles war, aber dann ging der Rechen etwas mehr. Es liegt ein Problem mit der Abhängigkeitsinjektion vor. Wir schreiben die einfachste Klasse, in der eine Abhängigkeit benötigt wird.

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

Zum Beispiel übergeben wir unseren HTTP-Client an unsere Anwendung. Wir speichern in der Klasseneigenschaft. Es scheint, dass alles gut funktionieren wird.

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

Wir erben von der C ++ - Schnittstelle, erstellen zuerst unser Objekt, übergeben die Abhängigkeit daran und erben dann. Zum Zeitpunkt der Vererbung macht Emscripten etwas Unglaubliches mit dem Objekt. Es ist am einfachsten zu glauben, dass es ein altes Objekt tötet, ein neues basierend auf seiner Vorlage erstellt und alle öffentlichen Methoden dorthin zieht. Gleichzeitig geht der Status des Objekts verloren und Sie erhalten ein Objekt, das nicht gebildet wird und nicht richtig funktioniert. Die Lösung dieses Problems ist recht einfach. Es ist erforderlich, einen Konstruktor zu verwenden, der nach der Vererbungsphase funktioniert.

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

Wir machen fast dasselbe: Wir speichern die Abhängigkeit im Feld des Objekts, aber dies ist das Objekt, das sich nach der Vererbung herausgestellt hat. Wir dürfen nicht vergessen, den Konstruktoraufruf an das übergeordnete Objekt weiterzuleiten, das sich auf der C ++ - Seite befindet. Die letzte Zeile ist ein Analogon zur super () -Methode in ES6. So geschieht die Vererbung in diesem Fall:

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

Zuerst erben wir, dann erstellen wir ein neues Objekt, an das die Abhängigkeit bereits übergeben wird, und das funktioniert.

Zeigertrick


Ein weiteres Problem ist die Übergabe von Objekten per Zeiger von C ++ an JavaScript. Wir haben bereits einen HTTP-Client erstellt. Der Einfachheit halber haben wir ein wichtiges Detail übersehen.

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

Die Methode gibt den Wert sofort zurück, dh es stellt sich heraus, dass die Anforderung synchron sein sollte. Schließlich fordert AJAX AJAX an und dass sie asynchron sind, sodass die Methode im wirklichen Leben entweder nichts zurückgibt oder wir die Anforderungs-ID zurückgeben können. Damit jedoch jemand die Antwort zurückgibt, übergeben wir den Listener als zweiten Parameter, in dem Rückrufe von C ++ erfolgen.

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

In JS sieht es so aus:

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

Wir haben eine get-Funktion, die dieses Listener-Objekt übernimmt. Wir starten den Dateidownload und legen den Rückruf auf. Wenn die Datei heruntergeladen wird, ziehen wir die gewünschte Funktion aus dem Listener und übergeben das Ergebnis an ihn.

Es scheint, dass der Plan gut ist, aber wenn die get-Funktion abgeschlossen ist, werden alle lokalen Variablen zerstört, und zusammen mit ihnen werden die Funktionsparameter, dh der Zeiger, zerstört, und emscripten zur Laufzeit zerstört das Objekt auf der C ++ - Seite.

Wenn der Zeilenlistener.onResult (Ergebnis) aufgerufen wird, ist der Listener daher nicht mehr vorhanden. Beim Zugriff darauf tritt ein Speicherzugriffsfehler auf, der zum Absturz der Anwendung führt.

Ich möchte dies vermeiden, und es gibt eine Lösung, aber es hat mehrere Wochen gedauert, bis ich sie gefunden habe.

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

Es stellt sich heraus, dass es eine Methode zum Klonen eines Zeigers gibt. Aus irgendeinem Grund ist es nicht dokumentiert, funktioniert aber einwandfrei und ermöglicht es Ihnen, die Referenzanzahl im Emscripten-Zeiger zu erhöhen. Dies ermöglicht es uns, es in einem Abschluss auszusetzen. Wenn wir dann unseren Rückruf starten, ist unser Listener über diesen Zeiger erreichbar und wir können nach Bedarf arbeiten.

Das Wichtigste ist, nicht zu vergessen, diesen Zeiger zu löschen, da dies sonst zu einem Speicherverlustfehler führt, der sehr schlimm ist.

Schnelles Schreiben in den Speicher


Wenn wir Videos herunterladen, sind dies relativ große Informationsmengen, und ich möchte die Menge des Hin- und Herkopierens von Daten reduzieren, um sowohl Speicher als auch Zeit zu sparen. Es gibt einen Trick, wie eine große Menge von Informationen aus JavaScript direkt in den WebAssembly-Speicher geschrieben werden kann.

 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 sind unsere Daten als typisiertes Array. Wir können seine Länge nehmen und die Zuweisung von Speicher der Größe, die wir benötigen, vom WebAssembly-Modul anfordern. Die Malloc-Funktion gibt einen Zeiger an uns zurück. Dies ist nur der Index des Arrays, das den gesamten Speicher in WebAssembly enthält. Von der JavaScript-Seite sieht es einfach wie ein ArrayBuffer aus.

Im nächsten Schritt schneiden wir von einer bestimmten Stelle aus ein Fenster in diesen ArrayBuffer der richtigen Größe und kopieren dort unsere Daten. Trotz der Tatsache, dass die Set-Operation eine Kopiersemantik aufweist, sah ich beim Betrachten dieses Abschnitts im Profiler keinen langen Prozess. Ich denke, dass der Browser diesen Vorgang mit Hilfe der Verschiebungssemantik optimiert, dh den Besitz des Speichers von einem Objekt auf ein anderes überträgt.

In unserer Anwendung verlassen wir uns auch auf die Verschiebungssemantik, um Speicherkopien zu sparen.

Adblock


Ein interessantes Problem bei der Änderung mit Adblock. Es stellt sich heraus, dass in Russland alle beliebten Blocker ein Abonnement für die RU-Adlist erhalten, und es gibt eine wunderbare Regel, die das Herunterladen von WebAssembly von Websites von Drittanbietern verbietet. Zum Beispiel mit einem CDN.



Der Ausweg besteht nicht darin, das CDN zu verwenden, sondern alles auf Ihrer Domain zu speichern (dies passt nicht zu uns). Oder benennen Sie die WASM-Datei um, damit sie nicht dieser Regel entspricht. Sie können immer noch zum Forum dieser Genossen gehen und versuchen, sie davon zu überzeugen, diese Regel zu entfernen. Ich denke, sie rechtfertigen sich, indem sie auf diese Weise gegen die Bergleute kämpfen, obwohl ich nicht weiß, warum die Bergleute nicht raten können, die Datei umzubenennen.

Produktion


Infolgedessen gingen wir in Produktion. Ja, es war nicht einfach, es hat 8 Monate gedauert und ich möchte mich fragen, ob es sich gelohnt hat. Meiner Meinung nach hat es sich gelohnt:

Keine Notwendigkeit zu installieren


Wir haben festgestellt, dass unser Code an den Benutzer geliefert wird, ohne dass Programme installiert werden müssen. Wenn wir ein Browser-Plug-In hatten, musste der Benutzer es herunterladen und installieren, und dies ist ein riesiger Filter für die Technologieverteilung. Jetzt sieht sich der Benutzer nur noch das Video auf der Website an und versteht nicht einmal, dass eine ganze Maschine unter der Haube arbeitet und dass dort alles kompliziert ist. Der Browser lädt nur eine zusätzliche Datei mit dem Code herunter, z. B. ein Bild oder eine CSS-Datei.

Einheitliche Codebasis und Debugging auf verschiedenen Plattformen


Gleichzeitig konnten wir unsere einzelne Codebasis beibehalten. Wir können denselben Code auf verschiedenen Plattformen verdrehen, und es ist wiederholt vorgekommen, dass Fehler, die auf einer der Plattformen unsichtbar waren, auf der anderen auftraten. Auf diese Weise können wir versteckte Fehler mit verschiedenen Tools auf verschiedenen Plattformen erkennen.

Schnellspanner


Wir haben eine schnelle Version erhalten, da wir als einfache Webanwendung veröffentlicht werden können und den C ++ - Code mit jeder neuen Version aktualisieren können. Es ist nicht vergleichbar mit der Veröffentlichung neuer Plugins, einer mobilen Anwendung oder einer SmartTV-Anwendung. Die Veröffentlichung hängt nur von uns ab: Wenn wir wollen, wird sie veröffentlicht.

Schnelles Feedback


Und das bedeutet schnelles Feedback: Wenn etwas schief geht, können wir tagsüber feststellen, dass ein Problem vorliegt, und darauf reagieren.

Ich glaube, dass all diese Probleme diese Vorteile wert waren. Nicht jeder hat eine C ++ - Anwendung, aber wenn Sie eine haben und diese im Browser haben möchten, ist WebAssembly ein 100% iger Anwendungsfall für Sie.

Wo bewerben?


Nicht jeder schreibt in C ++. Für WebAssembly ist jedoch nicht nur C ++ verfügbar. Ja, dies ist historisch gesehen die allererste Plattform, die noch in asm.js, einer frühen Mozilla-Technologie, verfügbar war. Übrigens hat es also ziemlich gute Werkzeuge, wie Sie sind älter als die Technologie selbst.

Rost


Die neue Rust-Sprache, die ebenfalls von Mozilla entwickelt wird, holt C ++ jetzt in Bezug auf Tools ein und überholt es. Alles geht so weit, dass sie den coolsten Entwicklungsprozess für WebAssembly machen.

Lua, Perl, Python, PHP usw.


Fast alle interpretierten Sprachen sind auch in WebAssembly verfügbar, da ihre Interpreter in C ++ geschrieben sind. Sie wurden einfach in WebAssembly kompiliert und jetzt können Sie PHP in einem Browser drehen.

Geh


In Version 1.11 haben sie eine Beta-Version der Kompilierung in WebAssembly erstellt, in 2.0 versprechen sie Release-Unterstützung. Ihre Unterstützung wurde später angezeigt, da WebAssembly den Garbage Collector nicht unterstützt und Go eine Sprache für verwalteten Speicher ist. Also mussten sie ihren Garbage Collector unter WebAssembly ziehen.

Kotlin / Native


Über die gleiche Geschichte mit Kotlin. Ihr Compiler hat experimentelle Unterstützung, aber sie müssen auch etwas mit dem Garbage Collector tun. Ich weiß nicht, welchen Status es gibt.

3D-Grafiken


Was können Sie noch denken? , — 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.


Vielen Dank für Ihre Aufmerksamkeit! .



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

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


All Articles