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

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. Aşağıda açıklanan adımlardan bazıları bu nedenle 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 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 özelliğinin 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. 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).
Çizgi film resmi, ön işleme işleminden sonra gri tonlamaya dönüştürüldü.
İlk olarak ölçeklendirilir, ardından eşiğe 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 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, 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ü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 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'.

İş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 öğesini WebAssembly'de derleyin

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

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ş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 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. cd dizininde src/ dosyasını bulun. Artık mkbitmap ve potrace adlı iki yeni dosya da mevcuttur. Bu makale için yalnızca mkbitmap geçerlidir. .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'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 bir yerel 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şareti içeren modüler bir 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. 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 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();

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

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ğ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ı DevTools 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ö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 al

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 ürününün girdiyi 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. 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 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'ü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ı.

İlk gerçek 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. 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();

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ı olsa da 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ı 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, bazı JavaScript kodlarıyla çıktı resminin bir ö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.