Bagaimana cara mengintegrasikan WebAssembly ke dalam pengaturan ini? Dalam artikel ini, kita akan membahasnya dengan C/C++ dan Emscripten sebagai contoh.
WebAssembly (wasm) sering kali digambarkan sebagai primitif performa atau cara untuk menjalankan codebase C++ yang ada di web. Dengan squoosh.app, kami ingin menunjukkan bahwa setidaknya ada perspektif ketiga untuk wasm: memanfaatkan ekosistem besar dari bahasa pemrograman lain. Dengan Emscripten, Anda dapat menggunakan kode C/C++, Rust memiliki dukungan wasm bawaan, dan tim Go juga sedang mengerjakannya. Saya yakin banyak bahasa lainnya akan mengikuti.
Dalam skenario ini, wasm bukan inti aplikasi Anda, melainkan bagian teka-teki: modul lain. Aplikasi Anda sudah memiliki JavaScript, CSS, aset gambar, sistem build yang berfokus pada web, dan bahkan mungkin framework seperti React. Bagaimana cara mengintegrasikan WebAssembly ke dalam penyiapan ini? Dalam artikel ini, kita akan membahasnya dengan C/C++ dan Emscripten sebagai contoh.
Docker
Menurut saya, Docker sangat bermanfaat saat bekerja dengan Emscripten. Library C/C++ sering ditulis agar berfungsi dengan sistem operasi tempat library tersebut dibuat. Memiliki lingkungan yang konsisten akan sangat membantu. Dengan Docker, Anda akan mendapatkan sistem Linux tervirtualisasi yang sudah disiapkan agar berfungsi dengan Emscripten serta telah menginstal semua alat dan dependensi. Jika ada yang tidak ada, Anda cukup menginstalnya tanpa harus khawatir tentang pengaruhnya terhadap mesin Anda sendiri atau project Anda yang lain. Jika terjadi kesalahan, buang penampung dan mulai ulang. Jika berhasil sekali, Anda dapat yakin bahwa kode tersebut akan terus berfungsi dan menghasilkan hasil yang identik.
Docker Registry memiliki image Emscripten dari trzeci yang telah saya gunakan secara ekstensif.
Integrasi dengan npm
Dalam sebagian besar kasus, titik entri ke project web adalah package.json
npm. Berdasarkan konvensi, sebagian besar project dapat dibuat dengan npm install &&
npm run build
.
Secara umum, artefak build yang dihasilkan oleh Emscripten (file .js
dan .wasm
)
harus diperlakukan hanya sebagai modul JavaScript lain dan aset
lain. File JavaScript dapat ditangani oleh pemaket seperti webpack atau penggabungan,
dan file wasm harus diperlakukan seperti aset biner yang lebih besar lainnya, seperti
gambar.
Dengan demikian, artefak build Emscripten harus dibangun sebelum proses build "normal" dimulai:
{
"name": "my-worldchanging-project",
"scripts": {
"build:emscripten": "docker run --rm -v $(pwd):/src trzeci/emscripten
./build.sh",
"build:app": "<the old build command>",
"build": "npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
Tugas build:emscripten
baru dapat memanggil Emscripten secara langsung, tetapi seperti
yang disebutkan sebelumnya, sebaiknya gunakan Docker untuk memastikan lingkungan build
konsisten.
docker run ... trzeci/emscripten ./build.sh
memberi tahu Docker untuk menjalankan container baru menggunakan image trzeci/emscripten
dan menjalankan perintah ./build.sh
.
build.sh
adalah skrip shell yang akan Anda tulis selanjutnya. --rm
memberi tahu Docker untuk menghapus penampung setelah selesai berjalan. Dengan cara ini, Anda tidak akan membuat
kumpulan image mesin yang sudah tidak berlaku dari waktu ke waktu. -v $(pwd):/src
berarti
Anda ingin Docker "mencerminkan" direktori saat ini ($(pwd)
) ke /src
di dalam
penampung. Setiap perubahan yang Anda buat pada file di direktori /src
di dalam
penampung akan dicerminkan ke project sebenarnya. Direktori yang dicerminkan ini
disebut "bind mount".
Mari kita lihat build.sh
:
#!/bin/bash
set -e
export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CXXFLAGS="${OPTIMIZE}"
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
src/my-module.cpp
# Create output folder
mkdir -p dist
# Move artifacts
mv my-module.{js,wasm} dist
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="
Ada banyak hal yang bisa dibedah di sini!
set -e
menempatkan shell ke mode "fail fast". Jika ada perintah dalam skrip yang menampilkan error, seluruh skrip akan langsung dibatalkan. Hal ini dapat
sangat membantu karena output terakhir skrip akan selalu berupa pesan
berhasil atau error yang menyebabkan build gagal.
Dengan pernyataan export
, Anda menentukan nilai beberapa variabel lingkungan. Fungsi ini memungkinkan Anda meneruskan parameter command line tambahan ke compiler C (CFLAGS
), compiler C++ (CXXFLAGS
), dan penaut (LDFLAGS
).
Semuanya menerima setelan pengoptimal melalui OPTIMIZE
untuk memastikan semuanya dioptimalkan dengan cara yang sama. Ada beberapa kemungkinan nilai
untuk variabel OPTIMIZE
:
-O0
: Jangan melakukan pengoptimalan apa pun. Tidak ada kode mati yang dihapus, dan Emscripten juga tidak meminifikasi kode JavaScript yang dihasilkannya. Bagus untuk proses debug.-O3
: Mengoptimalkan performa secara agresif.-Os
: Mengoptimalkan performa dan ukuran secara agresif sebagai kriteria sekunder.-Oz
: Mengoptimalkan ukuran secara agresif, dengan mengorbankan performa jika perlu.
Untuk web, saya merekomendasikan -Os
.
Perintah emcc
memiliki banyak opsi sendiri. Perlu diperhatikan bahwa emcc dianggap sebagai "pengganti langsung untuk compiler seperti GCC atau clang". Jadi, semua
flag yang mungkin Anda ketahui dari GCC kemungkinan besar juga akan diterapkan oleh emcc. Flag -s
bersifat khusus karena memungkinkan kita mengonfigurasi Emscripten
secara khusus. Semua opsi yang tersedia dapat ditemukan di
settings.js
Emscripten,
tetapi file tersebut bisa sangat melelahkan. Berikut adalah daftar tanda Emscripten
yang menurut saya paling penting bagi developer web:
--bind
mengaktifkan embind.-s STRICT=1
menghentikan dukungan untuk semua opsi build yang tidak digunakan lagi. Hal ini memastikan kode Anda dibangun dengan cara yang kompatibel dengan versi baru.-s ALLOW_MEMORY_GROWTH=1
memungkinkan memori dikembangkan secara otomatis jika diperlukan. Pada saat penulisan, Emscripten akan mengalokasikan memori sebesar 16 MB pada awalnya. Saat kode Anda mengalokasikan potongan memori, opsi ini akan menentukan apakah operasi ini akan membuat seluruh modul wasm gagal saat memori habis, atau apakah kode lem diizinkan untuk memperluas total memori untuk menampung alokasi.-s MALLOC=...
memilih penerapanmalloc()
yang akan digunakan.emmalloc
adalah implementasimalloc()
yang kecil dan cepat khusus untuk Emscripten. Alternatifnya adalahdlmalloc
, yaitu implementasimalloc()
yang lengkap. Anda hanya perlu beralih kedlmalloc
jika sering mengalokasikan banyak objek kecil atau jika ingin menggunakan threading.-s EXPORT_ES6=1
akan mengubah kode JavaScript menjadi modul ES6 dengan ekspor default yang berfungsi dengan bundler apa pun.-s MODULARIZE=1
juga harus ditetapkan.
Flag berikut tidak selalu diperlukan atau hanya berguna untuk tujuan proses debug:
-s FILESYSTEM=0
adalah flag yang terkait dengan Emscripten dan kemampuannya untuk mengemulasikan sistem file untuk Anda saat kode C/C++ menggunakan operasi sistem file. Alat ini melakukan beberapa analisis pada kode yang dikompilasi untuk memutuskan apakah akan menyertakan emulasi sistem file dalam kode glue atau tidak. Namun, terkadang analisis ini bisa salah dan Anda membayar kode glue tambahan sebesar 70 kB untuk emulasi sistem file yang mungkin tidak Anda perlukan. Dengan-s FILESYSTEM=0
, Anda dapat memaksa Emscripten untuk tidak menyertakan kode ini.-g4
akan membuat Emscripten menyertakan informasi proses debug di.wasm
dan juga menghasilkan file peta sumber untuk modul wasm. Anda dapat membaca lebih lanjut tentang proses debug dengan Emscripten di bagian debug.
Dan, berhasil! Untuk menguji penyiapan ini, mari kita buat my-module.cpp
kecil:
#include <emscripten/bind.h>
using namespace emscripten;
int say_hello() {
printf("Hello from your wasm module\n");
return 0;
}
EMSCRIPTEN_BINDINGS(my_module) {
function("sayHello", &say_hello);
}
Dan index.html
:
<!doctype html>
<title>Emscripten + npm example</title>
Open the console to see the output from the wasm module.
<script type="module">
import wasmModule from "./my-module.js";
const instance = wasmModule({
onRuntimeInitialized() {
instance.sayHello();
}
});
</script>
(Berikut adalah ringkasan yang berisi semua file.)
Untuk mem-build semuanya, jalankan
$ npm install
$ npm run build
$ npm run serve
Membuka localhost:8080 akan menampilkan output berikut di konsol DevTools:
Menambahkan kode C/C++ sebagai dependensi
Jika ingin mem-build library C/C++ untuk aplikasi web, Anda memerlukan kodenya untuk
menjadi bagian dari project. Anda dapat menambahkan kode ke repositori project secara manual atau menggunakan npm untuk mengelola dependensi semacam ini. Misalnya, saya
ingin menggunakan libvpx di webapp. libvpx
adalah library C++ untuk mengenkode gambar dengan VP8, codec yang digunakan dalam file .webm
.
Namun, libvpx tidak ada di npm dan tidak memiliki package.json
, jadi saya tidak dapat menginstalnya menggunakan npm secara langsung.
Untuk menyelesaikan masalah ini, ada
napa. napa memungkinkan Anda menginstal URL repositori
git sebagai dependensi ke folder node_modules
.
Instal napa sebagai dependensi:
$ npm install --save napa
dan pastikan untuk menjalankan napa
sebagai skrip penginstalan:
{
// ...
"scripts": {
"install": "napa",
// ...
},
"napa": {
"libvpx": "git+https://github.com/webmproject/libvpx"
}
// ...
}
Saat Anda menjalankan npm install
, napa akan menangani cloning repositori GitHub libvpx ke dalam node_modules
dengan nama libvpx
.
Sekarang Anda dapat memperluas skrip build untuk membangun libvpx. libvpx menggunakan configure
dan make
untuk dibangun. Untungnya, Emscripten dapat membantu memastikan bahwa configure
dan
make
menggunakan compiler Emscripten. Untuk tujuan ini, terdapat perintah wrapper emconfigure
dan emmake
:
# ... above is unchanged ...
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
(
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
# ... below is unchanged ...
Library C/C++ dibagi menjadi dua bagian: header (file .h
atau
.hpp
secara tradisional) yang menentukan struktur data, class, konstanta, dll. yang
ditampilkan oleh library dan library aktual (secara tradisional file .so
atau .a
). Untuk
menggunakan konstanta VPX_CODEC_ABI_VERSION
library dalam kode, Anda harus
menyertakan file header library menggunakan pernyataan #include
:
#include "vpxenc.h"
#include <emscripten/bind.h>
int say_hello() {
printf("Hello from your wasm module with libvpx %d\n", VPX_CODEC_ABI_VERSION);
return 0;
}
Masalahnya adalah compiler tidak tahu di mana harus mencari vpxenc.h
.
Inilah kegunaan flag -I
. Alat ini memberi tahu compiler direktori mana
yang harus diperiksa file header. Selain itu, Anda juga harus memberi compiler file library yang sebenarnya:
# ... above is unchanged ...
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s ASSERTIONS=0 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
-I ./node_modules/libvpx \
src/my-module.cpp \
build-vpx/libvpx.a
# ... below is unchanged ...
Jika menjalankan npm run build
sekarang, Anda akan melihat bahwa proses ini mem-build .js
baru
dan file .wasm
baru, serta halaman demo memang akan menghasilkan konstanta:
Perhatikan juga bahwa proses build memerlukan waktu yang lama. Penyebab
waktu build yang lama dapat bervariasi. Dalam kasus libvpx, prosesnya memerlukan waktu lama karena
mekompilasi encoder dan decoder untuk VP8 dan VP9 setiap kali Anda menjalankan
perintah build, meskipun file sumber belum berubah. Bahkan perubahan
kecil pada my-module.cpp
akan memerlukan waktu lama untuk di-build. Akan sangat
berguna untuk menyimpan artefak build libvpx setelah
di-build untuk pertama kalinya.
Salah satu cara untuk melakukan ini adalah dengan menggunakan variabel lingkungan.
# ... above is unchanged ...
eval $@
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
test -n "$SKIP_LIBVPX" || (
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
# ... below is unchanged ...
(Berikut adalah gist yang berisi semua file.)
Perintah eval
memungkinkan kita menetapkan variabel lingkungan dengan meneruskan parameter ke skrip build. Perintah test
akan melewati pembuatan libvpx jika
$SKIP_LIBVPX
ditetapkan (ke nilai apa pun).
Sekarang Anda dapat mengompilasi modul tetapi melewati membangun ulang libvpx:
$ npm run build:emscripten -- SKIP_LIBVPX=1
Menyesuaikan lingkungan build
Terkadang library bergantung pada alat tambahan untuk di-build. Jika dependensi ini tidak ada di lingkungan build yang disediakan oleh image Docker, Anda perlu menambahkannya sendiri. Sebagai contoh, Anda juga ingin membuat
dokumentasi libvpx menggunakan doxygen. Doxygen tidak
tersedia di dalam penampung Docker, tetapi Anda dapat menginstalnya menggunakan apt
.
Jika Anda melakukannya di build.sh
, Anda harus mendownload ulang dan menginstal ulang
doxygen setiap kali ingin mem-build library. Hal itu tidak hanya akan memboroskan, tetapi juga akan menghentikan Anda mengerjakan project saat offline.
Jadi, masuk akal untuk membangun image Docker Anda sendiri. Image Docker dibuat dengan
menulis Dockerfile
yang menjelaskan langkah-langkah build. Dockerfile cukup andal dan memiliki banyak perintah, tetapi sering kali Anda hanya perlu menggunakan FROM
, RUN
, dan ADD
. Dalam hal ini:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen
Dengan FROM
, Anda dapat mendeklarasikan image Docker yang ingin digunakan sebagai titik
awal. Saya memilih trzeci/emscripten
sebagai dasar — image yang telah Anda gunakan
selama ini. Dengan RUN
, Anda menginstruksikan Docker untuk menjalankan perintah shell di dalam
container. Apa pun perubahan yang dilakukan perintah ini pada penampung kini menjadi bagian dari
image Docker. Untuk memastikan image Docker telah dibangun dan tersedia sebelum menjalankan build.sh
, Anda harus menyesuaikan package.json
sedikit:
{
// ...
"scripts": {
"build:dockerimage": "docker image inspect -f '.' mydockerimage || docker build -t mydockerimage .",
"build:emscripten": "docker run --rm -v $(pwd):/src mydockerimage ./build.sh",
"build": "npm run build:dockerimage && npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
(Berikut adalah gist yang berisi semua file.)
Tindakan ini akan mem-build image Docker Anda, tetapi hanya jika belum di-build. Kemudian,
semuanya berjalan seperti sebelumnya, tetapi sekarang lingkungan build memiliki perintah
doxygen
yang tersedia, yang akan menyebabkan dokumentasi libvpx juga
di-build.
Kesimpulan
Tidak mengherankan jika kode C/C++ dan npm tidak cocok secara alami, tetapi Anda dapat membuatnya berfungsi cukup nyaman dengan beberapa alat tambahan dan isolasi yang disediakan Docker. Penyiapan ini tidak akan berfungsi untuk setiap project, tetapi ini adalah titik awal yang baik yang dapat Anda sesuaikan dengan kebutuhan Anda. Jika Anda memiliki peningkatan, harap bagikan.
Lampiran: Penggunaan lapisan image Docker
Solusi alternatifnya adalah mengenkapsulasi lebih banyak masalah ini dengan Docker dan pendekatan cerdas Docker untuk penyimpanan dalam cache. Docker mengeksekusi Dockerfile langkah demi langkah dan menetapkan hasil dari setiap langkah sebagai image-nya sendiri. Gambar perantara ini sering disebut "lapisan". Jika perintah di Dockerfile tidak berubah, Docker tidak akan dijalankan ulang saat Anda membangun ulang Dockerfile. Sebagai gantinya, lapisan akan digunakan kembali dari terakhir kali image dibuat.
Sebelumnya, Anda harus melakukan beberapa upaya untuk tidak membangun ulang libvpx setiap kali
membuat aplikasi. Sebagai gantinya, Anda dapat memindahkan petunjuk proses build untuk libvpx
dari build.sh
ke Dockerfile
untuk memanfaatkan mekanisme caching
Docker:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen git && \
mkdir -p /opt/libvpx/build && \
git clone https://github.com/webmproject/libvpx /opt/libvpx/src
RUN cd /opt/libvpx/build && \
emconfigure ../src/configure --target=generic-gnu && \
emmake make
(Berikut adalah ringkasan yang berisi semua file.)
Perhatikan bahwa Anda perlu menginstal git dan meng-clone libvpx secara manual karena Anda tidak memiliki
mount bind saat menjalankan docker build
. Sebagai efek samping, napa tidak perlu lagi digunakan.