Bagaimana cara mengintegrasikan WebAssembly ke dalam pengaturan ini? Dalam artikel ini, kita akan menggunakan C/C++ dan Emscripten sebagai contoh.
WebAssembly (wasm) sering kali dibingkai sebagai primitif kinerja atau cara untuk menjalankan C++ yang ada codebase di web. Dengan squoosh.app, kami ingin menunjukkan setidaknya ada perspektif ketiga bagi wasm: memanfaatkan data ekosistem bahasa pemrograman lain. Dengan Emscripten, Anda dapat menggunakan kode C/C++, Rust memiliki dukungan wasm bawaan, dan fitur Go tim kami juga sedang mengerjakannya. Saya adalah yakinlah banyak bahasa lain akan menyusul.
Dalam skenario ini, wasm bukanlah pusat aplikasi Anda, melainkan teka-teki bagian: modul lain. Aplikasi Anda sudah memiliki JavaScript, CSS, aset gambar, sistem build yang berfokus pada web dan bahkan mungkin framework seperti React. Bagaimana Anda mengintegrasikan WebAssembly ke dalam pengaturan ini? Dalam artikel ini, kita akan mengerjakan dengan C/C++ dan Emscripten sebagai contoh.
Docker
Menurut saya, Docker sangat bermanfaat saat bekerja dengan Emscripten. C/C++ perpustakaan sering ditulis untuk bekerja dengan sistem operasi yang mereka bangun. Memiliki lingkungan yang konsisten akan sangat membantu. Dengan Docker, Anda mendapatkan sistem Linux yang tervirtualisasi yang sudah diatur untuk bekerja dengan Emscripten dan memiliki semua alat dan dependensi yang terinstal. Jika ada yang kurang, Anda bisa menginstalnya tanpa harus khawatir tentang bagaimana hal itu mempengaruhi komputer Anda atau project lainnya. Jika ada yang salah, buang wadah dan mulailah berakhir. Jika pernah berhasil, Anda bisa yakin bahwa aplikasi itu akan terus berfungsi dan memberikan hasil yang identik.
Docker Registry memiliki Emscripten gambar oleh trzeci yang telah saya gunakan secara ekstensif.
Integrasi dengan npm
Dalam kebanyakan kasus, titik masuk ke proyek web adalah npm dari
package.json
. Berdasarkan konvensi, sebagian besar project dapat dibuat dengan npm install &&
npm run build
.
Secara umum, artefak build yang dihasilkan oleh Emscripten (.js
dan .wasm
) harus diperlakukan hanya sebagai modul JavaScript lain dan hanya
aset. File JavaScript dapat ditangani oleh pemaket seperti webpack atau {i>rollup<i},
dan file wasm harus diperlakukan seperti
aset biner lainnya yang lebih besar, seperti
gambar.
Dengan demikian, artefak build Emscripten harus dibangun sebelum proses build 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 sebagai
yang disebutkan sebelumnya, sebaiknya gunakan
Docker untuk memastikan lingkungan build
konsisten.
docker run ... trzeci/emscripten ./build.sh
memberi tahu Docker untuk menjalankan
container menggunakan image trzeci/emscripten
dan menjalankan perintah ./build.sh
.
build.sh
adalah skrip shell yang akan Anda tulis berikutnya. --rm
memberi tahu
Docker untuk menghapus container setelah selesai berjalan. Dengan cara ini, Anda tidak membangun
sekumpulan gambar mesin yang
usang dari waktu ke waktu. -v $(pwd):/src
berarti bahwa
Anda ingin Docker "mencerminkan" direktori saat ini ($(pwd)
) ke /src
di dalam
container-nya. Setiap perubahan yang Anda buat pada file dalam direktori /src
di dalam
akan dicerminkan ke
project Anda yang sebenarnya. Direktori yang dicerminkan ini
disebut “{i>bind mounts<i}”.
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 dalam mode "gagal cepat" mode. Jika ada perintah dalam skrip
mengembalikan pesan {i>error<i}, maka seluruh skrip
akan segera dibatalkan. Dapat berupa
sangat membantu karena {i>output<i}
terakhir dari skrip akan selalu berhasil
atau error yang menyebabkan build gagal.
Dengan pernyataan export
, Anda menentukan nilai beberapa lingkungan
variabel. Keduanya memungkinkan Anda meneruskan parameter command line tambahan ke C
compiler (CFLAGS
), compiler C++ (CXXFLAGS
), dan penaut (LDFLAGS
).
Mereka semua menerima setelan pengoptimal melalui OPTIMIZE
untuk memastikan bahwa
semuanya dioptimalkan dengan
cara yang sama. Ada beberapa kemungkinan nilai
untuk variabel OPTIMIZE
:
-O0
: Jangan lakukan pengoptimalan apa pun. Tidak ada kode mati yang dihilangkan, dan Emscripten tidak memperkecil kode JavaScript yang dihasilkannya. Bagus untuk proses debug.-O3
: Mengoptimalkan performa secara agresif.-Os
: Mengoptimalkan performa dan ukuran secara agresif sebagai elemen sekunder kriteria.-Oz
: Mengoptimalkan ukuran secara agresif, dengan mengorbankan performa jika perlu.
Untuk web, saya biasanya merekomendasikan -Os
.
Perintah emcc
memiliki berbagai opsinya sendiri. Perhatikan bahwa ECC adalah
seharusnya menjadi "pengganti langsung untuk kompiler seperti GCC atau clang". Jadi, semua
yang mungkin Anda ketahui dari GCC kemungkinan besar
akan diimplementasikan oleh emcc sebagai
ya. Flag -s
bersifat khusus karena memungkinkan kita mengonfigurasi Emscripten
secara spesifik. Semua opsi yang tersedia dapat ditemukan di
settings.js
,
tetapi file tersebut bisa
sangat melelahkan. Berikut adalah daftar tanda Emscripten
yang menurut saya paling penting
bagi pengembang web:
--bind
mengaktifkan embind.-s STRICT=1
menghentikan dukungan untuk semua opsi build yang tidak digunakan lagi. Hal ini memastikan kode Anda dibuat dengan cara yang kompatibel dengan versi baru.-s ALLOW_MEMORY_GROWTH=1
memungkinkan memori tumbuh secara otomatis jika diperlukan. Pada saat penulisan, Emscripten akan mengalokasikan memori sebesar 16 MB pada awalnya. Saat kode Anda mengalokasikan potongan memori, opsi ini memutuskan apakah operasi ini akan membuat seluruh modul wasm gagal saat memori habis, atau jika kode lem diperbolehkan untuk memperluas total memori hingga mengakomodasi alokasi tersebut.-s MALLOC=...
memilih implementasimalloc()
yang akan digunakan.emmalloc
sama dengan implementasimalloc()
yang kecil dan cepat khusus untuk Emscripten. Tujuan alternatifnya adalahdlmalloc
, implementasimalloc()
yang lengkap. Hanya Anda perlu beralih kedlmalloc
jika Anda mengalokasikan banyak objek kecil sering atau jika Anda ingin menggunakan threading.-s EXPORT_ES6=1
akan mengubah kode JavaScript menjadi modul ES6 dengan ekspor default yang dapat digunakan dengan pemaket apa pun. Juga memerlukan-s MODULARIZE=1
untuk ditetapkan.
Tanda berikut tidak selalu diperlukan atau hanya membantu untuk proses debug tujuan:
-s FILESYSTEM=0
adalah flag yang berhubungan dengan Emscripten dan kemampuannya untuk mengemulasi sistem file ketika kode C/C++ menggunakan operasi sistem file. Ia melakukan beberapa analisis pada kode yang dikompilasi untuk memutuskan apakah akan menyertakan emulasi sistem file dalam kode {i>glue<i} atau tidak. Namun terkadang, analisis bisa salah dan Anda membayar 70 kB untuk lem tambahan kode 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 memunculkan file peta sumber untuk modul wasm. Anda dapat membaca lebih lanjut di proses debug dengan Emscripten dalam proses debug bagian.
Dan, berhasil! Untuk menguji penyiapan ini, mari kita siapkan 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 gist yang berisi semua file.)
Untuk membangun semuanya, jalankan
$ npm install
$ npm run build
$ npm run serve
Menavigasi ke {i>localhost:8080 <i}akan menampilkan {i>output<i} berikut ini Konsol DevTools:
Menambahkan kode C/C++ sebagai dependensi
Jika ingin membangun library C/C++ untuk aplikasi web, Anda memerlukan kodenya
dari proyek Anda. Anda dapat menambahkan kode ke repositori project secara manual
atau Anda juga dapat menggunakan npm untuk mengelola dependensi semacam ini. Katakanlah saya
ingin menggunakan libvpx di aplikasi web saya. 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 bisa
menginstalnya menggunakan npm secara langsung.
Untuk keluar dari
teka-teki ini, ada
napa. napa memungkinkan Anda
menginstal semua git
URL repositori sebagai dependensi ke folder node_modules
Anda.
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 GitHub libvpx
ke node_modules
Anda 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, ada wrapper
perintah 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 (secara tradisional .h
atau
file .hpp
) yang menentukan struktur data, class, konstanta, dll. yang
mengekspos library dan library sebenarnya (biasanya file .so
atau .a
). Kepada
menggunakan konstanta VPX_CODEC_ABI_VERSION
library dalam kode Anda, Anda harus
untuk 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, compiler tidak tahu di mana harus mencari vpxenc.h
.
Inilah fungsi flag -I
. Ini memberi tahu compiler
direktori mana yang harus
periksa file {i>header<i}. Selain itu, Anda juga perlu
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 tersebut membangun .js
baru
dan file .wasm
baru dan bahwa halaman demo benar-benar akan menghasilkan konstanta:
Perhatikan juga bahwa proses build memerlukan waktu yang lama. Alasan
waktu build yang lama dapat bervariasi. Dalam kasus libvpx, butuh waktu lama karena
mengkompilasi encoder dan decoder untuk VP8 dan VP9 setiap kali Anda menjalankan
perintah {i>build<i} Anda, meskipun file sumbernya tidak berubah. Bahkan tugas kecil
proses build perubahan ke my-module.cpp
akan memerlukan waktu yang lama. Akan sangat
mempertahankan artefak build dari libvpx setelah
pertama kali dibuat.
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 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 dibangun. Jika dependensi ini
tidak ada di lingkungan build yang disediakan
oleh image Docker, Anda harus
menambahkannya sendiri. Sebagai contoh, katakanlah Anda juga ingin membangun
dokumentasi libvpx menggunakan doxygen. Doksigen tidak
yang tersedia di dalam container Docker, tetapi Anda dapat menginstalnya menggunakan apt
.
Jika Anda melakukannya di build.sh
, Anda perlu mendownload ulang dan menginstal ulang
{i>doxygen<i} setiap kali Anda ingin
membangun perpustakaan Anda. Tidak hanya akan menjadi
pemborosan, tetapi juga akan menghentikan Anda dari mengerjakan proyek Anda saat {i>offline<i}.
Jadi, masuk akal untuk membangun image Docker Anda sendiri. Image Docker dibangun oleh
menulis Dockerfile
yang menjelaskan langkah-langkah build. Dockerfile cukup
canggih dan memiliki banyak
perintah, tetapi sebagian besar
Anda bisa menghemat waktu hanya dengan 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 awalan
poin. Saya memilih trzeci/emscripten
sebagai dasar — gambar 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 kontainer sekarang menjadi bagian dari
image Docker. Untuk memastikan bahwa image Docker Anda telah dibangun dan
tersedia sebelum menjalankan build.sh
, Anda harus menyesuaikan package.json
bit:
{
// ...
"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 gist yang berisi semua file.)
Ini akan membangun image Docker Anda, tetapi hanya jika image tersebut belum dibangun. Selanjutnya
semuanya berjalan seperti sebelumnya, tetapi sekarang lingkungan build memiliki doxygen
yang akan menyebabkan
dokumentasi {i>libvpx <i}dibuat sebagai
ya.
Kesimpulan
Tidak mengherankan jika kode C/C++ dan npm tidak cocok, tetapi Anda bisa membuatnya bekerja dengan cukup nyaman dengan beberapa alat tambahan dan isolasi yang disediakan Docker. Pengaturan ini tidak akan berfungsi untuk setiap proyek, namun titik awal yang layak yang dapat disesuaikan dengan kebutuhan Anda. Jika Anda memiliki peningkatan, silakan bagikan.
Lampiran: Penggunaan lapisan image Docker
Solusi alternatifnya adalah dengan mengenkapsulasi lebih banyak masalah ini dengan Docker dan Pendekatan cerdas Docker untuk caching. Docker mengeksekusi Dockerfile langkah demi langkah dan menetapkan hasil dari setiap langkah sebuah gambarnya sendiri. Gambar perantara ini sering disebut "lapisan". Jika perintah di Dockerfile belum berubah, Docker tidak akan menjalankan kembali langkah itu ketika Anda membangun ulang Dockerfile. Sebagai gantinya ia menggunakan kembali lapisan sejak terakhir kali gambar dibuat.
Sebelumnya, Anda harus berusaha keras untuk tidak membangun kembali libvpx setiap kali
Anda membangun aplikasi. Sebagai gantinya, Anda dapat memindahkan
instruksi pembuatan untuk {i>libvpx<i}
dari build.sh
ke Dockerfile
untuk memanfaatkan caching Docker
mekanisme:
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 gist yang berisi semua file.)
Perhatikan bahwa Anda perlu menginstal git dan meng-clone libvpx secara manual karena Anda tidak memiliki
bind mount saat menjalankan docker build
. Sebagai efek samping, tidak perlu
{i>napa<i} lagi.