Kompilasi C di WebAssembly tanpa Emscripten

Kompiler adalah bagian dari Emscripten . Tetapi bagaimana jika Anda menghapus semua peluit dan hanya meninggalkannya?

Emscripten diperlukan untuk mengkompilasi C / C ++ ke dalam WebAssembly . Tapi ini lebih dari sekedar kompiler. Tujuan Emscripten adalah untuk sepenuhnya mengganti kompiler C / C ++ Anda dan menjalankan kode di web yang awalnya tidak dirancang untuk Web. Untuk ini, Emscripten mengemulasi seluruh sistem operasi POSIX. Jika program menggunakan fopen () , maka Emscripten akan memberikan emulasi sistem file. Jika OpenGL digunakan, Emscripten akan memberikan konteks GL yang kompatibel dengan C yang didukung oleh WebGL . Ini banyak pekerjaan, dan banyak kode yang harus diimplementasikan dalam paket akhir. Tapi bisakah Anda ... menghapusnya?

Kompilator aktual dalam toolkit Emscripten adalah LLVM. Dialah yang menerjemahkan kode C ke dalam bytecode WebAssembly. Ini adalah kerangka kerja modular modern untuk analisis, transformasi dan optimalisasi program. LLVM bersifat modular dalam arti tidak pernah dikompilasi secara langsung ke dalam kode mesin. Alih-alih, kompiler front-end bawaan menghasilkan representasi perantara (IR). Representasi perantara ini, pada kenyataannya, disebut LLVM, kependekan dari Low-Level Virtual Machine, karenanya nama proyek.

Kompiler backend kemudian menerjemahkan IR ke dalam kode mesin host. Keuntungan dari pemisahan ketat ini adalah bahwa arsitektur baru didukung oleh penambahan "sederhana" dari kompiler baru. Dalam hal ini, WebAssembly hanyalah salah satu dari banyak tujuan kompilasi yang didukung LLVM, dan untuk beberapa waktu telah diaktifkan dengan flag khusus. Dimulai dengan LLVM 8, target kompilasi WebAssembly tersedia secara default.

Di MacOS, Anda dapat menginstal LLVM menggunakan homebrew :

$ brew install llvm $ brew link --force llvm 

Periksa dukungan WebAssembly:

 $ llc --version LLVM (http://llvm.org/): LLVM version 8.0.0 Optimized build. Default target: x86_64-apple-darwin18.5.0 Host CPU: skylake Registered Targets: # …,  … systemz - SystemZ thumb - Thumb thumbeb - Thumb (big endian) wasm32 - WebAssembly 32-bit # ! ! ! wasm64 - WebAssembly 64-bit x86 - 32-bit X86: Pentium-Pro and above x86-64 - 64-bit X86: EM64T and AMD64 xcore - XCore 

Sepertinya kita sudah siap!

Kompilasi C dengan Hard Way


Catatan: berikut adalah beberapa format RAW WebAssembly tingkat rendah. Jika Anda merasa sulit untuk dipahami, ini normal. Penggunaan WebAssembly yang baik tidak memerlukan pemahaman seluruh teks dalam artikel ini. Jika Anda mencari kode untuk menempelkan salinan, lihat panggilan ke kompiler di bagian Optimasi . Tetapi jika Anda tertarik, teruslah membaca! Saya sebelumnya menulis pengantar Webassembly dan WAT murni : ini adalah dasar yang diperlukan untuk memahami posting ini.
Peringatan: Saya akan sedikit menyimpang dari standar dan mencoba menggunakan format yang dapat dibaca manusia di setiap langkah (sejauh mungkin). Program kami di sini akan sangat sederhana untuk menghindari situasi perbatasan dan tidak terganggu:

 // Filename: add.c int add(int a, int b) { return a*a + b; } 

Sungguh prestasi teknik yang luar biasa! Terutama karena program ini disebut add , tetapi pada kenyataannya ia tidak menambahkan apa-apa (tidak menambah). Lebih penting lagi: program tidak menggunakan pustaka standar, dan dari jenis di sini, hanya 'int'.

Mengubah C menjadi Tampilan LLVM Internal


Langkah pertama adalah mengubah program C kami menjadi LLVM IR. Ini adalah tugas dari compiler frontend clang , yang diinstal dengan LLVM:

 clang \ --target=wasm32 \ # Target WebAssembly -emit-llvm \ # Emit LLVM IR (instead of host machine code) -c \ # Only compile, no linking just yet -S \ # Emit human-readable assembly rather than binary add.c 

Dan sebagai hasilnya, kita mendapatkan add.ll dengan representasi internal dari LLVM IR. Saya tunjukkan hanya demi kelengkapan . Saat bekerja dengan WebAssembly atau bahkan dentang, Anda sebagai pengembang C tidak akan pernah berhubungan dengan LLVM IR.

 ; ModuleID = 'add.c' source_filename = "add.c" target datalayout = "em:ep:32:32-i64:64-n32:64-S128" target triple = "wasm32" ; Function Attrs: norecurse nounwind readnone define hidden i32 @add(i32, i32) local_unnamed_addr #0 { %3 = mul nsw i32 %0, %0 %4 = add nsw i32 %3, %1 ret i32 %4 } attributes #0 = { norecurse nounwind readnone "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="false" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.module.flags = !{!0} !llvm.ident = !{!1} !0 = !{i32 1, !"wchar_size", i32 4} !1 = !{!"clang version 8.0.0 (tags/RELEASE_800/final)"} 

LLVM IR penuh dengan metadata dan anotasi tambahan, yang memungkinkan kompiler membuat keputusan yang lebih tepat ketika membuat kode mesin.

Ubah LLVM IR menjadi file objek


Langkah selanjutnya adalah memanggil kompiler backend llc untuk membuat file objek dari representasi internal.

add.o keluaran add.o sudah menjadi modul WebAssembly yang valid yang berisi semua kode yang dikompilasi dari file C. Tetapi biasanya Anda tidak akan dapat menjalankan file objek karena mereka tidak memiliki bagian-bagian penting.

Jika kita -filetype=obj dalam perintah, kita akan mendapatkan assembler LLVM untuk WebAssembly, format yang dapat dibaca manusia yang agak mirip dengan WAT. Namun, alat llvm-mc untuk bekerja dengan file tersebut belum sepenuhnya mendukung format dan sering tidak dapat memproses file. Oleh karena itu, kami membongkar file objek setelah fakta. Diperlukan alat khusus untuk memverifikasi file objek ini. Dalam kasus WebAssembly, itu adalah wasm-objdump , bagian dari WebAssembly Binary Toolkit atau singkatnya.

 $ brew install wabt # in case you haven't $ wasm-objdump -x add.o add.o: file format wasm 0x1 Section Details: Type[1]: - type[0] (i32, i32) -> i32 Import[3]: - memory[0] pages: initial=0 <- env.__linear_memory - table[0] elem_type=funcref init=0 max=0 <- env.__indirect_function_table - global[0] i32 mutable=1 <- env.__stack_pointer Function[1]: - func[0] sig=0 <add> Code[1]: - func[0] size=75 <add> Custom: - name: "linking" - symbol table [count=2] - 0: F <add> func=0 binding=global vis=hidden - 1: G <env.__stack_pointer> global=0 undefined binding=global vis=default Custom: - name: "reloc.CODE" - relocations for section: 3 (Code) [1] R_WASM_GLOBAL_INDEX_LEB offset=0x000006(file=0x000080) symbol=1 <env.__stack_pointer> 

Output menunjukkan bahwa fungsi add () kami ada di modul ini, tetapi juga berisi bagian khusus dengan metadata dan, yang mengejutkan, beberapa impor. Pada tahap penautan selanjutnya, bagian khusus akan dianalisis dan dihapus, dan penaut (penghubung) akan berurusan dengan impor.

Tata letak


Secara tradisional, tugas penghubung adalah untuk merakit beberapa file objek menjadi file yang dapat dieksekusi. Linker LLVM disebut lld , dan dipanggil dengan target symlink. Untuk WebAssembly, ini wasm-ld .

 wasm-ld \ --no-entry \ # We don't have an entry function --export-all \ # Export everything (for now) -o add.wasm \ add.o 

Hasilnya adalah modul WebAssembly berukuran 262 byte.

Luncurkan


Tentu saja, yang paling penting adalah untuk melihat bahwa semuanya benar-benar berfungsi. Seperti pada artikel terakhir , Anda dapat menggunakan beberapa baris JavaScript tertanam untuk memuat dan menjalankan modul WebAssembly ini.

 <!DOCTYPE html> <script type="module"> async function init() { const { instance } = await WebAssembly.instantiateStreaming( fetch("./add.wasm") ); console.log(instance.exports.add(4, 1)); } init(); </script> 

Jika semuanya baik-baik saja, Anda akan melihat nomor 17 di konsol DevTool. Kami baru saja berhasil mengkompilasi C ke dalam WebAssembly tanpa menyentuh Emscripten. Perlu juga dicatat bahwa tidak ada middleware untuk mengkonfigurasi dan memuat modul WebAssembly.

Kompilasi C sedikit lebih sederhana


Untuk mengkompilasi C di WebAssembly, kami telah mengambil banyak langkah. Seperti yang saya katakan, untuk tujuan pendidikan, kami memeriksa secara rinci semua tahapan. Mari kita lompati format perantara yang dapat dibaca manusia dan segera menerapkan kompiler C sebagai pisau tentara Swiss, seperti yang dikembangkan:

 clang \ --target=wasm32 \ -nostdlib \ # Don't try and link against a standard library -Wl,--no-entry \ # Flags passed to the linker -Wl,--export-all \ -o add.wasm \ add.c 

Di sini kita mendapatkan file .wasm sama, tetapi dengan satu perintah.

Optimasi


Lihatlah modul WAT WebAssembly kami dengan menjalankan wasm2wat :

 (module (type (;0;) (func)) (type (;1;) (func (param i32 i32) (result i32))) (func $__wasm_call_ctors (type 0)) (func $add (type 1) (param i32 i32) (result i32) (local i32 i32 i32 i32 i32 i32 i32 i32) global.get 0 local.set 2 i32.const 16 local.set 3 local.get 2 local.get 3 i32.sub local.set 4 local.get 4 local.get 0 i32.store offset=12 local.get 4 local.get 1 i32.store offset=8 local.get 4 i32.load offset=12 local.set 5 local.get 4 i32.load offset=12 local.set 6 local.get 5 local.get 6 i32.mul local.set 7 local.get 4 i32.load offset=8 local.set 8 local.get 7 local.get 8 i32.add local.set 9 local.get 9 return) (table (;0;) 1 1 anyfunc) (memory (;0;) 2) (global (;0;) (mut i32) (i32.const 66560)) (global (;1;) i32 (i32.const 66560)) (global (;2;) i32 (i32.const 1024)) (global (;3;) i32 (i32.const 1024)) (export "memory" (memory 0)) (export "__wasm_call_ctors" (func $__wasm_call_ctors)) (export "__heap_base" (global 1)) (export "__data_end" (global 2)) (export "__dso_handle" (global 3)) (export "add" (func $add))) 

Wow, kode yang luar biasa. Yang mengejutkan saya, modul ini menggunakan memori (seperti yang terlihat dari operasi i32.load dan i32.store ), delapan lokal dan beberapa variabel global. Mungkin, Anda dapat secara manual menulis versi yang lebih ringkas. Program ini sangat besar karena kami tidak menerapkan optimasi apa pun. Mari kita lakukan:

 clang \ --target=wasm32 \ + -O3 \ # Agressive optimizations + -flto \ # Add metadata for link-time optimizations -nostdlib \ -Wl,--no-entry \ -Wl,--export-all \ + -Wl,--lto-O3 \ # Aggressive link-time optimizations -o add.wasm \ add.c 

Catatan: secara teknis, optimisasi tata letak (LTO) tidak memberikan keuntungan karena kami hanya membuat satu file. Dalam proyek-proyek besar, KPP akan membantu mengurangi ukuran file secara signifikan.
Setelah menjalankan perintah ini, file .wasm menurun dari 262 menjadi 197 byte, dan WAT juga menjadi lebih sederhana:

 (module (type (;0;) (func)) (type (;1;) (func (param i32 i32) (result i32))) (func $__wasm_call_ctors (type 0)) (func $add (type 1) (param i32 i32) (result i32) local.get 0 local.get 0 i32.mul local.get 1 i32.add) (table (;0;) 1 1 anyfunc) (memory (;0;) 2) (global (;0;) (mut i32) (i32.const 66560)) (global (;1;) i32 (i32.const 66560)) (global (;2;) i32 (i32.const 1024)) (global (;3;) i32 (i32.const 1024)) (export "memory" (memory 0)) (export "__wasm_call_ctors" (func $__wasm_call_ctors)) (export "__heap_base" (global 1)) (export "__data_end" (global 2)) (export "__dso_handle" (global 3)) (export "add" (func $add))) 

Panggil perpustakaan standar


Menggunakan C tanpa perpustakaan libc standar tampaknya agak kasar. Adalah logis untuk menambahkannya, tetapi saya akan jujur: itu tidak akan mudah. Faktanya, kami tidak secara langsung memanggil perpustakaan libc apa pun di artikel . Ada beberapa yang cocok, terutama glibc , musl , dan dietlibc . Namun, sebagian besar pustaka ini seharusnya berjalan di sistem operasi POSIX, yang mengimplementasikan serangkaian panggilan sistem tertentu. Karena kita tidak memiliki antarmuka kernel dalam JavaScript, kita harus mengimplementasikan sendiri panggilan sistem POSIX ini, mungkin melalui JavaScript. Ini adalah tugas yang sulit dan saya tidak akan melakukannya di sini. Berita baiknya adalah ini yang dilakukan Emscripten untuk Anda .

Tentu saja, tidak semua fungsi libc bergantung pada panggilan sistem. Fungsi-fungsi seperti strlen() , sin() atau bahkan memset() diimplementasikan dalam C. sederhana. Ini berarti bahwa Anda dapat menggunakan fungsi-fungsi ini atau bahkan hanya menyalin / menempel implementasinya dari beberapa perpustakaan yang disebutkan.

Memori dinamis


Tanpa libc, antarmuka dasar C seperti malloc() dan free() tidak tersedia untuk kami. Dalam WAT yang tidak dioptimalkan, kami melihat bahwa kompiler menggunakan memori jika perlu. Ini berarti bahwa kita tidak bisa hanya menggunakan memori sesuka kita, tanpa risiko merusaknya. Anda perlu memahami bagaimana ini digunakan.

Model memori LLVM


Metode segmentasi memori WebAssembly akan mengejutkan programmer berpengalaman sedikit. Pertama, di WebAssembly, alamat null diizinkan secara teknis, tetapi sering kali masih diperlakukan sebagai kesalahan. Kedua, tumpukan lebih dulu dan tumbuh turun (ke alamat yang lebih rendah), dan tumpukan muncul kemudian dan tumbuh. Alasannya adalah bahwa memori WebAssembly dapat meningkat saat runtime. Ini berarti bahwa tidak ada ujung yang tetap untuk mengakomodasi tumpukan atau tumpukan.

Berikut ini tata letak wasm-ld :



Tumpukan tumbuh turun, dan tumpukan tumbuh. Tumpukan dimulai dengan __data_end , dan heap __heap_base dengan __heap_base . Karena tumpukan ditempatkan terlebih dahulu, ia dibatasi oleh ukuran maksimum yang ditetapkan selama kompilasi, mis. __heap_base dikurangi __data_end

Jika kita kembali dan melihat bagian global di WAT kita, kita menemukan nilai-nilai ini: __heap_base diatur ke 66560, dan __data_end diatur ke 1024. Ini berarti bahwa tumpukan dapat tumbuh hingga maksimum 64 KiB, yang tidak banyak. Untungnya, wasm-ld memungkinkan Anda untuk mengubah nilai ini:

 clang \ --target=wasm32 \ -O3 \ -flto \ -nostdlib \ -Wl,--no-entry \ -Wl,--export-all \ -Wl,--lto-O3 \ + -Wl,-z,stack-size=$[8 * 1024 * 1024] \ # Set maximum stack size to 8MiB -o add.wasm \ add.c 

Majelis alokator


Area tumpukan diketahui dimulai dengan __heap_base . Karena fungsi malloc() tidak ada, kami tahu bahwa area memori berikutnya dapat digunakan dengan aman. Kita dapat menempatkan data di sana sesuka kita, dan tidak perlu takut akan kerusakan memori, karena tumpukan tumbuh ke arah lain. Namun, tumpukan yang gratis untuk semua orang bisa dengan cepat tersumbat, jadi biasanya diperlukan semacam manajemen memori dinamis. Salah satu opsi adalah mengambil implementasi malloc () lengkap, seperti implementasi malloc dari Doug Lee , yang digunakan dalam Emscripten. Ada beberapa implementasi kecil dengan berbagai trade-off.

Tapi mengapa tidak menulis malloc() Anda sendiri malloc() ? Kami sangat terhambat sehingga tidak ada bedanya. Salah satu yang paling sederhana adalah pengalokasi benjolan: sangat cepat, sangat kecil dan mudah diimplementasikan. Tetapi ada kekurangannya: Anda tidak bisa membebaskan memori. Meskipun sekilas pengalokasian seperti itu tampaknya sangat tidak berguna, tetapi ketika mengembangkan Squoosh, saya menemukan preseden di mana itu akan menjadi pilihan yang sangat baik. Konsep pengalokasi benjolan adalah bahwa kami menyimpan alamat awal memori yang tidak digunakan sebagai global. Jika program meminta n byte memori, kami memindahkan marker ke n dan mengembalikan nilai sebelumnya:

 extern unsigned char __heap_base; unsigned int bump_pointer = &__heap_base; void* malloc(int n) { unsigned int r = bump_pointer; bump_pointer += n; return (void *)r; } void free(void* p) { // lol } 

Variabel global dari WAT sebenarnya didefinisikan oleh wasm-ld , sehingga kita dapat mengaksesnya dari kode C sebagai variabel biasa jika kita mendeklarasikannya sebagai extern . Jadi, kami hanya menulis malloc() kami sendiri malloc() ... dalam lima baris C.

Catatan: pengalokasi benjolan kami tidak sepenuhnya kompatibel dengan malloc() dari C. Misalnya, kami tidak memberikan jaminan penyelarasan. Tapi itu bekerja dengan cukup baik, jadi ...

Penggunaan memori dinamis


Untuk menguji, mari kita membuat fungsi C, yang mengambil larik angka dengan ukuran acak dan menghitung jumlahnya. Tidak terlalu menarik, tetapi ini memaksa kita untuk menggunakan memori dinamis, karena kita tidak tahu ukuran array saat perakitan:

 int sum(int a[], int len) { int sum = 0; for(int i = 0; i < len; i++) { sum += a[i]; } return sum; } 

Fungsi penjumlahan (), mudah-mudahan, cukup mudah. Pertanyaan yang lebih menarik adalah bagaimana cara mengirimkan array dari JavaScript ke WebAssembly - setelah semua, WebAssembly hanya memahami angka. Gagasan umum adalah menggunakan malloc() dari JavaScript untuk mengalokasikan memori, menyalin nilai-nilai di sana dan meneruskan alamat (nomor!) Di mana array berada:

 <!DOCTYPE html> <script type="module"> async function init() { const { instance } = await WebAssembly.instantiateStreaming( fetch("./add.wasm") ); const jsArray = [1, 2, 3, 4, 5]; // Allocate memory for 5 32-bit integers // and return get starting address. const cArrayPointer = instance.exports.malloc(jsArray.length * 4); // Turn that sequence of 32-bit integers // into a Uint32Array, starting at that address. const cArray = new Uint32Array( instance.exports.memory.buffer, cArrayPointer, jsArray.length ); // Copy the values from JS to C. cArray.set(jsArray); // Run the function, passing the starting address and length. console.log(instance.exports.sum(cArrayPointer, cArray.length)); } init(); </script> 

Setelah memulai, Anda akan melihat jawaban 15 di konsol DevTools, yang sebenarnya merupakan jumlah semua angka dari 1 hingga 5.

Kesimpulan


Jadi, Anda membaca sampai akhir. Selamat! Sekali lagi, jika Anda merasa sedikit kelebihan beban, semuanya beres. Tidak perlu membaca semua detail. Memahami mereka sepenuhnya opsional untuk pengembang web yang baik dan bahkan tidak diperlukan untuk penggunaan WebAssembly yang luar biasa . Tetapi saya ingin membagikan informasi ini, karena ini memungkinkan Anda untuk benar-benar menghargai semua pekerjaan yang dilakukan proyek seperti Emscripten untuk Anda. Pada saat yang sama, ini memberikan pemahaman tentang betapa kecilnya modul komputasi WebAssembly yang murni. Modul Wasm untuk menjumlahkan array hanya berukuran 230 byte, termasuk pengalokasi memori dinamis . Mengkompilasi kode yang sama dengan Emscripten akan menghasilkan 100 byte kode WebAssembly dan 11K kode tautan JavaScript. Kita harus berusaha demi hasil seperti itu, tetapi ada situasi ketika itu sepadan.

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


All Articles