Terkadang, Anda ingin menggunakan library yang hanya tersedia sebagai kode C atau C++. Biasanya, ini adalah saatnya Anda menyerah. Tidak lagi, karena sekarang kita punya Emscripten dan WebAssembly (atau Wasm)!
Toolchain
Saya menetapkan tujuan saya sendiri bagaimana mengkompilasi beberapa kode C yang ada untuk Wasman. Ada derau di sekitar backend Wasm LLVM, jadi Saya mulai mencari tahu tentang hal itu. Meskipun Anda bisa mendapatkan program sederhana untuk dikompilasi dengan cara ini, saat Anda ingin menggunakan pustaka standar C atau bahkan mengompilasi banyak file, Anda mungkin akan mengalami masalah. Hal ini membawa saya ke posisi utama pelajaran yang saya pelajari:
Meskipun digunakan sebagai compiler C-to-asm.js, Emscripten kini berkembang menjadi menargetkan Wasm dan dalam proses pengalihan ke backend LLVM resmi secara internal. Emscripten juga memberikan Implementasi library standar C yang kompatibel dengan Wasm. Gunakan Emscripten. Ini berisi banyak pekerjaan tersembunyi, mengemulasi sistem file, menyediakan manajemen memori, menggabungkan OpenGL dengan WebGL — hal yang sebenarnya tidak perlu Anda kembangkan untuk diri sendiri.
Meskipun mungkin terdengar seperti Anda perlu khawatir tentang kembung, saya tentu saja khawatir — compiler Emscripten menghapus semua yang tidak diperlukan. Di .... saya eksperimen, modul Wasm yang dihasilkan memiliki ukuran yang tepat sesuai logika yang dimilikinya dan tim Emscripten dan WebAssembly sedang mengerjakan pembuatan dapat menjadi jauh lebih kecil di masa depan.
Anda bisa mendapatkan Emscripten dengan mengikuti petunjuk di situs atau menggunakan Homebrew. Jika Anda adalah penggemar perintah Docker{i> <i}seperti saya dan tidak ingin menginstal banyak hal di sistem Anda hanya bermain dengan WebAssembly, ada sebuah layanan Image Docker yang dapat Anda gunakan sebagai gantinya:
$ docker pull trzeci/emscripten
$ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>
Mengompilasi sesuatu yang sederhana
Mari kita ambil contoh hampir kanonik dari penulisan fungsi di C yang menghitung bilangan fibonaccith:
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
if(n <= 0){
return 0;
}
int i, t, a = 0, b = 1;
for (i = 1; i < n; i++) {
t = a + b;
a = b;
b = t;
}
return b;
}
Jika Anda tahu C, fungsi itu sendiri seharusnya tidak terlalu mengejutkan. Bahkan jika Anda tidak tahu C tapi mengerti JavaScript, Anda diharapkan dapat memahami apa yang terjadi.
emscripten.h
adalah file header yang disediakan oleh Emscripten. Kita hanya memerlukannya sehingga
memiliki akses ke makro EMSCRIPTEN_KEEPALIVE
, tetapi
menyediakan lebih banyak fungsi.
Makro ini memberi tahu compiler untuk tidak menghapus fungsi meskipun fungsi tersebut muncul
tidak digunakan. Jika kita menghilangkan makro tersebut, compiler akan mengoptimalkan fungsi
— tidak ada yang menggunakannya.
Mari kita simpan semua itu dalam file bernama fib.c
. Untuk mengubahnya menjadi file .wasm
, kita
perlu beralih ke perintah compiler Emscripten emcc
:
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c
Mari kita bahas perintah ini. emcc
adalah compiler Emscripten. fib.c
adalah C kita
. Sejauh ini, hasilnya bagus. -s WASM=1
memberi tahu Emscripten untuk memberi kita file Wasm
bukan file asm.js.
-s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'
memberi tahu compiler untuk keluar dari
Fungsi cwrap()
tersedia di file JavaScript — selengkapnya tentang fungsi ini
nanti. -O3
memberi tahu compiler untuk mengoptimalkan secara agresif. Anda dapat memilih lebih rendah
angka untuk mengurangi waktu build, tetapi hal ini juga akan membuat paket yang dihasilkan
lebih besar karena kompiler mungkin tidak
menghapus kode yang tidak digunakan.
Setelah menjalankan perintah, Anda akan mendapatkan file JavaScript bernama
a.out.js
dan file WebAssembly bernama a.out.wasm
. File Wasm (atau
"module") berisi kompilasi kode C kita dan ukurannya akan cukup kecil. Tujuan
file JavaScript menangani pemuatan dan
inisialisasi modul Wasm dan
menyediakan API yang lebih baik. Jika diperlukan, Gemini juga akan
menangani pengaturan
tumpukan, heap, dan fungsionalitas lain yang biasanya diharapkan disediakan oleh
sistem operasi saat
menulis kode C. Dengan demikian, file JavaScript sedikit
lebih besar, dengan berat 19KB (~5KB gzip).
Menjalankan sesuatu yang sederhana
Cara termudah untuk memuat dan menjalankan modul Anda adalah dengan menggunakan JavaScript yang dihasilkan
. Setelah Anda memuat file tersebut, Anda akan memiliki
Module
global
bagi Anda. Gunakan
cwrap
untuk membuat fungsi native JavaScript yang menangani parameter konversi
ke sesuatu yang ramah C dan
memanggil fungsi yang digabungkan. cwrap
mengambil
nama fungsi, jenis nilai yang ditampilkan, dan jenis argumen sebagai argumen, dalam urutan tersebut:
<script src="a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const fib = Module.cwrap('fib', 'number', ['number']);
console.log(fib(12));
};
</script>
Jika Anda jalankan kode ini, Anda akan melihat "144" di konsol, yang merupakan angka Fibonacci ke-12.
Tujuan: Mengompilasi {i>library<i} C
Sampai sekarang, kode C yang telah kita tulis ditulis dengan mempertimbangkan Wasm. Inti untuk WebAssembly, bagaimanapun, adalah dengan mengambil ekosistem C perpustakaan dan memungkinkan pengembang menggunakannya di web. Perpustakaan ini sering mengandalkan pustaka standar C, sistem operasi, sistem file dan banyak hal. Emscripten menyediakan sebagian besar fitur ini, meskipun ada beberapa batasan yang ada.
Mari kembali ke tujuan awal saya: mengompilasi encoder untuk WebP ke Wasm. Tujuan untuk codec WebP ditulis dalam C dan tersedia di GitHub serta beberapa ekstensi Dokumentasi API. Itu adalah titik awal yang cukup baik.
$ git clone https://github.com/webmproject/libwebp
Untuk memulai yang sederhana, mari kita coba mengekspos WebPGetEncoderVersion()
dari
encode.h
ke JavaScript dengan menulis file C bernama webp.c
:
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetEncoderVersion();
}
Ini adalah program sederhana yang baik untuk menguji apakah kita bisa mendapatkan kode sumber libwebp untuk dikompilasi, karena kita tidak memerlukan parameter atau struktur data yang kompleks untuk panggil fungsi ini.
Untuk mengompilasi program ini, kita perlu memberi
tahu kompilator di mana ia dapat menemukan
file header libwebp menggunakan flag -I
dan juga meneruskan semua file C ke
{i>libwebp<i} yang dibutuhkan. Saya akan jujur: Saya memberikan semua huruf C
file yang dapat saya temukan dan mengandalkan
{i>compiler<i} untuk menghapus semua yang
tidak diperlukan. Desainer ini tampaknya bekerja dengan cemerlang!
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
Sekarang kita hanya memerlukan beberapa HTML dan JavaScript untuk memuat modul baru kita:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = async (_) => {
const api = {
version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
</script>
Kita akan melihat nomor versi koreksi di output:
Mendapatkan gambar dari JavaScript ke Wasm
Mendapatkan nomor versi encoder adalah hal yang bagus, tetapi pengkodean akan lebih mengesankan, bukan? Kalau begitu, mari kita lakukan.
Pertanyaan pertama yang harus kita jawab adalah: Bagaimana cara memasukkan gambar ke area Wasm?
Melihat
encoding API libwebp, fitur ini mengharapkan
array byte dalam RGB, RGBA, BGR, atau BGRA. Untungnya, Canvas API memiliki
getImageData()
,
yang memberi kita
Uint8ClampedArray
yang berisi data gambar dalam RGBA:
async function loadImage(src) {
// Load image
const imgBlob = await fetch(src).then((resp) => resp.blob());
const img = await createImageBitmap(imgBlob);
// Make canvas same size as image
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// Draw image onto canvas
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
Sekarang "hanya" penyalinan data dari JavaScript akan diarahkan ke Wasm mendarat. Untuk itu, kita perlu mengekspos dua fungsi tambahan. Tugas yang mengalokasikan memori untuk gambar di dalam land Wasm dan yang mengosongkannya lagi:
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
return malloc(width * height * 4 * sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
free(p);
}
create_buffer
mengalokasikan buffer untuk gambar RGBA — karenanya 4 byte per piksel.
Pointer yang ditampilkan oleh malloc()
adalah alamat sel memori pertama
{i>buffer<i} itu. Saat pointer dikembalikan ke posisi JavaScript, ini diperlakukan sebagai
hanya sebuah angka. Setelah mengekspos fungsi ke JavaScript menggunakan cwrap
, kita dapat
gunakan angka itu untuk menemukan titik awal buffer
dan menyalin data gambar.
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
const image = await loadImage('/image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);
Grand Finale: Mengenkode gambar
Image tersebut kini tersedia di halaman Wasm. Sekarang saatnya memanggil encoder WebP untuk
lakukan tugasnya! Melihat
Dokumentasi WebP, WebPEncodeRGBA
sepertinya sangat cocok. Fungsi ini membawa pointer ke gambar input dan
dimensinya, serta opsi kualitas antara 0 dan 100. {i>Software<i} ini juga mengalokasikan
buffer output untuk kita, yang harus kita bebaskan menggunakan WebPFree()
setelah
dilakukan dengan gambar WebP.
Hasil operasi encoding adalah buffer output dan panjangnya. Karena fungsi di C tidak dapat memiliki {i>array <i}sebagai jenis nilai yang ditampilkan (kecuali jika kita mengalokasikan secara dinamis), saya menggunakan array global statis. Saya tahu, tidak bersih C (faktanya, itu bergantung pada fakta bahwa pointer Wasm lebarnya 32 bit), tetapi untuk menjaga segala sesuatunya sederhana, saya pikir ini adalah jalan pintas yang adil.
int result[2];
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* img_in, int width, int height, float quality) {
uint8_t* img_out;
size_t size;
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
result[0] = (int)img_out;
result[1] = size;
}
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t* result) {
WebPFree(result);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
return result[0];
}
EMSCRIPTEN_KEEPALIVE
int get_result_size() {
return result[1];
}
Sekarang dengan semua itu ada, kita bisa memanggil fungsi penyandian, ambil ukuran pointer dan gambar, memasukkannya ke dalam buffering JavaScript kita sendiri, dan melepaskan semua {i>buffer <i}Wasm-land yang telah kita alokasikan dalam proses.
api.encode(p, image.width, image.height, 100);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result(resultPointer);
Bergantung pada ukuran gambar, Anda mungkin akan mengalami error ketika Wasm tidak dapat menambah memori yang cukup untuk mengakomodasi gambar input dan output:
Untungnya, solusi untuk masalah ini ada dalam pesan error. Kita hanya perlu
menambahkan -s ALLOW_MEMORY_GROWTH=1
ke perintah kompilasi.
Demikianlah. Kami mengompilasi encoder WebP dan melakukan transcoding gambar JPEG untuk
WebP. Untuk membuktikan bahwa itu berhasil, kita dapat mengubah buffer hasil menjadi blob dan menggunakan
di elemen <img>
:
const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img);
Lihatlah, hebatnya gambar WebP baru!
Kesimpulan
Tidak perlu berjalan kaki di taman untuk membuat perpustakaan C berfungsi di {i>browser<i}, tetapi sekali Anda memahami keseluruhan proses dan bagaimana aliran data bekerja, itu menjadi lebih mudah dan hasilnya bisa mengejutkan.
WebAssembly membuka banyak kemungkinan baru di web untuk pemrosesan, {i>crunching <i}dan {i>game<i}. Perlu diingat bahwa Wasm bukanlah solusi alternatif yang seharusnya diterapkan pada semuanya, tetapi ketika Anda mencapai salah satu hambatan itu, Wasm dapat alat yang sangat membantu.
Konten bonus: Menjalankan sesuatu yang sederhana dengan cara yang sulit
Jika Anda ingin mencoba dan menghindari file JavaScript yang dihasilkan, Anda mungkin dapat tempat mesin terhubung. Mari kita kembali ke contoh Fibonacci. Untuk memuat dan menjalankannya sendiri, kita bisa lakukan hal berikut:
<!DOCTYPE html>
<script>
(async function () {
const imports = {
env: {
memory: new WebAssembly.Memory({ initial: 1 }),
STACKTOP: 0,
},
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/a.out.wasm'),
imports,
);
console.log(instance.exports._fib(12));
})();
</script>
Modul WebAssembly yang dibuat oleh Emscripten tidak memiliki memori untuk berfungsi
kecuali jika Anda
menyediakan memori. Cara Anda menyediakan modul Wasm
apa pun adalah dengan menggunakan objek imports
— parameter kedua
Fungsi instantiateStreaming
. Modul Wasm dapat mengakses
semua yang ada di dalam
objek impor, tetapi
tidak ada hal lain di luarnya. Berdasarkan konvensi, modul
dikompilasi oleh Emscripting, terdapat
beberapa hal dari proses pemuatan JavaScript
lingkungan:
- Pertama, ada
env.memory
. Modul Wasm tidak mengetahui kondisi luar sehingga perlu untuk mendapatkan beberapa memori untuk digunakan. MasukWebAssembly.Memory
Ini merepresentasikan bagian memori linear (yang dapat di-tumbuhkan secara opsional). Ukuran parameter ada di "dalam unit halaman WebAssembly", artinya kode di atas mengalokasikan 1 halaman memori, dengan setiap halaman memiliki ukuran 64 KiB. Tanpa menyediakanmaximum
, memori secara teoritis tidak terikat dalam pertumbuhan (saat ini Chrome memiliki dengan batas maksimum 2 GB). Sebagian besar modul WebAssembly tidak perlu mengatur maksimum. env.STACKTOP
menentukan tempat stack seharusnya mulai berkembang. Stack diperlukan untuk melakukan panggilan fungsi dan mengalokasikan memori untuk variabel lokal. Karena kita tidak melakukan kesalahan manajemen memori dinamis di lingkungan di program Fibonacci, kita bisa menggunakan seluruh memori sebagai tumpukan, sehinggaSTACKTOP = 0
.