mkbitmap'i WebAssembly'ye derleme

WebAssembly nedir ve nereden geldi? bölümünde, Bugünün WebAssembly'sini nasıl ortaya koyduğumuzu açıkladım. Bu makalede, WebAssembly'de mevcut bir C programını (mkbitmap) derleme yaklaşımımı göstereceğim. Bu, hello world örneğinden daha karmaşıktır. Dosyalarla çalışmayı, WebAssembly ile JavaScript alanları arasında iletişim kurmayı ve bir tuvalde çizim yapmayı kapsar ancak yine de sizi bunaltmayacak kadar yönetilebilirdir.

Bu makale, WebAssembly hakkında bilgi edinmek isteyen web geliştiricileri için yazılmış olup mkbitmap gibi bir şeyi WebAssembly için derlemek istediğinizde ne yapmanız gerektiğini adım adım gösterir. Adil bir uyarı olarak, ilk çalıştırmada bir uygulamanın veya kitaplığın derlenmemesi tamamen normaldir. Bu nedenle, aşağıda açıklanan adımlardan bazıları çalışmadı. Dolayısıyla geri dönüp farklı bir şekilde denemem gerekti. Makalede, sihirli son derleme komutu sanki gökyüzünden düşmüş gibi gösterilmiyor, bunun yerine gerçek ilerleme durumum açıklanıyor, bazı hayal kırıklıkları da var.

Yaklaşık mkbitmap

mkbitmap C programı bir görüntüyü okur ve resme şu işlemlerden birini veya daha fazlasını şu sırayla uygular: ters çevirme, üst geçiş filtreleme, ölçeklendirme ve eşik. Her işlem ayrı ayrı kontrol edilebilir ve açılabilir veya kapatılabilir. mkbitmap ürününün temel kullanımı, renkli veya gri tonlamalı resimleri diğer programlara, özellikle de SVGcode temelini oluşturan izleme programı potrace için girdi olarak uygun bir biçime dönüştürmektir. Bir ön işleme aracı olarak mkbitmap özellikle çizgi filmler veya el yazısı metinler gibi taranmış çizgi resimleri yüksek çözünürlüklü iki düzeyli resimlere dönüştürmek için kullanışlıdır.

mkbitmap öğesine çeşitli seçenekler ve bir veya daha fazla dosya adı ileterek kullanırsınız. Tüm ayrıntılar için aracın Kılavuz sayfasına bakın:

$ mkbitmap [options] [filename...]
Renkli çizgi film resim.
Orijinal resim (Kaynak).
Çizgi film resmi, ön işleme işleminden sonra gri tonlamaya dönüştürüldü.
İlk olarak ölçeklendirilir, daha sonra eşiğe ayarlanır: mkbitmap -f 2 -s 2 -t 0.48 (Kaynak).

Kodu alın

İlk adım, mkbitmap kaynak kodunu almaktır. Sertifikayı projenin web sitesinde bulabilirsiniz. Bu yazının yazıldığı tarihte potrace-1.16.tar.gz en son sürümdür.

Yerel olarak derleyip yükleyin

Bir sonraki adım, nasıl davrandığı hakkında fikir edinmek için aracı yerel olarak derlemek ve yüklemektir. INSTALL dosyası aşağıdaki talimatları içerir:

  1. cd öğesini ekleyerek paketin kaynak kodunu içeren dizine ekleyin ve paketi sisteminizde yapılandırmak için ./configure yazın.

    configure özelliğinin çalışması biraz zaman alabilir. Çalışırken hangi özellikleri kontrol ettiğini belirten bazı mesajlar yazdırır.

  2. Paketi derlemek için make yazın.

  3. İsteğe bağlı olarak, genellikle yeni oluşturulmuş ikili programları kullanarak paketle gelen kendi kendine testleri çalıştırmak için make check yazın.

  4. Programları ve varsa veri dosyalarını ve belgeleri yüklemek için make install yazın. Kök tarafından sahip olunan bir öneke yüklerken, paketin normal bir kullanıcı olarak yapılandırılıp derlenmesi ve yalnızca make install aşamasının kök ayrıcalıklarıyla yürütülmesi önerilir.

Bu adımları uyguladığınızda, potrace ve mkbitmap olmak üzere iki yürütülebilir dosya ortaya çıkar. Bu ikinci dosya bu makalenin odak noktasıdır. mkbitmap --version komutunu çalıştırarak düzgün şekilde çalıştığını doğrulayabilirsiniz. Makinemden elde edilen dört adımın tümünün çıktısı aşağıdaki gibidir, ancak kısa olması için yoğun bir şekilde kırpılmıştır:

1. Adım, ./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

2. Adım, 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'.

3. Adım, 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'.

4. Adım, 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'.

İşe yarayıp yaramadığını kontrol etmek için mkbitmap --version komutunu çalıştırın:

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

Sürüm ayrıntılarını alırsanız mkbitmap dosyasını başarıyla derleyip yüklemişsiniz demektir. Ardından, bu adımların eşdeğerinin WebAssembly ile çalışmasını sağlayın.

mkbitmap öğesini WebAssembly'de derleyin

Emscripten, C/C++ programlarını WebAssembly'de derlemeye yarayan bir araçtır. Emscripten'in Building Projects (Projeler) belgelerinde şu bilgiler yer alır:

Emscripten ile büyük projeler oluşturmak çok kolaydır. Emscripten, oluşturma dosyalarınızı gcc yerine emcc eklentisini kullanacak şekilde yapılandıran iki basit komut dosyası sağlar. Çoğu durumda, projenizin mevcut derleme sisteminin geri kalanı değiştirilmeden kalır.

Dokümanlar devam ediyor (kısa bir düzenleme için):

Normalde aşağıdaki komutları kullanarak derleme yaptığınız senaryoyu düşünün:

./configure
make

Emscripten ile derleme yapmak için şu komutları kullanırsınız:

emconfigure ./configure
emmake make

Dolayısıyla, ./configure emconfigure ./configure, make ise emmake make olur. Aşağıda, bu işlemi mkbitmap ile nasıl yapabileceğiniz gösterilmektedir.

0. Adım, make clean:

$ make clean
Making clean in src
 rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo

1. Adım, 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

2. Adım, 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'.

Her şey yolundaysa artık dizinde bir yerde .wasm dosyası bulunuyor olmalıdır. Bu öğeleri find . -name "*.wasm" komutunu çalıştırarak bulabilirsiniz:

$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm

Son ikisi de umut verici görünüyor. Bu nedenle, src/ dizinine cd ekleyin. Ayrıca mkbitmap ve potrace adlı iki yeni dosya da mevcuttur. Bu makale için yalnızca mkbitmap alakalıdır. .js uzantısına sahip olmamaları biraz kafa karıştırıcı olsa da aslında bu JavaScript dosyalarıdır ve kısa bir head çağrısıyla doğrulanabilir:

$ 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)

mv mkbitmap mkbitmap.js (ve isterseniz mv potrace potrace.js) çağrısı yaparak JavaScript dosyasını mkbitmap.js olarak yeniden adlandırın. Şimdi sıra, komut satırında node mkbitmap.js --version komutunu çalıştırarak dosyayı Node.js ile çalıştırarak çalışıp çalışmadığını görmek için ilk teste geldi:

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

mkbitmap dosyasını WebAssembly'de başarıyla derlediniz. Şimdi sıradaki adım, dosyanın tarayıcıda çalışmasını sağlamaktır.

Tarayıcıda WebAssembly ile mkbitmap

mkbitmap.js ve mkbitmap.wasm dosyalarını mkbitmap adlı yeni bir dizine kopyalayın ve mkbitmap.js JavaScript dosyasını yükleyen bir index.html HTML ortak dosyası oluşturun.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>mkbitmap</title>
  </head>
  <body>
    <script src="mkbitmap.js"></script>
  </body>
</html>

mkbitmap dizinini sunan bir yerel sunucu başlatın ve tarayıcınızda açın. Giriş yapmanızı isteyen bir istem görürsünüz. Aracın man sayfasına göre, "[i]f dosya adı bağımsız değişkeni belirtilmemişse mkbitmap standart girişten okuyor" (emscripten için varsayılan olarak prompt()) olduğundan beklenen bir durumdur.

Giriş isteyen bir istem gösteren mkbitmap uygulaması.

Otomatik yürütmeyi engelle

mkbitmap öğesinin hemen çalışmasını durdurmak ve bunun yerine kullanıcı girişini beklemek için Emscripten'in Module nesnesini anlamanız gerekir. Module, Emscripten tarafından oluşturulan kodun yürütme sırasında çeşitli noktalarda çağırdığı özelliklere sahip genel bir JavaScript nesnesidir. Kodun yürütülmesini kontrol etmek için bir Module uygulaması sağlayabilirsiniz. Bir Emscripten uygulaması başlatıldığında, Module nesnesindeki değerlere bakar ve bunları uygular.

mkbitmap söz konusu olduğunda, istemin görünmesine neden olan ilk çalıştırmayı önlemek için Module.noInitialRun değerini true olarak ayarlayın. script.js adında bir komut dosyası oluşturun, bunu index.html içinde <script src="mkbitmap.js"></script> öğesinin öncesine ekleyin ve aşağıdaki kodu script.js öğesine ekleyin. Şimdi uygulamayı yeniden yüklediğinizde istem kaybolacaktır.

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

Biraz daha derleme bayrağı içeren modüler bir yapı oluşturun

Uygulamaya giriş sağlamak için Module.FS uygulamasında Emscripten'in dosya sistemi desteğini kullanabilirsiniz. Belgenin Include File System Support (Dosya Sistemi Desteği Dahil Olma) bölümünde şunlar belirtiliyor:

Emscripten, dosya sistemi desteğini otomatik olarak dahil edip etmemeye karar verir. Birçok program dosyalara ihtiyaç duymaz ve dosya sistemi desteği de göz ardı edilebilir bir boyut değildir. Bu nedenle, Emscripten neden bulamadığında dosyayı yüklemekten kaçınır. Yani C/C++ kodunuz dosyalara erişmezse FS nesnesi ve diğer dosya sistemi API'leri çıkışa dahil edilmez. Diğer yandan, C/C++ kodunuzda dosya kullanılıyorsa dosya sistemi desteği de otomatik olarak dahil edilir.

Maalesef mkbitmap, Emscripten'in dosya sistemi desteğini otomatik olarak eklemediği durumlardan biridir. Bu nedenle, bunu yapmasını açıkça belirtmeniz gerekir. Bu, daha önce açıklanan emconfigure ve emmake adımlarını uygulamanız ve CFLAGS bağımsız değişkeni aracılığıyla birkaç işaret daha ayarlamanız gerektiği anlamına gelir. Aşağıdaki işaretler diğer projeler için de yararlı olabilir.

Ayrıca bu örnekte, configure komut dosyasına WebAssembly için derlediğinizi bildirmek için --host işaretini wasm32 olarak ayarlamanız gerekir.

Son emconfigure komutu şöyle görünür:

$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'

emmake make öğesini tekrar çalıştırmayı ve yeni oluşturulan dosyaları mkbitmap klasörüne kopyalamayı unutmayın.

index.html dosyasını, yalnızca script.js ES modülünü yükleyecek şekilde değiştirin. Ardından, bu modülden mkbitmap.js modülünü içe aktarın.

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

Şimdi uygulamayı tarayıcıda açtığınızda, Module nesnesinin Geliştirici Araçları konsoluna kaydedildiğini görürsünüz. İstem kaybolur çünkü mkbitmap main() işlevi artık başlangıçta çağrılmaz.

Beyaz ekranlı mkbitmap uygulaması, Geliştirici Araçları konsoluna kaydedilen Modül nesnesini gösteriyor.

Ana işlevi manuel olarak yürütme

Sonraki adım, Module.callMain() komutunu çalıştırarak mkbitmap öğesinin main() işlevini manuel olarak çağırmaktır. callMain() işlevi, komut satırında ileteceğiniz dizelerle tek tek eşleşen bir bağımsız değişken dizisi alır. Komut satırında mkbitmap -v komutunu çalıştırırsanız tarayıcıda Module.callMain(['-v']) yöntemini çağırırsınız. Bu işlem mkbitmap sürüm numarasını Geliştirici Araçları konsoluna kaydeder.

// This is `script.js`.
import loadWASM from './mkbitmap.js';

const run = async () => {
  const Module = await loadWASM();
  Module.callMain(['-v']);
};

run();

Beyaz ekranlı mkbitmap uygulaması, Geliştirici Araçları konsoluna kaydedilen mkbitmap sürüm numarasını gösteriyor.

Standart çıkışı yönlendir

Varsayılan olarak standart çıkış (stdout) konsoldur. Ancak bunu başka bir şeye, örneğin çıkışı bir değişkene depolayan işleve yönlendirebilirsiniz. Bu, Module.print özelliğini ayarlayarak çıkışı HTML'ye ekleyebileceğiniz anlamına gelir.

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

mkbitmap sürüm numarasını gösteren mkbitmap uygulaması.

Giriş dosyasını bellek dosya sistemine al

Giriş dosyasını bellek dosya sistemine almak için komut satırında mkbitmap filename eşdeğerinin olması gerekir. Bu konuya nasıl yaklaştığımı anlamak için önce mkbitmap ürününün girdisini nasıl beklediği ve çıktısını nasıl oluşturduğuyla ilgili bilgi verelim.

mkbitmap için desteklenen giriş biçimleri PNM (PBM, PGM, PPM) ve BMP'dir. Çıkış biçimleri, bit eşlemler için PBM ve gri haritalar için PGM'dir. Bir filename bağımsız değişkeni verilirse mkbitmap varsayılan olarak, son eki .pbm olarak değiştirilerek giriş dosyasının adından alınan bir çıkış dosyası oluşturur. Örneğin, example.bmp giriş dosyası adı için çıkış dosyasının adı example.pbm olur.

Emscripten, yerel dosya sistemini simüle eden sanal bir dosya sistemi sağlar. Böylece, eşzamanlı dosya API'lerini kullanan yerel kod çok az değişiklikle veya hiç değişiklik yapılmadan derlenebilir ve çalışabilir. mkbitmap ürününün bir giriş dosyasını filename komut satırı bağımsız değişkeni olarak aktarılmış gibi okuması için Emscripten'in sağladığı FS nesnesini kullanmanız gerekir.

FS nesnesi, bellek içi bir dosya sistemi (genellikle MEMFS olarak adlandırılır) tarafından desteklenir ve sanal dosya sistemine dosya yazmak için kullandığınız bir writeFile() işlevine sahiptir. writeFile() özelliğini aşağıdaki kod örneğinde gösterildiği gibi kullanıyorsunuz.

Dosya yazma işleminin çalıştığını doğrulamak için FS nesnesinin readdir() işlevini '/' parametresiyle çalıştırın. example.bmp dosyasını ve her zaman otomatik olarak oluşturulan bir dizi varsayılan dosya görürsünüz.

Sürüm numarasını yazdırmak için Module.callMain(['-v']) için yapılan önceki çağrının kaldırıldığını unutmayın. Bunun nedeni, Module.callMain() işlevinin genellikle yalnızca bir kez çalıştırılmasını bekleyen bir işlev olmasıdır.

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

example.bmp dahil olmak üzere bellek dosya sistemindeki bir dosya dizisini gösteren mkbitmap uygulaması.

İlk gerçek yürütme

Her şey hazır olduğunda Module.callMain(['example.bmp']) komutunu çalıştırarak mkbitmap yürütün. MEMFS'nin '/' klasörünün içeriğini günlüğe kaydettiğinizde yeni oluşturulan example.pbm çıkış dosyasını example.bmp giriş dosyasının yanında göreceksiniz.

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

example.bmp ve example.pbm de dahil olmak üzere bellek dosya sistemindeki bir dosya dizisini gösteren mkbitmap uygulaması.

Çıkış dosyasını bellek dosya sisteminden alma

FS nesnesinin readFile() işlevi, son adımda oluşturulan example.pbm öğesinin bellek dosya sisteminden alınmasını sağlar. Tarayıcılar genellikle doğrudan tarayıcı içinde görüntüleme için PBM dosyalarını desteklemediğinden işlev, File nesnesine dönüştürdüğünüz ve diske kaydettiğiniz bir Uint8Array değerini döndürür. (Dosya kaydetmenin daha zarif yolları vardır, ancak dinamik olarak oluşturulmuş bir <a download> kullanmak en yaygın olarak desteklenen yöntemdir.) Kaydedilen dosyayı favori resim görüntüleyicinizde açabilirsiniz.

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

Giriş .bmp dosyası ve çıkış .pbm dosyasının önizlemesini içeren macOS Finder.

Etkileşimli kullanıcı arayüzü ekleme

Bu noktaya kadar, giriş dosyasına kod gömülür ve mkbitmap, varsayılan parametrelerle çalışır. Son adım, kullanıcının dinamik olarak bir giriş dosyası seçmesine, mkbitmap parametrelerini değiştirmesine ve ardından belirlenen seçeneklerle aracı çalıştırmasına izin vermektir.

// 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']);

PBM resim biçiminin ayrıştırılması özellikle zor değildir. Bu nedenle, bazı JavaScript kodlarıyla çıktı resminin bir önizlemesini bile gösterebilirsiniz. Bunu yapmanın bir yolunu öğrenmek için aşağıdaki yerleştirilmiş demonun kaynak koduna göz atın.

Sonuç

Tebrikler, mkbitmap öğesini WebAssembly’de başarıyla derlediniz ve tarayıcıda çalışmasını sağladınız. Çıkmaz sokaklar vardı ve işe yarayana kadar aracı bir kereden fazla derlemeniz gerekti. Ancak yukarıda yazdığım gibi, bu, işin bir parçası. Takılırsanız StackOverflow'un webassembly etiketini de unutmayın. İyi derlemeler!

Teşekkür

Bu makale Sam Clegg ve Rachel Andrew tarafından incelenmiştir.