Mengompilasi mkbitmap ke WebAssembly

Di Apa itu WebAssembly dan dari mana asalnya?, Saya menjelaskan bagaimana kita mendapatkan WebAssembly saat ini. Dalam artikel ini, saya akan menunjukkan pendekatan saya dalam mengompilasi program C yang ada, mkbitmap, ke WebAssembly. Contoh ini lebih kompleks daripada contoh hello world, karena mencakup cara bekerja dengan file, berkomunikasi antara WebAssembly dan JavaScript, serta menggambar ke kanvas, tetapi masih cukup mudah untuk dikelola.

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, tidak mendapatkan aplikasi atau library untuk dikompilasi pada proses pertama sepenuhnya normal, itulah sebabnya beberapa langkah yang dijelaskan di bawah akhirnya tidak berfungsi, jadi saya harus kembali dan mencoba lagi dengan cara yang berbeda. Artikel ini tidak menampilkan perintah kompilasi akhir yang ajaib seolah-olah muncul dari langit, tetapi menjelaskan progres saya yang sebenarnya, termasuk beberapa frustrasi.

Tentang mkbitmap

Program C mkbitmap membaca gambar dan menerapkan satu atau beberapa operasi berikut ke gambar tersebut, dalam urutan ini: inversi, pemfilteran highpass, penskalaan, dan penentuan nilai minimum. Setiap operasi dapat dikontrol dan diaktifkan atau dinonaktifkan secara terpisah. Penggunaan utama mkbitmap adalah untuk mengonversi gambar berwarna atau skala abu-abu menjadi format yang sesuai sebagai input untuk program lain, terutama program penelusuran potrace yang menjadi 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 mengetahui semua detailnya, lihat halaman manual alat:

$ mkbitmap [options] [filename...]
Gambar kartun berwarna.
Gambar asli (Sumber).
Gambar kartun dikonversi menjadi skala abu-abu setelah praproses.
Pertama-tama diskalakan, lalu diberi nilai minimum: 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.

Kompilasi dan instal secara lokal

Langkah berikutnya adalah mengompilasi dan menginstal alat secara lokal untuk memahami cara kerjanya. File INSTALL berisi petunjuk berikut:

  1. 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, aplikasi ini akan mencetak beberapa pesan yang memberi tahu fitur yang sedang diperiksa.

  2. Ketik make untuk mengompilasi paket.

  3. Secara opsional, ketik make check untuk menjalankan pengujian mandiri apa pun yang disertakan dalam paket, umumnya menggunakan biner yang baru saja dibuat dan belum diinstal.

  4. Ketik make install untuk menginstal program dan file data serta dokumentasi. Saat menginstal ke dalam awalan yang dimiliki oleh root, sebaiknya paket dikonfigurasi dan dibangun sebagai pengguna biasa, dan hanya fase make install yang dijalankan 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 apakah fungsi ini berfungsi dengan benar dengan menjalankan mkbitmap --version. Berikut adalah output dari keempat langkah di komputer saya, yang dipangkas secara signifikan agar lebih ringkas:

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 Anda mendapatkan detail versi, Anda telah berhasil mengompilasi dan menginstal mkbitmap. Selanjutnya, buat langkah-langkah yang setara ini berfungsi dengan WebAssembly.

Mengompilasi mkbitmap ke WebAssembly

Emscripten adalah alat untuk mengompilasi program C/C++ ke WebAssembly. Dokumentasi Building Projects Emscripten menyatakan hal berikut:

Membangun project besar dengan Emscripten sangatlah mudah. Emscripten menyediakan dua skrip sederhana yang mengonfigurasi makefile Anda untuk menggunakan emcc sebagai pengganti langsung gcc—dalam sebagian besar kasus, sistem build project Anda yang lain tidak berubah.

Kemudian, dokumentasi berlanjut (sedikit diedit agar lebih ringkas):

Pertimbangkan kasus saat Anda biasanya membangun dengan perintah berikut:

./configure
make

Untuk membangun dengan Emscripten, Anda akan menggunakan 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 lancar, sekarang akan ada file .wasm di suatu tempat di 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 ke direktori src/. Sekarang ada juga dua file baru yang sesuai, mkbitmap dan potrace. Untuk artikel ini, hanya mkbitmap yang relevan. Fakta bahwa file tersebut tidak memiliki 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 jika Anda mau). Sekarang saatnya melakukan pengujian pertama untuk melihat apakah fungsi tersebut berhasil dengan menjalankan file menggunakan Node.js di command line dengan menjalankan node mkbitmap.js --version:

$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.

Anda telah berhasil mengompilasi mkbitmap ke WebAssembly. Sekarang langkah selanjutnya 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 menyalurkan direktori mkbitmap dan membukanya di browser Anda. Anda akan melihat perintah yang meminta Anda untuk memasukkan input. Hal ini sudah diperkirakan, karena, menurut halaman manual alat ini, "[j]ika tidak ada argumen nama file yang diberikan, mkbitmap akan bertindak sebagai filter, membaca dari input standar", yang secara default adalah prompt() untuk Emscripten.

Aplikasi mkbitmap menampilkan perintah yang meminta input.

Mencegah eksekusi otomatis

Untuk menghentikan mkbitmap agar tidak langsung dieksekusi dan membuatnya menunggu input pengguna, Anda harus 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 memberikan implementasi Module untuk mengontrol eksekusi kode. Saat aplikasi Emscripten dimulai, aplikasi akan melihat nilai pada objek Module dan menerapkannya.

Dalam kasus mkbitmap, tetapkan Module.noInitialRun ke true untuk mencegah menjalankan awal yang menyebabkan perintah muncul. Buat skrip bernama script.js, sertakan sebelum <script src="mkbitmap.js"></script> di index.html, lalu tambahkan kode berikut ke script.js. Saat Anda memuat ulang aplikasi, perintah tersebut akan hilang.

var Module = {
  // Don't run main() at page load
  noInitialRun: true,
};

Buat build modular dengan beberapa tanda build lainnya

Untuk memberikan input ke aplikasi, Anda dapat menggunakan dukungan sistem file Emscripten di Module.FS. Bagian Termasuk Dukungan Sistem File dalam dokumentasi menyatakan:

Emscripten memutuskan apakah akan menyertakan dukungan sistem file secara otomatis. Banyak program tidak memerlukan file, dan dukungan sistem file tidak dapat diabaikan ukurannya, sehingga Emscripten menghindari penyertaan dukungan sistem file jika 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. 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, jadi Anda harus secara eksplisit memberitahunya untuk melakukannya. Artinya, Anda harus mengikuti langkah-langkah emconfigure dan emmake yang dijelaskan sebelumnya, dengan beberapa tanda lagi yang ditetapkan melalui argumen CFLAGS. Flag berikut juga dapat berguna untuk project lain.

Selain itu, dalam kasus khusus ini, Anda perlu menyetel tanda --host ke wasm32 untuk memberi tahu skrip configure bahwa Anda sedang mengompilasi untuk WebAssembly.

Perintah emconfigure akhir akan 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 sehingga hanya memuat modul ES script.js, yang kemudian Anda impor modul mkbitmap.js darinya.

<!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 di browser sekarang, Anda akan melihat objek Module yang dicatat ke konsol DevTools, dan perintah akan hilang, karena fungsi main() dari mkbitmap tidak lagi dipanggil di awal.

Aplikasi mkbitmap dengan layar putih, yang menampilkan objek Modul yang dicatat ke konsol DevTools.

Jalankan 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 apa yang akan Anda teruskan di command line. Jika di command line Anda menjalankan mkbitmap -v, Anda akan memanggil Module.callMain(['-v']) di browser. Tindakan ini 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();

Aplikasi mkbitmap dengan layar putih, yang menampilkan nomor versi mkbitmap yang dicatat ke konsol DevTools.

Mengalihkan output standar

Output standar (stdout) secara default adalah konsol. Namun, Anda dapat mengalihkannya ke sesuatu yang lain, misalnya, fungsi yang menyimpan output ke variabel. Artinya, Anda dapat menambahkan output ke HTML dengan menetapkan 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();

Aplikasi mkbitmap yang menampilkan nomor versi mkbitmap.

Mendapatkan file input ke dalam sistem file memori

Untuk memasukkan file input ke sistem file memori, Anda memerlukan perintah yang setara dengan mkbitmap filename di command line. Untuk memahami cara saya melakukannya, pertama-tama, ketahui latar belakang tentang cara mkbitmap mengharapkan input dan membuat output-nya.

Format input mkbitmap yang didukung adalah PNM (PBM, PGM, PPM) dan BMP. Format outputnya 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 sufiksnya menjadi .pbm. Misalnya, untuk nama file input example.bmp, nama file outputnya 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 (biasanya disebut 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 memverifikasi bahwa operasi penulisan 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 karena Module.callMain() adalah fungsi yang umumnya hanya diharapkan untuk 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();

Aplikasi mkbitmap menampilkan serangkaian file dalam sistem file memori, termasuk example.bmp.

Eksekusi sebenarnya pertama

Setelah semuanya siap, jalankan mkbitmap dengan menjalankan Module.callMain(['example.bmp']). Catat konten 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();

Aplikasi mkbitmap menampilkan array file dalam sistem file memori, termasuk example.bmp dan example.pbm.

Mendapatkan file output dari sistem file memori

Fungsi readFile() objek FS memungkinkan pengambilan example.pbm yang dibuat pada langkah terakhir dari sistem file memori. Fungsi ini menampilkan Uint8Array yang Anda konversi menjadi objek File dan simpan ke disk, karena browser umumnya tidak mendukung file PBM untuk dilihat langsung di browser. (Ada cara yang lebih elegan untuk menyimpan file, tetapi menggunakan <a download> yang dibuat secara dinamis adalah cara yang paling banyak didukung.) Setelah file disimpan, Anda dapat membukanya di penampil gambar favorit Anda.

// 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();

macOS Finder dengan pratinjau file .bmp input dan file .pbm output.

Menambahkan UI interaktif

Hingga saat ini, file input di-hardcode dan mkbitmap berjalan dengan parameter default. Langkah terakhir adalah membiarkan 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 diuraikan, jadi dengan beberapa kode JavaScript, Anda bahkan dapat menampilkan pratinjau gambar output. Lihat kode sumber demo sematan di bawah untuk mengetahui salah satu cara melakukannya.

Kesimpulan

Selamat, Anda telah berhasil mengompilasi mkbitmap ke WebAssembly dan membuatnya berfungsi di browser. Ada beberapa kebuntuan dan Anda harus mengompilasi alat lebih dari sekali hingga berfungsi, tetapi seperti yang saya tulis di atas, hal itu adalah bagian dari pengalaman. Jangan lupa juga tag webassembly StackOverflow jika Anda mengalami kesulitan. Selamat mengompilasi!

Ucapan terima kasih

Artikel ini ditinjau oleh Sam Clegg dan Rachel Andrew.