Di bagian Apa itu WebAssembly dan dari mana asalnya?, saya menjelaskan bagaimana kami bisa sampai
dengan WebAssembly saat ini. Dalam artikel ini, saya akan menunjukkan pendekatan saya dalam mengompilasi program C yang ada, mkbitmap
, ke WebAssembly. Aktivitas ini lebih kompleks daripada contoh hello world, karena mencakup mengerjakan file, berkomunikasi antara webAssembly dan JavaScript, dan menggambar ke kanvas, tetapi masih cukup mudah dikelola sehingga Anda tidak kewalahan.
Artikel ini ditulis untuk developer web yang ingin mempelajari WebAssembly dan menunjukkan langkah demi langkah cara melanjutkan jika Anda ingin mengompilasi sesuatu seperti mkbitmap
ke WebAssembly. Sebagai peringatan yang wajar, tidak membuat aplikasi atau library untuk dikompilasi saat pertama kali dijalankan adalah hal yang normal. Itulah sebabnya beberapa langkah yang dijelaskan di bawah ini akhirnya tidak berfungsi, jadi saya harus mundur dan mencoba lagi dengan cara yang berbeda. Artikel ini tidak menampilkan perintah kompilasi akhir ajaib seolah-olah telah jatuh dari langit, tetapi menggambarkan progres saya yang sebenarnya, termasuk beberapa frustrasi.
Tentang mkbitmap
Program mkbitmap
C membaca gambar dan menerapkan satu atau beberapa operasi berikut ke gambar tersebut, dalam urutan ini: inversi, pemfilteran highpass, penskalaan, dan ambang batas. Setiap operasi dapat dikontrol satu per satu dan diaktifkan atau dinonaktifkan. Penggunaan utama mkbitmap
adalah untuk mengonversi gambar berwarna atau hitam putih menjadi format yang cocok sebagai input untuk program lain, terutama program pelacakan potrace
yang membentuk dasar SVGcode. Sebagai alat pra-pemrosesan, mkbitmap
sangat berguna untuk mengonversi gambar garis yang dipindai, seperti kartun atau teks tulisan tangan, menjadi gambar dua tingkat beresolusi tinggi.
Anda menggunakan mkbitmap
dengan meneruskan sejumlah opsi dan satu atau beberapa nama file. Untuk semua detail, lihat halaman manual alat:
$ mkbitmap [options] [filename...]
![Gambar kartun berwarna.](https://web.dev/static/articles/compiling-mkbitmap-to-webassembly/image/cartoon-image-color.png?authuser=19&hl=id)
![Gambar kartun dikonversi menjadi hitam putih setelah pra-pemrosesan.](https://web.dev/static/articles/compiling-mkbitmap-to-webassembly/image/cartoon-image-converted-97142bc66e12e.png?authuser=19&hl=id)
mkbitmap -f 2 -s 2 -t 0.48
(Sumber).Mendapatkan kode
Langkah pertama adalah mendapatkan kode sumber mkbitmap
. Anda dapat menemukannya di situs project. Pada saat penulisan ini, potrace-1.16.tar.gz adalah versi terbaru.
Mengompilasi dan menginstal secara lokal
Langkah berikutnya adalah mengompilasi dan menginstal alat secara lokal untuk memahami perilakunya. File INSTALL
berisi petunjuk berikut:
cd
ke direktori yang berisi kode sumber paket dan ketik./configure
untuk mengonfigurasi paket bagi sistem Anda.Menjalankan
configure
mungkin memerlukan waktu beberapa saat. Saat berjalan, ia akan mencetak beberapa pesan yang memberi tahu fitur mana yang sedang diperiksa.Ketik
make
untuk mengompilasi paket.Jika ingin, ketik
make check
untuk menjalankan pengujian mandiri yang disertakan dengan paket, umumnya menggunakan biner yang baru saja di-uninstal.Ketik
make install
untuk menginstal program dan semua file data dan dokumentasi. Saat menginstal ke awalan yang dimiliki oleh root, sebaiknya paket dikonfigurasi dan dibangun sebagai pengguna biasa, dan hanya fasemake install
yang dieksekusi dengan hak istimewa root.
Dengan mengikuti langkah-langkah ini, Anda akan mendapatkan dua file yang dapat dieksekusi, potrace
dan mkbitmap
—yang terakhir adalah fokus artikel ini. Anda dapat memverifikasi bahwa kode tersebut berfungsi dengan benar dengan menjalankan mkbitmap --version
. Berikut adalah output dari keempat langkah mesin saya, yang dipangkas secara ketat agar singkat:
Langkah 1, ./configure
:
$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
[…]
config.status: executing libtool commands
Langkah 2, make
:
$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
clang -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all-am'.
Langkah 3, make check
:
$ make check
Making check in src
make[1]: Nothing to be done for `check'.
Making check in doc
make[1]: Nothing to be done for `check'.
[…]
============================================================================
Testsuite summary for potrace 1.16
============================================================================
# TOTAL: 8
# PASS: 8
# SKIP: 0
# XFAIL: 0
# FAIL: 0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Nothing to be done for `check-am'.
Langkah 4, sudo make install
:
$ sudo make install
Password:
Making install in src
.././install-sh -c -d '/usr/local/bin'
/bin/sh ../libtool --mode=install /usr/bin/install -c potrace mkbitmap '/usr/local/bin'
[…]
make[2]: Nothing to be done for `install-data-am'.
Untuk memeriksa apakah berhasil, jalankan mkbitmap --version
:
$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Jika mendapatkan detail versi, berarti Anda telah berhasil mengompilasi dan menginstal mkbitmap
. Selanjutnya, gunakan langkah-langkah ini untuk WebAssembly.
Mengompilasi mkbitmap
ke WebAssembly
Emscripten adalah alat untuk mengompilasi program C/C++ ke WebAssembly. Dokumentasi Building Projects Emscripten menyatakan hal berikut:
Membangun proyek besar dengan Emscripten sangatlah mudah. Emscripten menyediakan dua skrip sederhana yang mengonfigurasi makefile Anda untuk menggunakan
emcc
sebagai pengganti langsung untukgcc
—umumnya sistem build project Anda saat ini tidak berubah.
Dokumentasi kemudian dilanjutkan (sedikit diedit agar lebih singkat):
Pertimbangkan kasus ketika Anda biasanya membangun dengan perintah berikut:
./configure
make
Untuk membuat aplikasi dengan Emscripten, gunakan perintah berikut:
emconfigure ./configure
emmake make
Jadi, pada dasarnya, ./configure
menjadi emconfigure ./configure
dan make
menjadi emmake make
. Berikut cara melakukannya dengan mkbitmap
.
Langkah 0, make clean
:
$ make clean
Making clean in src
rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo
Langkah 1, emconfigure ./configure
:
$ emconfigure ./configure
configure: ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
[…]
config.status: executing libtool commands
Langkah 2, emmake make
:
$ emmake make
make: make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
/opt/homebrew/Cellar/emscripten/3.1.36/libexec/emcc -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all'.
Jika semuanya berjalan baik, sekarang seharusnya ada file .wasm
di suatu tempat dalam direktori. Anda dapat menemukannya dengan menjalankan find . -name "*.wasm"
:
$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm
Dua yang terakhir terlihat menjanjikan, jadi cd
masuk ke direktori src/
. Sekarang juga ada dua file baru yang sesuai, mkbitmap
dan potrace
. Untuk artikel ini, hanya mkbitmap
yang relevan. Faktanya, ekstensi .js
sedikit membingungkan, tetapi sebenarnya file tersebut adalah file JavaScript, yang dapat diverifikasi dengan panggilan head
cepat:
$ cd src/
$ head -n 20 mkbitmap
// include: shell.js
// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module != 'undefined' ? Module : {};
// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)
Ganti nama file JavaScript menjadi mkbitmap.js
dengan memanggil mv mkbitmap mkbitmap.js
(dan mv potrace potrace.js
masing-masing jika Anda mau).
Sekarang saatnya pengujian pertama untuk mengetahui apakah pengujian berhasil dengan mengeksekusi file menggunakan Node.js pada command line dengan menjalankan node mkbitmap.js --version
:
$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
Anda berhasil mengompilasi mkbitmap
ke WebAssembly. Sekarang, langkah berikutnya adalah membuatnya berfungsi di browser.
mkbitmap
dengan WebAssembly di browser
Salin file mkbitmap.js
dan mkbitmap.wasm
ke direktori baru bernama mkbitmap
, lalu buat file boilerplate HTML index.html
yang memuat file JavaScript mkbitmap.js
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<script src="mkbitmap.js"></script>
</body>
</html>
Mulai server lokal yang melayani direktori mkbitmap
dan buka di browser Anda. Anda akan melihat perintah yang meminta Anda untuk memasukkan input. Hal ini sudah diharapkan, karena, sesuai dengan halaman utama alat, "[i]jika tidak ada argumen nama file yang diberikan, maka mkbitmap bertindak sebagai filter, yang membaca dari input standar", yang secara default untuk Emscripten adalah prompt()
.
Mencegah eksekusi otomatis
Agar mkbitmap
tidak langsung dieksekusi dan harus menunggu input pengguna, Anda perlu memahami objek Module
Emscripten. Module
adalah objek JavaScript global dengan atribut yang dipanggil oleh kode yang dihasilkan Emscripten di berbagai titik dalam eksekusinya.
Anda dapat menyediakan implementasi Module
untuk mengontrol eksekusi kode.
Saat dimulai, aplikasi Emscripten melihat nilai pada objek Module
dan menerapkannya.
Untuk mkbitmap
, tetapkan Module.noInitialRun
ke true
untuk mencegah proses awal yang menyebabkan perintah muncul. Buat skrip bernama script.js
, sertakan sebelum <script src="mkbitmap.js"></script>
di index.html
dan tambahkan kode berikut ke script.js
. Saat Anda memuat ulang aplikasi, perintah akan hilang.
var Module = {
// Don't run main() at page load
noInitialRun: true,
};
Membuat build modular dengan beberapa flag build lainnya
Untuk memberikan input ke aplikasi, Anda dapat menggunakan dukungan sistem file Emscripten di Module.FS
. Bagian Include File System Support pada status dokumentasi:
Emscripten memutuskan apakah akan menyertakan dukungan sistem file secara otomatis. Banyak program yang tidak memerlukan file, dan ukuran dukungan sistem file tidak dapat diabaikan, jadi Emscripten menghindari penyertaan file ketika tidak ada alasan untuk melakukannya. Artinya, jika kode C/C++ Anda tidak mengakses file, objek
FS
dan API sistem file lainnya tidak akan disertakan dalam output. Selain itu, di sisi lain, jika kode C/C++ Anda menggunakan file, dukungan sistem file akan disertakan secara otomatis.
Sayangnya, mkbitmap
adalah salah satu kasus di mana Emscripten tidak otomatis menyertakan dukungan sistem file, sehingga Anda harus memintanya secara eksplisit untuk melakukannya. Artinya, Anda harus mengikuti langkah emconfigure
dan emmake
yang dijelaskan sebelumnya, dengan beberapa flag lainnya yang ditetapkan melalui argumen CFLAGS
. Penanda berikut mungkin juga berguna untuk proyek-proyek lain.
- Setel
-sFILESYSTEM=1
agar dukungan sistem file disertakan. - Setel
-sEXPORTED_RUNTIME_METHODS=FS,callMain
agarModule.FS
danModule.callMain
diekspor. - Setel
-sMODULARIZE=1
dan-sEXPORT_ES6
untuk membuat modul ES6 modern. - Setel
-sINVOKE_RUN=0
untuk mencegah proses awal yang menyebabkan perintah muncul.
Selain itu, dalam kasus tertentu ini, Anda perlu menyetel flag --host
ke wasm32
untuk memberi tahu skrip configure
yang Anda kompilasi untuk WebAssembly.
Perintah emconfigure
akhir terlihat seperti ini:
$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'
Jangan lupa untuk menjalankan emmake make
lagi dan menyalin file yang baru dibuat ke folder mkbitmap
.
Ubah index.html
agar hanya memuat modul ES script.js
, tempat Anda kemudian mengimpor modul mkbitmap.js
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<!-- No longer load `mkbitmap.js` here -->
<script src="script.js" type="module"></script>
</body>
</html>
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
console.log(Module);
};
run();
Saat membuka aplikasi sekarang di browser, Anda akan melihat objek Module
dicatat ke dalam konsol DevTools, dan prompt akan hilang, karena fungsi main()
dari mkbitmap
tidak lagi dipanggil di awal.
Mengeksekusi fungsi utama secara manual
Langkah berikutnya adalah memanggil fungsi main()
mkbitmap
secara manual dengan menjalankan Module.callMain()
. Fungsi callMain()
menggunakan array argumen, yang cocok satu per satu dengan yang akan Anda teruskan pada command line. Jika Anda menjalankan mkbitmap -v
di command line, Anda akan memanggil Module.callMain(['-v'])
di browser. Tindakan ini akan mencatat nomor versi mkbitmap
ke konsol DevTools.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
Module.callMain(['-v']);
};
run();
Mengalihkan output standar
Output standar (stdout
) secara default adalah konsol. Namun, Anda dapat mengalihkannya ke hal lain, misalnya, fungsi yang menyimpan output ke variabel. Artinya, Anda dapat menambahkan output ke HTML dengan menyetel properti Module.print
.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
let consoleOutput = 'Powered by ';
const Module = await loadWASM({
print: (text) => (consoleOutput += text),
});
Module.callMain(['-v']);
document.body.textContent = consoleOutput;
};
run();
Memasukkan file input ke dalam sistem file memori
Untuk memasukkan file input ke sistem file memori, Anda memerlukan mkbitmap filename
yang setara pada command line. Untuk memahami cara saya melakukannya, pertama-tama pelajari latar belakang tentang cara mkbitmap
mengharapkan input-nya, lalu membuat output-nya.
Format input mkbitmap
yang didukung adalah PNM (PBM, PGM, PPM) dan BMP. Format {i>output-<i}nya adalah PBM untuk bitmap, dan PGM untuk peta abu-abu. Jika argumen filename
diberikan, mkbitmap
secara default akan membuat file output yang namanya diperoleh dari nama file input dengan mengubah akhirannya menjadi .pbm
. Misalnya, untuk nama file input example.bmp
, nama file output adalah example.pbm
.
Emscripten menyediakan sistem file virtual yang menyimulasikan sistem file lokal, sehingga kode native yang menggunakan API file sinkron dapat dikompilasi dan dijalankan dengan sedikit atau tanpa perubahan.
Agar mkbitmap
dapat membaca file input seolah-olah diteruskan sebagai argumen command line filename
, Anda harus menggunakan objek FS
yang disediakan Emscripten.
Objek FS
didukung oleh sistem file dalam memori (umumnya dikenal sebagai MEMFS) dan memiliki fungsi writeFile()
yang Anda gunakan untuk menulis file ke sistem file virtual. Anda menggunakan writeFile()
seperti yang ditunjukkan dalam contoh kode berikut.
Untuk memastikan bahwa operasi tulis file berhasil, jalankan fungsi readdir()
objek FS
dengan parameter '/'
. Anda akan melihat example.bmp
dan sejumlah file default yang selalu dibuat secara otomatis.
Perhatikan bahwa panggilan sebelumnya ke Module.callMain(['-v'])
untuk mencetak nomor versi telah dihapus. Hal ini disebabkan karena Module.callMain()
adalah fungsi yang umumnya hanya akan dijalankan satu kali.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
console.log(Module.FS.readdir('/'));
};
run();
Eksekusi aktual pertama
Setelah semuanya berada di tempatnya, jalankan mkbitmap
dengan menjalankan Module.callMain(['example.bmp'])
. Catat isi folder '/'
MEMFS, dan Anda akan melihat file output example.pbm
yang baru dibuat di samping file input example.bmp
.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
console.log(Module.FS.readdir('/'));
};
run();
Memperoleh file output dari sistem file memori
Fungsi readFile()
objek FS
memungkinkan pembuatan example.pbm
di langkah terakhir keluar dari sistem file memori. Fungsi ini menampilkan Uint8Array
yang Anda konversi ke objek File
dan simpan ke disk, karena browser umumnya tidak mendukung file PBM untuk tampilan langsung dalam browser.
(Ada cara yang lebih elegan untuk menyimpan file, tetapi menggunakan <a download>
yang dibuat secara dinamis adalah cara yang paling didukung secara luas.) Setelah file disimpan, Anda dapat membukanya di penampil gambar favorit.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
const output = Module.FS.readFile('example.pbm', { encoding: 'binary' });
const file = new File([output], 'example.pbm', {
type: 'image/x-portable-bitmap',
});
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = file.name;
a.click();
};
run();
Menambahkan UI interaktif
Hingga titik ini, file input di-hardcode dan mkbitmap
berjalan dengan parameter default. Langkah terakhir adalah mengizinkan pengguna memilih file input secara dinamis, menyesuaikan parameter mkbitmap
, lalu menjalankan alat dengan opsi yang dipilih.
// Corresponds to `mkbitmap -o output.pbm input.bmp -s 8 -3 -f 4 -t 0.45`.
Module.callMain(['-o', 'output.pbm', 'input.bmp', '-s', '8', '-3', '-f', '4', '-t', '0.45']);
Format gambar PBM tidak terlalu sulit diurai, sehingga dengan beberapa kode JavaScript, Anda bahkan dapat menampilkan pratinjau gambar output. Lihat kode sumber demo tersemat di bawah untuk mengetahui satu cara melakukannya.
Kesimpulan
Selamat, Anda telah berhasil mengompilasi mkbitmap
ke WebAssembly dan membuatnya berfungsi di browser. Ada beberapa jalan buntu dan Anda harus mengompilasi alat tersebut lebih dari sekali sampai berhasil, tetapi seperti yang saya tulis di atas, itulah bagian dari pengalaman. Ingat juga tag webassembly
StackOverflow jika Anda mengalami kesulitan. Selamat membuat kompilasi!
Ucapan terima kasih
Artikel ini diulas oleh Sam Clegg dan Rachel Andrew.