mkbitmap'i WebAssembly'ye derleme

WebAssembly nedir ve nereden çıktı? başlıklı makalede, Günümüzdeki WebAssembly'nin nasıl ortaya çıktığını açıkladım. Bu makalede, mevcut bir C programını (mkbitmap) WebAssembly'ye derleme yaklaşımımı göstereceğim. Dosyalarla çalışma, WebAssembly ve JavaScript arasında iletişim kurma ve tuval çizme gibi işlemler içerdiğinden hello world örneğinden daha karmaşıktır ancak sizi bunaltmayacak kadar yönetilebilir.

Bu makale, WebAssembly'yi öğrenmek isteyen web geliştiriciler için yazılmıştır ve mkbitmap gibi bir şeyi WebAssembly'ye derlemek isterseniz adım adım nasıl ilerleyebileceğinizi gösterir. İlk çalıştırmada bir uygulamanın veya kitaplığın derlenmemesi tamamen normaldir. Bu nedenle, aşağıda açıklanan adımların bazıları çalışmadı ve geri dönüp farklı bir şekilde tekrar denemem gerekti. Makalede, nihai derleme komutu gökten düşmüş gibi gösterilmiyor. Bunun yerine, bazı hayal kırıklıkları da dahil olmak üzere gerçek ilerleme durumum açıklanıyor.

mkbitmap hakkında

mkbitmap C programı bir resmi okur ve sırasıyla şu işlemlerden birini veya daha fazlasını uygular: ters çevirme, yüksek geçiren filtreleme, ölçeklendirme ve eşikleme. Her işlem ayrı ayrı kontrol edilebilir ve etkinleştirilip devre dışı bırakılabilir. mkbitmap'nın temel kullanım amacı, renkli veya gri tonlamalı görüntüleri diğer programlar için giriş olarak uygun bir biçime, özellikle de SVGcode'un temelini oluşturan izleme programı potrace için uygun bir biçime dönüştürmektir. Bir ön işleme aracı olarak mkbitmap, özellikle karikatürler veya el yazısı metinler gibi taranmış çizimleri yüksek çözünürlüklü iki seviyeli görüntülere dönüştürmek için kullanışlıdır.

mkbitmap komutunu kullanmak için bu komuta bir dizi seçenek ve bir veya daha fazla dosya adı iletmeniz gerekir. Tüm ayrıntılar için aracın man sayfasına bakın:

$ mkbitmap [options] [filename...]
Renkli çizgi film resmi.
Orijinal resim (Kaynak).
Çizgi film resmi, ön işleme sonrasında gri tonlamaya dönüştürüldü.
Önce ölçeklendirilir, ardından eşik uygulanır: mkbitmap -f 2 -s 2 -t 0.48 (Kaynak).

Kodu alın

İlk adım, mkbitmap kaynak kodunu almaktır. Bu bilgiyi projenin web sitesinde bulabilirsiniz. Bu yazı yazıldığı sırada en son sürüm potrace-1.16.tar.gz'dir.

Yerel olarak derleme ve yükleme

Bir sonraki adım, nasıl davrandığını anlamak için aracı yerel olarak derleyip yüklemektir. INSTALL dosyası aşağıdaki talimatları içerir:

  1. cd komutunu kullanarak paketin kaynak kodunu içeren dizine gidin ve ./configure komutunu yazarak paketi sisteminiz için yapılandırın.

    configure komutunun çalıştırılması 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, pakette bulunan tüm kendi kendine testleri çalıştırmak için make check yazın. Bu testler genellikle yeni oluşturulmuş, yüklenmemiş ikili dosyaları kullanır.

  4. Programları, veri dosyalarını ve belgeleri yüklemek için make install yazın. Kök kullanıcının sahip olduğu bir öneke yükleme yaparken paketin normal bir kullanıcı olarak yapılandırılması ve oluşturulması, yalnızca make install aşamasının kök kullanıcı 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 elde edersiniz. Bu makalenin odak noktası mkbitmap dosyasıdır. mkbitmap --version komutunu çalıştırarak doğru çalıştığını doğrulayabilirsiniz. Kısa olması için önemli ölçüde kırpılmış olan, makinemdeki dört adımın çıktısı aşağıda verilmiştir:

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'.

Çalışıp çalışmadığı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'yı başarıyla derleyip yüklemişsinizdir. Ardından, bu adımların WebAssembly ile çalışmasını sağlayın.

mkbitmap dilini WebAssembly'ye derleme

Emscripten, C/C++ programlarını WebAssembly'ye derlemek için kullanılan bir araçtır. Emscripten'in Building Projects (Projeleri Oluşturma) dokümanında şunlar belirtilmektedir:

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

Belgelerde daha sonra (kısa olması için biraz düzenlenerek) şu bilgiler verilir:

Normalde aşağıdaki komutlarla derleme yaptığınızı düşünün:

./configure
make

Emscripten ile derleme yapmak için bunun yerine aşağıdaki komutları kullanırsınız:

emconfigure ./configure
emmake make

Yani ./configure, emconfigure ./configure'ye, make ise emmake make'e dönüşür. Aşağıda, bu işlemin mkbitmap ile nasıl yapılacağı gösterilmektedir.

Adım 0, 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 yolunda gittiyse dizinde artık .wasm dosya olmalıdır. Bu dosyaları find . -name "*.wasm" komutunu çalıştırarak bulabilirsiniz:

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

Son iki dosya umut verici görünüyor. Bu nedenle, cd dizinine src/. Ayrıca, mkbitmap ve potrace olmak üzere iki yeni ilgili dosya da eklendi. Bu makale için yalnızca mkbitmap geçerlidir. .js uzantısının olmaması biraz kafa karıştırıcı olsa da bunlar aslında JavaScript dosyalarıdır ve hızlı 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 işlevini çağırarak JavaScript dosyasını mkbitmap.js olarak yeniden adlandırın (isterseniz mv potrace potrace.js işlevini de çağırabilirsiniz). Şimdi, node mkbitmap.js --version komutunu çalıştırarak dosyayı komut satırında Node.js ile yürütüp çalışıp çalışmadığını görmek için ilk testi yapma zamanı:

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

mkbitmap, WebAssembly'ye başarıyla derlendi. Şimdi bir sonraki adım, uygulamanın tarayıcıda çalışmasını sağlamaktır.

mkbitmap Tarayıcıda WebAssembly ile

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 standart dosya 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 dizinine hizmet veren 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 kılavuz sayfasına göre "dosya adı bağımsız değişkeni verilmezse mkbitmap, standart girişten okuyarak filtre görevi görür". Emscripten'de standart giriş varsayılan olarak prompt() olduğundan bu durum beklenir.

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

Otomatik yürütmeyi önleme

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

mkbitmap durumunda, istemin görünmesine neden olan ilk çalıştırmayı önlemek için Module.noInitialRun değerini true olarak ayarlayın. script.js adlı bir komut dosyası oluşturun, index.html içindeki <script src="mkbitmap.js"></script>'den önce ekleyin ve script.js'ya aşağıdaki kodu ekleyin. Uygulamayı yeniden yüklediğinizde istem artık görünmez.

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

Birkaç derleme işareti daha içeren modüler bir derleme oluşturma

Uygulamaya giriş sağlamak için Module.FS içinde Emscripten'in dosya sistemi desteğini kullanabilirsiniz. Belgelerin Dosya Sistemi Desteği Dahil bölümünde şunlar belirtilir:

Emscripten, dosya sistemi desteğinin otomatik olarak dahil edilip edilmeyeceğine karar verir. Birçok programın dosyalara ihtiyacı yoktur ve dosya sistemi desteği boyut olarak önemsiz değildir. Bu nedenle Emscripten, bir neden görmediğinde bunu dahil etmekten kaçınır. Bu, C/C++ kodunuz dosyalara erişmiyorsa FS nesnesinin ve diğer dosya sistemi API'lerinin çıkışa dahil edilmeyeceği anlamına gelir. Diğer yandan, C/C++ kodunuzda dosya kullanılıyorsa dosya sistemi desteği otomatik olarak eklenir.

Maalesef mkbitmap, Emscripten'in dosya sistemi desteğini otomatik olarak dahil etmediği durumlardan biridir. Bu nedenle, bunu yapmasını açıkça belirtmeniz gerekir. Bu, daha önce açıklanan emconfigure ve emmake adımlarını, CFLAGS bağımsız değişkeni aracılığıyla ayarlanan birkaç ek işaretle birlikte uygulamanız gerektiği anlamına gelir. Aşağıdaki işaretler diğer projelerde de işe yarayabilir.

Ayrıca, bu özel durumda, configure komut dosyasına WebAssembly için derleme yaptığınızı bildirmek üzere --host işaretini wasm32 olarak ayarlamanız gerekir.

Son emconfigure komutu şu şekilde 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 komutunu tekrar çalıştırmayı ve yeni oluşturulan dosyaları mkbitmap klasörüne kopyalamayı unutmayın.

index.html dosyasını yalnızca ES modülü script.js'nü yükleyecek şekilde değiştirin. Ardından, mkbitmap.js modülünü bu dosyadan 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();

Uygulamayı artık tarayıcıda açtığınızda Module nesnesinin DevTools konsoluna kaydedildiğini görmeniz gerekir. mkbitmap işlevinin main() işlevi artık başlangıçta çağrılmadığından istem kaybolur.

Geliştirici Araçları konsoluna kaydedilen Module nesnesini gösteren, beyaz ekranlı mkbitmap uygulaması.

Ana işlevi manuel olarak yürütme

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

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

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

run();

mkbitmap sürüm numarasının DevTools konsoluna kaydedildiğini gösteren beyaz ekranlı mkbitmap uygulaması.

Standart çıkışı yönlendirme

Varsayılan olarak standart çıkış (stdout) konsoldur. Ancak bunu başka bir şeye yönlendirebilirsiniz. Örneğin, çıkışı bir değişkende depolayan bir işleve. 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 aktarma

Giriş dosyasını bellek dosya sistemine almak için komut satırında mkbitmap filename komutuna eşdeğer bir komut kullanmanız gerekir. Bu konuya nasıl yaklaştığımı anlamak için öncelikle mkbitmap'ın girişini nasıl beklediği ve çıkışını nasıl oluşturduğu hakkında biraz 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, gri eşlemler için ise PGM'dir. filename bağımsız değişkeni verilirse mkbitmap, varsayılan olarak son eki .pbm olarak değiştirerek giriş dosyasının adından elde edilen bir çıkış dosyası oluşturur. Örneğin, example.bmp giriş dosyasının adı example.pbm olur.

Emscripten, yerel dosya sistemini simüle eden bir sanal dosya sistemi sağlar. Böylece, senkron dosya API'lerini kullanan yerel kod, çok az değişiklikle veya hiç değişiklik yapılmadan derlenip çalıştırılabilir. mkbitmap'nın bir giriş dosyasını filename komut satırı bağımsız değişkeni olarak iletilmiş 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 dosyaları sanal dosya sistemine yazmak için kullandığınız bir writeFile() işlevine sahiptir. Aşağıdaki kod örneğinde gösterildiği gibi writeFile() kullanırsınız.

Dosya yazma işleminin çalıştığını doğrulamak için FS nesnesinin readdir() işlevini '/' parametresiyle çalıştırın. example.bmp 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'])'a 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ı gereken 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();

mkbitmap uygulamasında, example.bmp dahil olmak üzere bellek dosya sistemindeki bir dizi dosya gösteriliyor.

İlk gerçek yürütme

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

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

mkbitmap uygulamasında, bellek dosya sistemindeki bir dizi dosya (ör. example.bmp ve example.pbm) gösteriliyor.

Çıkış dosyasını bellek dosya sisteminden çıkarma

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 PBM dosyalarını doğrudan tarayıcıda görüntülemeyi desteklemediğinden işlev, Uint8Array döndürür. Bu dosyayı File nesnesine dönüştürüp diske kaydedersiniz. (Dosya kaydetmenin daha zarif yolları vardır ancak dinamik olarak oluşturulan <a download> en yaygın olarak desteklenen yöntemdir.) Dosya kaydedildikten sonra, en sevdiğiniz resim görüntüleyicide 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ı sabit kodlanmıştır ve mkbitmap, varsayılan parametrelerle çalışır. Son adımda, kullanıcının giriş dosyasını dinamik olarak seçmesine, mkbitmap parametrelerini ayarlamasına ve ardından aracı seçilen seçeneklerle çalıştırmasına izin verilir.

// 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, biraz JavaScript koduyla çıkış resminin önizlemesini bile gösterebilirsiniz. Bunu yapmanın bir yolu için aşağıdaki yerleştirilmiş demoyu kaynak kodu ile inceleyin.

Sonuç

Tebrikler! mkbitmap öğesini WebAssembly'ye başarıyla derlediniz ve tarayıcıda çalışmasını sağladınız. Bazı çıkmaz sokaklar vardı ve çalışana kadar aracı birden fazla kez derlemeniz gerekti. Ancak yukarıda da yazdığım gibi bu, deneyimin bir parçası. Takılırsanız StackOverflow'un webassembly etiketini de kullanabilirsiniz. İyi derlemeler!

Teşekkür

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