WebPengembangan perakitan: menyapu dan contoh nyata



Pengumuman WebAssembly terjadi pada tahun 2015 - tetapi sekarang, setelah bertahun-tahun, masih ada beberapa yang bisa membanggakannya dalam produksi. Materi tentang pengalaman seperti itu semakin berharga: informasi dari tangan pertama tentang bagaimana hidup dengannya dalam praktik masih kurang.

Pada konferensi HolyJS, sebuah laporan tentang pengalaman menggunakan WebAssembly menerima nilai tinggi dari penonton, dan sekarang versi teks dari laporan ini telah disiapkan khusus untuk Habr (sebuah video juga dilampirkan).



Nama saya Andrey, saya akan memberi tahu Anda tentang WebAssembly. Kita dapat mengatakan bahwa saya mulai terlibat dalam web di abad yang lalu, tetapi saya rendah hati, jadi saya tidak akan mengatakan itu. Selama waktu ini, saya berhasil mengerjakan backend dan frontend, dan bahkan menggambar sedikit desain. Hari ini saya tertarik pada hal-hal seperti WebAssembly, C ++ dan hal-hal asli lainnya. Saya juga sangat suka tipografi dan mengumpulkan teknologi lama.

Pertama, saya akan berbicara tentang bagaimana tim dan saya mengimplementasikan WebAssembly di proyek kami, kemudian kami akan membahas apakah Anda memerlukan sesuatu dari WebAssembly, dan diakhiri dengan beberapa tips jika Anda ingin mengimplementasikannya sendiri.

Bagaimana kami menerapkan WebAssembly


Saya bekerja untuk Inetra, kami berlokasi di Novosibirsk dan melakukan beberapa proyek kami sendiri. Salah satunya adalah ByteFog. Ini adalah teknologi peer-to-peer untuk mengirimkan video ke pengguna. Pelanggan kami adalah layanan yang mendistribusikan sejumlah besar video. Mereka memiliki masalah: ketika beberapa acara populer terjadi, misalnya, konferensi pers seseorang atau acara olahraga, bagaimana tidak bersiap-siap untuk itu, banyak klien datang, bersandar pada server, dan server sedih. Pelanggan menerima kualitas video yang sangat buruk saat ini.

Tetapi semua orang menonton konten yang sama. Mari kita minta perangkat tetangga dari pengguna untuk membagikan potongan video, dan kemudian kita akan membongkar server, menghemat bandwidth, dan pengguna akan menerima video dalam kualitas yang lebih baik. Awan ini adalah teknologi kami, server proxy ByteFog kami.



Kami harus dipasang di setiap perangkat yang dapat menampilkan video, oleh karena itu kami mendukung berbagai platform: Windows, Linux, Android, iOS, Web, Tizen. Bahasa apa yang harus dipilih untuk memiliki basis kode tunggal pada semua platform ini? Kami memilih C ++ karena ternyata memiliki keuntungan terbesar: -D Lebih serius, kami memiliki keahlian yang baik dalam C ++, ini benar-benar bahasa yang cepat, dan dalam portabilitas mungkin hanya yang kedua setelah C.

Kami mendapat aplikasi yang cukup besar (900 kelas), tetapi berfungsi dengan baik. Di bawah Windows dan Linux, kami mengkompilasi ke dalam kode asli. Untuk Android dan iOS, kami membangun perpustakaan yang kami sambungkan ke aplikasi. Kami akan berbicara tentang Tizen di lain waktu, tetapi di Web kami dulu bekerja sebagai plugin browser.

Ini adalah teknologi API Netscape Plugin. Seperti namanya, ini sudah sangat tua, dan juga memiliki kelemahan: ia memberikan akses yang sangat luas ke sistem, sehingga kode pengguna dapat menyebabkan masalah keamanan. Ini mungkin mengapa Chrome mematikan dukungan untuk teknologi ini pada tahun 2015, dan kemudian semua browser bergabung dengan flash mob ini. Jadi kami dibiarkan tanpa versi web selama hampir dua tahun.

Pada 2017, harapan baru datang. Seperti yang Anda bayangkan, ini adalah WebAssembly. Sebagai hasilnya, kami mengatur sendiri tugas porting aplikasi kami ke browser. Karena dukungan untuk Firefox dan Chrome sudah muncul di musim semi, dan pada musim gugur 2017, Edge dan Safari menarik diri.

Penting bagi kami untuk menggunakan kode yang sudah jadi, karena kami memiliki banyak logika bisnis yang tidak ingin kami gandakan, sehingga tidak menggandakan jumlah bug. Ambil Emscripten kompiler. Dia melakukan apa yang kita butuhkan - mengkompilasi aplikasi positif ke dalam browser dan menciptakan kembali lingkungan yang akrab dengan aplikasi asli di browser. Kita dapat mengatakan bahwa Emscripten adalah kode Browserify untuk C ++. Ini juga memungkinkan Anda untuk meneruskan objek dari C ++ ke JavaScript dan sebaliknya. Pikiran pertama kami adalah: sekarang mari kita ambil Emscripten, kompilasi saja, dan semuanya akan bekerja. Tentu saja tidak. Dari sinilah kami memulai perjalanan menyapu.

Hal pertama yang kami temui adalah kecanduan. Ada beberapa perpustakaan di basis kode kami. Sekarang tidak masuk akal untuk mendaftar mereka, tetapi bagi mereka yang mengerti, kami memiliki Peningkatan. Ini adalah perpustakaan besar yang memungkinkan Anda menulis kode lintas platform, tetapi sangat sulit untuk mengonfigurasi kompilasi dengannya. Saya ingin menyeret kode sesedikit mungkin ke browser.

Arsitektur Bytefog


Sebagai hasilnya, kami mengidentifikasi intinya: kami dapat mengatakan bahwa ini adalah server proxy yang berisi logika bisnis utama. Server proxy ini mengambil data dari dua sumber. Yang pertama dan utama adalah HTTP, yaitu saluran ke server distribusi video, yang kedua adalah jaringan P2P kami, yaitu saluran ke proxy lain yang sama dari beberapa pengguna lain. Kami memberikan data terutama kepada pemain, karena tugas kami adalah menampilkan konten berkualitas tinggi kepada pengguna. Jika sumber daya tetap ada, kami mendistribusikan konten ke jaringan P2P sehingga pengguna lain dapat mengunduhnya. Di dalamnya ada cache pintar yang melakukan semua keajaiban.



Setelah mengkompilasi semua ini, kita dihadapkan dengan fakta bahwa WebAssembly dieksekusi di kotak pasir browser. Itu berarti tidak dapat melakukan lebih dari yang diberikan JavaScript. Sementara aplikasi asli menggunakan banyak hal khusus platform, seperti sistem file, jaringan, atau angka acak. Semua fitur ini harus diimplementasikan dalam JavaScript menggunakan apa yang diberikan browser kepada kami. Pelat ini berisi daftar penggantian yang cukup jelas.



Untuk memungkinkan ini, perlu untuk melihat implementasi kemampuan asli dalam aplikasi asli dan memasukkan antarmuka di sana, yaitu, menggambar perbatasan tertentu. Kemudian Anda menerapkan ini dalam JavaScript dan meninggalkan implementasi asli, dan sudah selama perakitan yang diperlukan dipilih. Jadi, kami melihat arsitektur kami dan menemukan semua tempat di mana perbatasan ini dapat ditarik. Secara kebetulan, ini adalah subsistem transportasi.



Untuk setiap tempat seperti itu kami mendefinisikan spesifikasi, yaitu, kami menetapkan kontrak: metode apa yang akan, parameter apa yang akan mereka miliki, tipe data apa. Setelah Anda melakukan ini, Anda dapat bekerja secara paralel, masing-masing pengembang di sisinya.

Apa hasilnya? Kami mengganti saluran pengiriman video utama dari penyedia dengan AJAX yang biasa. Kami mengeluarkan data ke pemain melalui pustaka HLS.js yang populer, tetapi ada kemungkinan mendasar untuk berintegrasi dengan pemain lain, jika perlu. Kami mengganti seluruh lapisan P2P dengan WebRTC.



Sebagai hasil kompilasi, beberapa file diperoleh. Yang paling penting adalah .wasm biner. Ini berisi bytecode yang dikompilasi yang akan dieksekusi oleh browser dan yang berisi semua warisan C ++ Anda. Tetapi dengan sendirinya itu tidak bekerja, yang disebut "kode lem" diperlukan, itu juga dihasilkan oleh kompiler. Kode lem sedang mengunduh file biner, dan Anda mengunggah kedua file ini ke produksi. Untuk keperluan debugging, Anda dapat menghasilkan representasi tekstual assembler - file .wast dan sumber data. Anda perlu memahami bahwa mereka bisa sangat besar. Dalam kasus kami, mereka mencapai 100 megabita atau lebih.

Mengumpulkan bungkusan itu


Mari kita lihat lebih dekat kode lem. Ini adalah ES5 tua yang baik dan biasa, yang dikumpulkan menjadi satu file. Ketika kami menghubungkannya ke halaman web, kami memiliki variabel global yang berisi semua modul-wasme instantiated kami, yang siap menerima permintaan ke API-nya.

Tetapi memasukkan file terpisah adalah komplikasi yang agak serius untuk perpustakaan yang akan digunakan pengguna. Kami ingin meletakkan semuanya dalam satu bundel. Untuk ini kami menggunakan Webpack dan opsi kompilasi khusus MODULARIZE.

Itu membungkus kode perekat dalam pola "Modul", dan kita dapat mengambilnya: impor atau gunakan harus jika kita menulis di ES5 - Webpack dengan tenang memahami ketergantungan ini. Ada masalah dengan Babel - dia tidak suka jumlah kode yang besar, tetapi ini adalah kode ES5, tidak perlu ditransformasikan, kami hanya menambahkannya untuk diabaikan.

Dalam mengejar jumlah file, saya memutuskan untuk menggunakan opsi SINGLE_FILE. Ini menerjemahkan semua binari yang dihasilkan dari kompilasi ke dalam bentuk Base64 dan mendorongnya ke dalam kode perekat sebagai string. Kedengarannya seperti ide yang bagus, tetapi setelah itu bundel menjadi 100 megabyte. Baik Webpack, Babel, atau bahkan browser tidak bekerja pada volume seperti itu. Bagaimanapun, kami tidak akan memaksa pengguna untuk memuat 100 megabyte ?!

Jika Anda memikirkannya, opsi ini tidak diperlukan. Kode perekat mengunduh file biner sendiri. Dia melakukan ini melalui HTTP, jadi kita mendapatkan caching di luar kotak, kita dapat mengatur header yang kita inginkan, misalnya, mengaktifkan kompresi, dan file WebAssembly dikompresi dengan sempurna.

Tetapi teknologi paling keren adalah kompilasi streaming. Yaitu, file WebAssembly, saat mengunduh dari server, sudah dapat dikompilasi di browser saat data tiba, dan ini sangat mempercepat pemuatan aplikasi Anda. Secara umum, semua teknologi WebAssembly memiliki fokus pada awal yang cepat dari basis kode yang besar.

Kemudian


Masalah lain dengan modul adalah bahwa itu adalah objek Thenable, yaitu, ia memiliki metode .then (). Fungsi ini memungkinkan Anda untuk menggantung panggilan balik pada saat modul dimulai, dan itu sangat nyaman. Tapi saya ingin antarmuka yang sesuai dengan Janji. Tenable bukan Janji, tapi tidak apa-apa, mari kita selesaikan sendiri. Mari kita menulis kode sederhana seperti ini:

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

Kami membuat Janji, mulai modul kami, dan sebagai panggilan balik kami memanggil fungsi tekad dan lulus modul yang kami pasang di sana. Semuanya tampak jelas, semuanya baik-baik saja, kami meluncurkan - ada sesuatu yang salah, browser kami beku, DevTools kami menggantung, dan prosesor memanas di komputer. Kami tidak memahami apa pun - semacam rekursi atau loop tak terbatas. Debugging cukup sulit, dan ketika kami menghentikan JavaScript, kami berakhir di fungsi Then di modul Emscripten.

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

Mari kita lihat lebih detail. Plot

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

bertanggung jawab untuk menggantung panggilan balik. Semuanya jelas di sini: fungsi asinkron yang memanggil panggilan balik kami. Segalanya seperti yang kita inginkan. Ada bagian lain dari fitur ini.

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

Disebut ketika modul sudah dimulai. Kemudian panggilan balik segera dipanggil, dan modul diteruskan ke parameter. Ini meniru perilaku Janji, dan tampaknya itulah yang kami harapkan. Tapi lalu apa yang salah?

Jika Anda membaca dokumentasi dengan cermat, ternyata ada poin yang sangat halus tentang Janji. Ketika kami menyelesaikan Janji menggunakan Thenable, browser akan membuka nilai dari Thenable ini, dan untuk melakukan ini, ia akan memanggil metode .then (). Akibatnya, kami menyelesaikan Janji, meneruskan modul ke sana. Browser bertanya: Lalu apakah ini objek? Ya, ini adalah Thenable. Kemudian fungsi .then () dipanggil pada modul, dan fungsi resolusinya sendiri diteruskan sebagai panggilan balik.

Modul ini memeriksa apakah sedang berjalan. Ini sudah berjalan, jadi panggilan balik dipanggil segera, dan modul yang sama diteruskan lagi. Sebagai panggilan balik, kami memiliki fungsi tekad, dan peramban bertanya: apakah ini objek yang dapat ditelusuri? Ya, ini adalah Thenable. Dan semuanya dimulai lagi. Akibatnya, kami masuk ke dalam siklus tanpa akhir dari mana browser tidak pernah kembali.



Saya tidak menemukan solusi elegan untuk masalah ini. Akibatnya, saya cukup menghapus metode .then () sebelum menyelesaikan, dan ini berhasil.

Emscripten


Jadi, kami menyusun modul, mengumpulkan JS, tetapi ada sesuatu yang hilang. Kita mungkin perlu melakukan beberapa pekerjaan yang bermanfaat. Untuk melakukan ini, transfer data dan hubungkan dua dunia - JS dan C ++. Bagaimana cara melakukannya? Emscripten menyediakan tiga opsi:

  • Yang pertama adalah fungsi ccall dan cwrap. Paling sering Anda akan menemui mereka di beberapa tutorial di WebAssembly, tetapi mereka tidak cocok untuk pekerjaan nyata, karena mereka tidak mendukung kemampuan C ++.
  • Yang kedua adalah WebIDL Binder. Ini sudah mendukung fungsi C ++, Anda sudah bisa menggunakannya. Ini adalah bahasa deskripsi antarmuka serius yang digunakan, misalnya, oleh W3C untuk dokumentasi mereka. Tapi kami tidak ingin membawanya ke proyek kami dan menggunakan opsi ketiga
  • Embind. Kita dapat mengatakan bahwa ini adalah cara asli untuk menghubungkan objek untuk Emscripten, didasarkan pada template C ++ dan memungkinkan Anda untuk melakukan banyak hal dengan meneruskan entitas yang berbeda dari C ++ ke JS dan sebaliknya.


Embind memungkinkan Anda untuk:

  • Panggil fungsi C ++ dari kode JavaScript
  • Buat objek JS dari kelas C ++
  • Dari kode C ++, buka API browser (jika karena alasan tertentu Anda menginginkan ini, Anda dapat, misalnya, menulis seluruh kerangka front-end dalam C ++).
  • Hal utama bagi kami: mengimplementasikan antarmuka JavaScript yang dijelaskan dalam C ++.


Pertukaran data


Poin terakhir ini penting, karena ini adalah tindakan yang akan terus Anda lakukan saat porting aplikasi. Karena itu, saya ingin membahasnya lebih detail. Sekarang akan ada kode C ++, tetapi jangan takut, hampir seperti TypeScript :-D

Skemanya adalah sebagai berikut:



Di sisi C ++, ada kernel yang ingin kami beri akses, misalnya, ke jaringan eksternal - untuk mengunggah video. Dulu melakukan ini menggunakan soket asli, ada beberapa jenis klien HTTP yang melakukan ini, tetapi tidak ada soket asli di WebAssembly. Kita harus keluar entah bagaimana, jadi kita memotong klien HTTP lama, memasukkan antarmuka ke tempat ini, dan mengimplementasikan antarmuka ini dalam JavaScript menggunakan AJAX biasa, dengan cara apa pun. Setelah itu, kita akan meneruskan objek yang dihasilkan kembali ke C ++, di mana kernel akan menggunakannya.

Mari kita buat klien HTTP paling sederhana yang hanya bisa membuat permintaan:

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

Untuk input, ia menerima string dengan URL untuk diunduh, dan ke output
string dengan hasil permintaan. Di C ++, string dapat memiliki data biner, jadi ini cocok untuk video. Emscripten membuat kami menulis di sini
Wrapper yang menakutkan:



Di dalamnya, hal utama adalah dua hal - nama fungsi di sisi C ++ (saya menandai mereka dalam warna hijau), dan nama-nama yang sesuai di sisi JavaScript (saya menandainya dengan warna biru). Sebagai hasilnya, kami menulis pernyataan komunikasi:



Ini bekerja seperti balok-balok Lego, dari mana kita merakitnya. Kami memiliki kelas, kelas ini memiliki metode, dan kami ingin mewarisi dari kelas ini untuk mengimplementasikan antarmuka. Itu saja. Kami pergi ke JavaScript dan mewarisi. Ini bisa dilakukan dengan dua cara. Yang pertama adalah memperpanjang. Ini sangat mirip dengan perpanjangan lama yang baik dari Backbone.



Modul ini berisi semua yang dikompilasi oleh Emscripten, dan memiliki properti dengan antarmuka yang diekspor. Kami memanggil metode extended dan melewatkan objek di sana dengan implementasi metode ini, yaitu, beberapa metode akan diimplementasikan dalam fungsi get
Dapatkan informasi menggunakan AJAX.

Pada hasil, ekstensi memberi kita konstruktor JavaScript biasa. Kita dapat menyebutnya sebanyak yang diperlukan dan menghasilkan objek dalam jumlah yang kita butuhkan. Tetapi ada situasi ketika kita memiliki satu objek, dan kita hanya ingin meneruskannya ke sisi C ++.



Untuk melakukan ini, ikatlah objek ini dengan jenis yang akan dipahami oleh C ++. Inilah fungsi fungsi implement. Pada output, ia tidak memberikan konstruktor, tetapi objek yang siap digunakan, klien kami, yang dapat kami berikan kembali ke C ++. Anda dapat melakukan ini, misalnya, seperti ini:

 var app = Module.makeApp(client, …) 

Misalkan kita memiliki pabrik yang membuat aplikasi kita, dan itu mengambil dependensi ke dalam parameter, misalnya, klien dan sesuatu yang lain. Ketika fungsi ini berfungsi, kita mendapatkan objek aplikasi kita, yang sudah berisi API yang kita butuhkan. Anda dapat melakukan yang sebaliknya:

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

Langsung dari C ++, ambil klien kami dari lingkup browser global. Selain itu, sebagai pengganti klien, bisa ada API browser apa pun, mulai dari konsol, berakhir dengan DOM API, WebRTC - apa pun yang Anda inginkan. Selanjutnya, kita memanggil metode yang dimiliki objek ini, dan kita membungkus semua nilai dalam val kelas sihir, yang disediakan oleh Emscripten.

Mengikat kesalahan


Secara umum, itu saja, tetapi ketika Anda memulai pengembangan, kesalahan mengikat menunggu Anda. Mereka terlihat seperti ini:



Emscripten mencoba membantu kami dan menjelaskan apa yang salah. Jika ini semua disimpulkan, maka Anda perlu memastikan bahwa mereka bertepatan (mudah untuk menutup dan mendapatkan kesalahan yang mengikat):

  • Nama
  • Jenis
  • Jumlah parameter

Sintaks Embind tidak biasa tidak hanya untuk vendor front-end, tetapi juga untuk orang yang berurusan dengan C ++. Ini adalah jenis DSL di mana mudah untuk membuat kesalahan, Anda harus mengikuti ini. Berbicara tentang antarmuka, ketika Anda mengimplementasikan beberapa jenis antarmuka dalam JavaScript, perlu bahwa itu persis cocok dengan apa yang Anda jelaskan dalam kontrak Anda.

Kami punya kasus yang menarik. Rekan saya Jura, yang terlibat dalam proyek di sisi C ++, menggunakan Extend untuk menguji modul-modulnya. Mereka bekerja dengan sempurna untuknya, jadi dia berkomitmen dan menyerahkannya kepada saya. Saya menggunakan implement untuk mengintegrasikan modul-modul ini ke dalam proyek JS. Dan mereka berhenti bekerja untuk saya. Ketika kami menemukan jawabannya, ternyata saat mengikat nama fungsi, kami mendapat kesalahan ketik.

Seperti yang dapat kita lihat dari namanya, Extend adalah ekstensi dari antarmuka, jadi jika Anda membuat kesalahan di suatu tempat, Extend tidak akan membuat kesalahan, itu akan memutuskan bahwa Anda baru saja menambahkan metode baru, dan itu tidak apa-apa.

Artinya, menyembunyikan kesalahan yang mengikat sampai metode itu sendiri dipanggil. Saya sarankan menggunakan Implement dalam semua kasus yang cocok untuk Anda, karena segera memeriksa kebenaran dari antarmuka yang diteruskan. Tetapi jika Anda perlu Memperpanjang, Anda harus menutup dengan tes panggilan masing-masing metode agar tidak mengacaukannya.

Perpanjang dan ES6


Masalah lain dengan Extend adalah bahwa ia tidak mendukung kelas ES6. Ketika Anda mewarisi dari objek yang berasal dari kelas ES6, Extend mengharapkan semua properti menjadi enumerable di dalamnya, tetapi ini tidak terjadi dengan ES6. Metode-metode ini ada dalam prototipe dan mereka memiliki enumerable: false. Saya menggunakan kruk seperti ini, di mana saya memeriksa prototipe dan menghidupkan enumerable: true:

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

Saya berharap suatu hari nanti saya bisa menyingkirkannya, karena ada pembicaraan di komunitas Emscripten tentang peningkatan dukungan untuk ES6.

RAM


Berbicara tentang C ++, orang tidak bisa tidak menyebutkan memori. Ketika kami memeriksa semuanya pada video berkualitas SD, semuanya baik-baik saja dengan kami, itu berfungsi dengan sempurna! Segera setelah kami melakukan tes FullHD, ada kekurangan memori. Tidak masalah, ada opsi TOTAL_MEMORY, yang menetapkan nilai memori awal untuk modul. Kami membuat setengah gigabyte, semuanya baik-baik saja, tetapi entah bagaimana itu tidak manusiawi bagi pengguna, karena kami menyimpan memori untuk semua orang, tetapi tidak semua orang berlangganan konten FullHD.

Ada opsi lain - ALLOW_MEMORY_GROWTH. Ini memungkinkan Anda untuk menumbuhkan memori
secara bertahap sesuai kebutuhan. Ini bekerja seperti ini: Emscripten secara default memberikan modul 16 megabyte untuk operasi. Ketika Anda semua menggunakannya, sepotong memori baru dialokasikan. Semua data lama disalin di sana, dan Anda masih memiliki jumlah ruang yang sama untuk yang baru. Ini terjadi hingga Anda mencapai 4 GB.

Misalkan Anda mengalokasikan 256 megabyte memori, tetapi Anda tahu pasti bahwa Anda berpikir bahwa aplikasi Anda memiliki cukup 192. Kemudian sisa memori akan digunakan secara tidak efisien. Anda menyorotnya, mengambilnya dari pengguna, tetapi tidak melakukan apa-apa dengannya. Saya ingin menghindari ini. Ada trik kecil: kita mulai bekerja dengan ingatan meningkat satu setengah kali. Kemudian pada langkah ketiga kita mencapai 192 megabyte, dan inilah yang kita butuhkan. Kami telah mengurangi konsumsi memori dengan sisa itu dan menghemat alokasi memori yang tidak perlu, dan semakin lama, semakin lama. Karena itu, saya sarankan untuk menggunakan kedua opsi ini bersama-sama.

Ketergantungan injeksi


Tampaknya itu saja, tetapi kemudian menyapu sedikit lebih. Ada masalah dengan Injeksi Ketergantungan. Kami menulis kelas paling sederhana di mana ketergantungan diperlukan.

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

Misalnya, kami meneruskan klien HTTP kami ke aplikasi kami. Kami menyimpan di properti kelas. Tampaknya semuanya akan bekerja dengan baik.

 Module.App.extend( β€³Appβ€³, new App(client) ) 

Kami mewarisi dari antarmuka C ++, pertama membuat objek kami, meneruskan ketergantungan padanya, dan kemudian mewarisi. Pada saat pewarisan, Emscripten melakukan sesuatu yang luar biasa dengan objek tersebut. Paling mudah untuk berpikir bahwa itu membunuh objek lama, membuat yang baru berdasarkan templatnya dan menyeret semua metode publik ke sana. Tetapi pada saat yang sama, keadaan objek hilang, dan Anda mendapatkan objek yang tidak terbentuk dan tidak berfungsi dengan benar. Memecahkan masalah ini cukup sederhana. Diperlukan untuk menggunakan konstruktor yang berfungsi setelah tahap pewarisan.

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

Kami melakukan hal yang hampir sama: kami menyimpan ketergantungan pada bidang objek, tetapi ini adalah objek yang berubah setelah pewarisan. Kita tidak boleh lupa untuk meneruskan panggilan konstruktor ke objek induk, yang terletak di sisi C ++. Baris terakhir adalah analog dari metode super () di ES6. Inilah bagaimana pewarisan terjadi dalam kasus ini:

 const appConstr = Module.App.extend( β€³Appβ€³, new App() ) const app = new appConstr(client) 

Pertama, kita mewarisi, lalu membuat objek baru di mana ketergantungan sudah dilewati, dan ini berfungsi.

Trik Pointer


Masalah lain adalah melewatkan objek dengan pointer dari C ++ ke JavaScript. Kami sudah melakukan klien HTTP. Untuk mempermudah, kami telah melewatkan satu detail penting.

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

Metode mengembalikan nilai segera, yaitu, ternyata permintaan harus sinkron. Tapi bagaimanapun, AJAX meminta AJAX dan mereka tidak sinkron, jadi dalam kehidupan nyata metode ini tidak akan mengembalikan apa-apa, atau kita dapat mengembalikan ID permintaan. Tetapi untuk meminta seseorang mengembalikan jawabannya, kami meneruskan pendengar sebagai parameter kedua, di mana akan ada panggilan balik dari C ++.

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

Di JS, tampilannya seperti ini:

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

Kami memiliki fungsi get yang mengambil objek pendengar ini. Kami mulai mengunduh file dan menutup panggilan balik. Saat file diunduh, kami menarik fungsi yang diinginkan dari pendengar dan meneruskan hasilnya.

Tampaknya rencana itu baik, tetapi ketika fungsi get selesai, semua variabel lokal akan dihancurkan, dan bersama mereka parameter fungsi, yaitu, pointer akan dihancurkan, dan runtime emscripten akan menghancurkan objek di sisi C ++.

Akibatnya, ketika harus memanggil line listener.onResult (result), listener tidak akan ada lagi, dan ketika mengaksesnya, kesalahan akses memori akan terjadi yang akan menyebabkan crash aplikasi.

Saya ingin menghindari ini, dan ada solusinya, tetapi butuh beberapa minggu untuk menemukannya.

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

Ternyata ada metode untuk mengkloning pointer. Untuk beberapa alasan, ini tidak didokumentasikan, tetapi berfungsi dengan baik, dan memungkinkan Anda untuk meningkatkan jumlah referensi di pointer Emscripten. Ini memungkinkan kita untuk menangguhkannya dalam penutupan, dan kemudian, ketika kita meluncurkan panggilan balik kita, pendengar kita akan dapat diakses oleh pointer ini dan kita dapat bekerja sesuai kebutuhan.

Yang paling penting adalah jangan lupa untuk menghapus pointer ini, jika tidak maka akan menyebabkan kesalahan kebocoran memori, yang sangat buruk.

Menulis cepat ke memori


Saat kami mengunduh video, ini adalah jumlah informasi yang relatif besar, dan saya ingin mengurangi jumlah menyalin data bolak-balik untuk menghemat memori dan waktu. Ada satu trik tentang cara menulis sejumlah besar informasi langsung ke memori WebAssembly dari 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 adalah data kami sebagai array yang diketik. Kita dapat mengambil panjangnya dan meminta alokasi memori dengan ukuran yang kita butuhkan dari modul WebAssembly. Fungsi malloc akan mengembalikan pointer kepada kami, yang hanya merupakan indeks dari array yang berisi semua memori di WebAssembly. Dari sisi JavaScript, sepertinya ArrayBuffer.

Pada langkah berikutnya, kita akan memotong jendela ke ArrayBuffer ini dengan ukuran yang tepat dari tempat tertentu dan menyalin data kita di sana. Terlepas dari kenyataan bahwa operasi yang ditetapkan memiliki salinan semantik, ketika saya melihat bagian ini di profiler, saya tidak melihat proses yang panjang. Saya pikir browser mengoptimalkan operasi ini dengan bantuan semantik bergerak, yaitu, mentransfer kepemilikan memori dari satu objek ke objek lainnya.

Dan dalam aplikasi kami, kami juga mengandalkan semantik bergerak untuk menghemat penyalinan memori.

Adblock


Masalah yang menarik, lebih tepatnya, pada perubahan, dengan Adblock. Ternyata di Rusia semua pemblokir populer menerima berlangganan Daftar RU, dan memiliki aturan yang begitu indah yang melarang mengunduh WebAssembly dari situs pihak ketiga. Misalnya dengan CDN.



Jalan keluarnya bukan menggunakan CDN, tetapi untuk menyimpan semuanya di domain Anda (ini tidak cocok untuk kami). Atau ganti nama file .wasm sehingga tidak cocok dengan aturan ini. Anda masih dapat pergi ke forum kawan-kawan ini dan mencoba meyakinkan mereka untuk menghapus aturan ini. Saya pikir mereka membenarkan diri mereka sendiri dengan melawan para penambang dengan cara ini, meskipun saya tidak tahu mengapa para penambang tidak bisa menebak untuk mengganti nama file.

Produksi


Akibatnya, kami mulai berproduksi. Ya, itu tidak mudah, butuh 8 bulan dan saya ingin bertanya pada diri sendiri apakah itu layak. Menurut pendapat saya - itu layak:

Tidak perlu menginstal


Kami mendapat bahwa kode kami dikirimkan kepada pengguna tanpa menginstal program apa pun. Ketika kami memiliki plug-in browser, pengguna harus mengunduh dan menginstalnya, dan ini adalah filter besar untuk distribusi teknologi. Sekarang pengguna hanya menonton video di situs dan bahkan tidak mengerti bahwa seluruh mesin bekerja di bawah tenda, dan semuanya rumit di sana. Peramban hanya mengunduh file tambahan dengan kode, seperti gambar atau .css.

Basis kode terpadu dan debugging pada platform berbeda


Pada saat yang sama, kami dapat mempertahankan basis kode tunggal kami. Kita dapat memutar kode yang sama pada platform yang berbeda dan telah berulang kali terjadi bahwa bug yang tidak terlihat di salah satu platform muncul di platform lain. Dan dengan demikian, kita dapat mendeteksi bug tersembunyi dengan alat berbeda di platform berbeda.

Rilis cepat


Kami mendapatkan rilis cepat, karena kami dapat dirilis sebagai aplikasi web sederhana dan memperbarui kode C ++ dengan setiap rilis baru. Itu tidak dibandingkan dengan cara melepaskan plugin baru, aplikasi seluler, atau aplikasi SmartTV. Rilis ini hanya tergantung pada kita: kapan kita mau, maka itu akan dirilis.

Umpan balik cepat


Dan itu berarti umpan balik cepat: jika terjadi kesalahan, pada siang hari kita dapat mengetahui bahwa ada masalah dan menanggapinya.

Saya percaya bahwa semua masalah ini sepadan dengan keuntungan ini. Tidak semua orang memiliki aplikasi C ++, tetapi jika Anda memilikinya dan Anda menginginkannya ada di browser - WebAssembly adalah case use 100% untuk Anda.

Tempat melamar


Tidak semua orang menulis di C ++. Tetapi tidak hanya C ++ yang tersedia untuk WebAssembly. Ya, ini secara historis platform pertama yang masih tersedia di asm.js, teknologi Mozilla awal. Omong-omong, karena itu, ia memiliki alat yang cukup bagus mereka lebih tua dari teknologi itu sendiri.

Karat


Bahasa Rust yang baru, yang juga sedang dikembangkan oleh Mozilla, sekarang mengejar dan menyalip C ++ dalam hal alat. Semuanya berjalan ke titik bahwa mereka akan membuat proses pengembangan paling keren untuk WebAssembly.

Lua, Perl, Python, PHP, dll.


Hampir semua bahasa yang ditafsirkan juga tersedia di WebAssembly, karena penerjemah mereka ditulis dalam C ++, mereka hanya dikompilasi ke dalam WebAssembly dan sekarang Anda dapat memutar PHP di browser.

Pergi


Di versi 1.11 mereka membuat versi kompilasi versi beta di WebAssembly, di 2.0 mereka menjanjikan dukungan rilis. Dukungan mereka muncul kemudian, karena WebAssembly tidak mendukung pengumpul sampah, dan Go adalah bahasa memori yang dikelola. Jadi mereka harus menyeret pengumpul sampah mereka di bawah WebAssembly.

Kotlin / Asli


Tentang kisah yang sama dengan Kotlin. Kompiler mereka memiliki dukungan eksperimental, tetapi mereka juga harus melakukan sesuatu dengan pemulung. Saya tidak tahu status apa yang ada.

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.


Terima kasih atas perhatian anda! .



HolyJS, : 24-25 HolyJS . (, Node.js Ryan Dahl!), β€” 1 .

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


All Articles