mkbitmap'i WebAssembly'ye derleme

WebAssembly nedir ve nereden geldi? başlıklı makalede, Bugünkü WebAssembly'in nasıl ortaya çıktığını açıkladım. Bu makalede, mevcut bir C programını (mkbitmap) WebAssembly'e derleme yaklaşımımı göstereceğim. Dosyalarla çalışma, WebAssembly ve JavaScript alanları arasında iletişim kurma ve tuvale çizim yapma gibi işlemleri içerdiğinden merhaba dünya örneğinden daha karmaşıktır ancak sizi bunaltmayacak kadar yönetilebilirdir.

WebAssembly'i öğrenmek isteyen web geliştiricileri için yazılan bu makalede, mkbitmap gibi bir kodu WebAssembly'e derlemek isterseniz nasıl ilerleyeceğiniz adım adım gösterilmektedir. Uygulama veya kitaplığın ilk çalıştırmada derlenmemesi tamamen normaldir. Bu nedenle, aşağıda açıklanan adımlardan bazıları işe yaramadı ve geri dönüp farklı bir şekilde tekrar denemem gerekti. Makalede, sihirli son derleme komutu sanki gökten inmiş gibi gösterilmiyor. Bunun yerine, bazı hayal kırıklıkları da dahil olmak üzere gerçek ilerlemem açıklanıyor.

Yaklaşık mkbitmap

mkbitmap C programı bir resim okur ve aşağıdaki işlemlerden birini veya daha fazlasını bu sırayla uygular: ters çevirme, yüksek geçiş filtreleme, ölçeklendirme ve eşik belirleme. Her işlem ayrı ayrı kontrol edilebilir ve etkinleştirilebilir ya da devre dışı bırakılabilir. mkbitmap işlevinin temel amacı, renkli veya gri tonlamalı resimleri diğer programlar için giriş olarak uygun bir biçime dönüştürmektir. Özellikle de SVGcode'un temelini oluşturan potrace izleme programını destekler. mkbitmap, ön işleme aracı olarak özellikle çizgi filmler veya elle yazılmış metinler gibi taranmış çizgi resimleri yüksek çözünürlüklü iki seviyeli resimlere dönüştürmek için kullanışlıdır.

mkbitmap işlevini, bir dizi seçenek ve bir veya daha fazla dosya adı ile kullanırsınız. Tüm ayrıntılar için aracın man sayfasına bakın:

$ mkbitmap [options] [filename...]
Renkli karikatür resmi.
Orijinal resim (Kaynak).
Ön işlemden sonra gri tonlamaya dönüştürülmüş karikatür resmi.
Önce ölçeklendirilir, ardından eşiğe göre ayarlanır: mkbitmap -f 2 -s 2 -t 0.48 (Kaynak).

Kodu alın

İlk adım, mkbitmap kaynağının kodunu elde etmektir. Bu bilgileri projenin web sitesinde bulabilirsiniz. Bu makalenin 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, ardından paketin kaynak kodunu içeren dizine gidin ve ./configure yazın. Böylece paketi sisteminiz için yapılandırabilirsiniz.

    configure'ün ç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, paketle birlikte gelen tüm kendi kendine testleri çalıştırmak için make check yazın. Bu testler genellikle yeni oluşturulan ve yüklenmemiş ikili dosyaları kullanır.

  4. Programları, veri dosyalarını ve dokümanları yüklemek için make install yazın. Kök kullanıcıya ait bir ön ek içine yükleme yaparken 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 adlı iki yürütülebilir dosya elde edersiniz. Bu makalenin odak noktası ikinci dosyadır. mkbitmap --version komutunu çalıştırarak işlemin doğru çalıştığını doğrulayabilirsiniz. Makinemdeki dört adımın da çıktısını kısaltmak için büyük ölçüde kırpılmış şekilde aşağıda bulabilirsiniz:

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

İşlemin iş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ı görüyorsanız mkbitmap başarıyla derlenmiş ve yüklenmiş demektir. Ardından, bu adımların eşdeğerini WebAssembly ile çalıştırın.

mkbitmap'ü WebAssembly olarak derleme

Emscripten, C/C++ programlarını WebAssembly'e derlemek için kullanılan bir araçtır. Emscripten'in Proje Oluşturma dokümanında aşağıdakiler belirtilmektedir:

Emscripten ile büyük projeler oluşturmak çok kolaydır. Emscripten, makefile'lerinizi 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.

Belgenin devamı (özetlemek için biraz düzenlenmiştir):

Normalde aşağıdaki komutlarla derleme yaptığınızı varsayalım:

./configure
make

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

emconfigure ./configure
emmake make

Yani temel olarak ./configure, emconfigure ./configure olur ve make, emmake make olur. Aşağıda, bu işlemin mkbitmap ile nasıl yapılacağı 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 yolunda giderse dizinde .wasm dosya olmalıdır. find . -name "*.wasm" komutunu çalıştırarak bulabilirsiniz:

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

Son iki seçenek umut verici görünüyor. Bu nedenle cd dizininde src/ dosyasını açın. Artık mkbitmap ve potrace adlı iki yeni dosya da mevcuttur. 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'u çağırarak (ve isterseniz sırasıyla mv potrace potrace.js) JavaScript dosyasını mkbitmap.js olarak yeniden adlandırın. Şimdi, node mkbitmap.js --version komutunu çalıştırarak dosyayı komut satırında Node.js ile çalıştırarak işe yarayıp yaramadığını görmek için ilk testi yapma zamanı:

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

mkbitmap dosyasını WebAssembly olarak başarıyla derlediniz. Bir sonraki adım, bu özelliğin 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 şablon 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 yerel bir sunucu başlatın ve tarayıcınızda açın. Sizden giriş yapmanızı isteyen bir istem görürsünüz. Bu, beklenen bir durumdur. Aracı kullanım kılavuzuna göre "[h]erhangi bir dosya adı bağımsız değişkeni verilmezse mkbitmap, standart girişten okuyan bir filtre görevi görür". Emscripten için varsayılan olarak bu prompt() olur.

Giriş isteğinde bulunan bir istemi gösteren mkbitmap uygulaması.

Otomatik yürütmeyi önleme

mkbitmap işlevinin hemen yürütülmesini durdurmak ve 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 bir global JavaScript nesnesi. Kodun yürütülmesini kontrol etmek için Module uygulamasını sağlayabilirsiniz. Emscripten uygulamaları başlatıldığında Module nesnesinde bulunan değerleri inceler ve uygular.

mkbitmap durumunda, istemine 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 dosyasında <script src="mkbitmap.js"></script>'ın önüne ekleyin ve script.js dosyasına aşağıdaki kodu ekleyin. Uygulamayı yeniden yüklediğinizde istem kaldırılır.

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

Daha fazla derleme işaretiyle modüler derleme oluşturma

Uygulamaya giriş sağlamak için Module.FS'de Emscripten'in dosya sistemi desteğini kullanabilirsiniz. Belgelerin Dosya Sistemi Desteği Ekleme bölümünde şu ifadeler yer alır:

Emscripten, dosya sistemi desteğinin eklenip eklenmeyeceğine otomatik olarak karar verir. Birçok programın dosyaya ihtiyacı yoktur ve dosya sistemi desteğinin boyutu göz ardı edilemez. Bu nedenle Emscripten, buna gerek görmediğinde bu desteği eklemez. Yani C/C++ kodunuz dosyalara erişmiyorsa FS nesnesi ve diğer dosya sistemi API'leri çıktıya dahil edilmez. Öte yandan, C/C++ kodunuz dosya kullanıyorsa dosya sistemi desteği otomatik olarak eklenir.

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

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

Nihai 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'ü yalnızca script.js ES modülünü yükleyecek şekilde değiştirin. Ardından mkbitmap.js modülünü bu modülden 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ı tarayıcıda açtığınızda DevTools konsoluna kaydedilen Module nesnesini görürsünüz. mkbitmap işlevinin main() işlevi artık başlangıçta çağrılmadığı için istem de kaldırılır.

DevTools konsoluna kaydedilen Modül nesnesini gösteren, beyaz ekranlı mkbitmap uygulaması.

Ana işlevi manuel olarak yürütme

Sonraki adım, Module.callMain()'yi çalıştırarak mkbitmap'nin main() işlevini manuel olarak çağırmaktır. callMain() işlevi, komut satırında ileteceğinizlerle tek tek eşleşen bir bağımsız değişken dizisi alır. Komut satırında mkbitmap -v çalıştırırsanız tarayıcıda Module.callMain(['-v']) çağrısını yaparsı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();

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

Standart çıkışı yönlendirme

Varsayılan standart çıkış (stdout) konsoldur. Ancak çıkışı başka bir yere yönlendirebilirsiniz. Örneğin, çıkışı bir değişkende depolayan bir işleve yönlendirebilirsiniz. Yani Module.print mülkünü ayarlayarak çıktıyı HTML'ye ekleyebilirsiniz.

// 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 alma

Giriş dosyasını bellek dosya sistemine almak için komut satırında mkbitmap filename eşdeğerine ihtiyacınız vardır. Bu konuya nasıl yaklaştığımı anlamak için önce mkbitmap'ün girişi nasıl beklediği ve çıkışı nasıl oluşturduğu hakkında biraz bilgi vereceğim.

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. filename bağımsız değişkeni sağlanırsa mkbitmap varsayılan olarak, son eki .pbm olarak değiştirilerek giriş dosya adından elde edilen bir çıkış dosyası oluşturur. Örneğin, example.bmp giriş dosya adı için çıkış dosya adı example.pbm olur.

Emscripten, yerel dosya sistemini simüle eden bir sanal dosya sistemi sağlar. Böylece, senkronize 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 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 sanal dosya sistemine dosya yazmak için kullandığınız bir writeFile() işlevine sahiptir. Aşağıdaki kod örneğinde gösterildiği gibi writeFile() değerini 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 önceki Module.callMain(['-v']) çağrısının kaldırıldığını unutmayın. Bunun nedeni, Module.callMain()'ün 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 sisteminde bir dizi dosyayı gösteren mkbitmap uygulaması.

Gerçek ilk yürütme

Her şey hazır olduğunda Module.callMain(['example.bmp'])'u çalıştırarak mkbitmap'ü yürütün. MEMFS'nin '/' klasörünün içeriğini günlüğe kaydedin. example.bmp giriş dosyasının yanında yeni oluşturulan example.pbm çıkış dosyasını 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();

Bellek dosya sisteminde example.bmp ve example.pbm gibi bir dizi dosyayı 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ıda görüntüleme için PBM dosyalarını desteklemediğinden, işlev bir Uint8Array döndürür. Bu Uint8Array'yi File nesnesine dönüştürüp diske kaydedersiniz. (Dosya kaydetmenin daha şık yolları vardır ancak dinamik olarak oluşturulmuş bir <a download> kullanmak en yaygın olarak desteklenen yöntemdir.) Dosya kaydedildikten sonra 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ının ve çıkış .pbm dosyasının önizlemesini içeren macOS Finder.

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

Bu aşamada giriş dosyası sabit kodlu olur ve mkbitmap varsayılan parametrelerle çalışır. Son adımda, kullanıcının dinamik olarak bir giriş dosyası seçmesine, mkbitmap parametrelerini değiştirmesine ve ardından aracı seçili seçeneklerle çalıştırmasına izin verin.

// 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, bir miktar JavaScript kodu kullanarak çıkış resminin önizlemesini bile gösterebilirsiniz. Bunu yapmanın bir yolu için aşağıdaki yerleşik demo'nun kaynak koduna bakın.

Sonuç

Tebrikler, mkbitmap dosyasını WebAssembly olarak başarıyla derlediniz ve tarayıcıda çalıştırmayı başardınız. Bazı çıkmaz sokaklara girdik ve aracın çalışması için birden fazla kez derlemeniz gerekti. Ancak yukarıda da belirttiğim gibi, bu deneyimin bir parçası. Takılırsanız StackOverflow'un webassembly etiketini de kullanabilirsiniz. İyi derlemeler dileriz.

Teşekkür ederiz

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