mkbitmap'i WebAssembly'ye derleme

WebAssembly nedir ve nereden geldi? bölümünde Bugün webAssembly'yi nasıl hazırladığımızı açıkladım. Bu makalede, mkbitmap adlı mevcut C programını WebAssembly'de derleme yaklaşımımı göstereceğim. Bu, dosyalarla çalışmayı, WebAssembly ile JavaScript alanları arasında iletişim kurmayı ve tuvale çizim yapmayı içerdiği için hello world örneğinden daha karmaşıktır ancak sizi bunaltmayacak kadar da yönetilebilir.

Bu makale WebAssembly konusunda bilgi edinmek isteyen web geliştiricileri için yazılmıştır ve mkbitmap gibi bir şeyi WebAssembly'de derlemek istediğinizde nasıl ilerleyebileceğiniz adım adım anlatılmaktadır. Adil bir uyarı olarak, bir uygulamanın veya kitaplığın ilk çalıştırmada derlenememesi son derece normaldir. Bu nedenle aşağıda açıklanan adımlardan bazıları çalışmadığı için geri gidip farklı bir şekilde tekrar denemem gerekti. Makale, sihirli nihai derleme komutunu sanki gökyüzünden düşmüş gibi göstermiyor, bunun yerine bazı hayal kırıklıklarını da içeren gerçek ilerlememi açıklıyor.

mkbitmap hakkında

mkbitmap C programı bir görüntüyü okur ve şu sırayla bir veya daha fazla işleme uygular: ters çevirme, yüksek geçiş filtrelemesi, ölçeklendirme ve eşik. Her işlem ayrı ayrı kontrol edilip açılabilir veya kapatılabilir. mkbitmap ürününün temel kullanımı renkli veya gri tonlamalı resimleri diğer programlara, özellikle de SVGcode'un temelini oluşturan izleme programı potrace için uygun bir biçime dönüştürmektir. Ön işleme aracı olan mkbitmap, özellikle çizgi filmler veya elle yazılmış metinler gibi taranmış çizgi sanatı yüksek çözünürlüklü iki düzeyli resimlere dönüştürmek için kullanışlıdır.

mkbitmap öğesini, bir dizi seçenek ve bir veya daha fazla dosya adı ileterek kullanırsınız. Tüm ayrıntılar için aracın yönergeler sayfasına bakın:

$ mkbitmap [options] [filename...]
Renkli çizgi film resmi.
Orijinal resim (Kaynak).
Çizgi film resmi, ön işlemeden sonra gri tonlamaya dönüştürüldü.
Önce ölçeklendirildi, daha sonra eşik uygulanacak: mkbitmap -f 2 -s 2 -t 0.48 (Kaynak).

Kodu alın

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

Derleyin ve yerel olarak yükleyin

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

  1. Paketi sisteminiz için yapılandırmak üzere paketin kaynak kodunu içeren dizine cd ve ./configure türünü ekleyin.

    configure uygulamasının çalışması biraz zaman alabilir. Sistem ç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 kendi kendine testleri, genellikle yeni oluşturulmuş kaldırılmış ikili programları kullanarak ç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öke ait bir ön eke yükleme yaparken paketin normal bir kullanıcı olarak yapılandırılması ve derlenmesi ve yalnızca kök ayrıcalıklarıyla yürütülen make install aşamasının uygulanması önerilir.

Bu adımları uyguladığınızda, potrace ve mkbitmap olmak üzere iki yürütülebilir dosya elde etmiş olmanız gerekir. Bu ikincisi bu makalede ele alınmaktadır. mkbitmap --version komutunu çalıştırarak düzgün şekilde çalıştığını doğrulayabilirsiniz. Makinemdeki dört adımın tamamının (kısa olması için büyük ölçüde kırpılmış hali) şu şekilde:

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 başarıyla derlenmiş ve yüklenmiş demektir. Ardından, bu adımların eşdeğerinin WebAssembly ile çalışmasını sağlayın.

mkbitmap öğesini WebAssembly'ye derleyin

Emscripten, C/C++ programlarını WebAssembly'ye derlemeye yarayan bir araçtır. Emscripten'in Proje Oluşturma belgelerinde şunlar belirtilmektedir:

Emscripten ile büyük projeler oluşturmak çok kolaydır. Emscripten, oluşturma dosyalarınızı gcc için geçici yedek olarak emcc kullanacak şekilde yapılandıran iki basit komut dosyası sağlar. Çoğu durumda, projenizin mevcut derleme sisteminin geri kalanında herhangi bir değişiklik olmaz.

Ardından dokümanlar bu şekilde devam eder (kısa olması için biraz düzenlendi):

Normalde derleme sırasında aşağıdaki komutları kullanın:

./configure
make

Empscripten ile derlemek için aşağıdaki komutları kullanmanız gerekir:

emconfigure ./configure
emmake make

Dolayısıyla, ./configure değeri emconfigure ./configure, make ise emmake make haline gelir. 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 dizinin bir yerinde .wasm dosyası olması gerekir. find . -name "*.wasm" komut dosyasını çalıştırarak bulabilirsiniz:

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

Son iki çözüm umut verici görünüyor. Bu nedenle src/ dizinine cd göz atın. Ayrıca artık iki yeni dosya var: mkbitmap ve potrace. Bu makale yalnızca mkbitmap ile ilgilidir. .js uzantısına sahip olmamaları biraz kafa karıştırıcıdır, ancak aslında JavaScript dosyalarıdır ve hızlı bir head çağrısıyla doğrulanabilirler:

$ 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 sırasıyla mv potrace potrace.js) yöntemini çağırarak JavaScript dosyasını mkbitmap.js olarak yeniden adlandırın. Şimdi ilk testin, node mkbitmap.js --version komutunu çalıştırarak komut satırında Node.js ile dosyayı çalıştırarak çalışıp çalışmadığını anlama zamanı:

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

mkbitmap öğesini WebAssembly'de başarıyla derlediniz. Şimdi bir sonraki adım, tarayıcının tarayıcıda çalışmasını sağlamak.

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 standart 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. Giriş yapmanızı isteyen bir istem görmeniz gerekir. Aracın kılavuz sayfasına göre, "[i]hiçbir dosya adı bağımsız değişkeni verilmemişse mkbit eşleme, standart girdiden okuma yapan bir filtre görevi görür". Bu, Emscripten için varsayılan olarak bir prompt() olduğundan beklendiği gibidir.

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

Otomatik yürütmeyi engelle

mkbitmap ürününün hemen yürütülmesini 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ütme işleminin çeşitli noktalarında çağırdığı özelliklere sahip genel bir JavaScript nesnesidir. Kodun yürütülmesini kontrol etmek için Module uygulaması 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 adında bir komut dosyası oluşturun, bu komut dosyasını index.html etiketinde <script src="mkbitmap.js"></script> öğesinden önce ekleyin ve aşağıdaki kodu script.js öğesine ekleyin. Uygulamayı şimdi yeniden yüklediğinizde istem kaybolur.

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

Daha fazla derleme bayrağıyla modüler bir derleme oluşturun

Uygulamaya giriş yapmak için Module.FS'de Emscripten'in dosya sistemi desteğini kullanabilirsiniz. Dokümanların Dosya Sistemi Desteği Dahil Olma bölümünde şunlar belirtilmektedir:

Emscripten, dosya sistemi desteğini otomatik olarak ekleyip eklememeye karar verir. Birçok programın dosyaya ihtiyacı yoktur ve dosya sistemi desteğinin boyutu göz ardı edilebilir düzeyde değildir. Bu nedenle Emscripten, bir neden görmediğinde dosyayı eklemekten kaçınır. Yani, C/C++ kodunuz dosyalara erişmiyorsa FS nesnesi ve diğer dosya sistemi API'leri çıkışa dahil edilmez. Diğer yandan, C/C++ kodunuz dosya kullanıyorsa, dosya sistemi desteği otomatik olarak eklenir.

Ne yazık ki mkbitmap, Emscripten'ın dosya sistemi desteğini otomatik olarak eklemediği durumlardan biridir. Dolayısıyla, bunu 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 ayarlanmış birkaç işareti daha uygulamanız gerektiği anlamına gelir. Aşağıdaki işaretler başka projeler için de faydalı olabilir.

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

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

index.html öğesini değiştirerek yalnızca mkbitmap.js modülünü içe aktaracağınız script.js ES modülünü yükleyecek şekilde değiştirin.

<!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ı şu anda tarayıcıda açtığınızda Module nesnesinin DevTools konsoluna kaydedildiğini görürsünüz. mkbitmap öğesinin main() işlevi artık başlangıçta çağrılmadığı için istem kaybolur.

Beyaz bir ekran görünen mkbitmap uygulaması, DevTools konsoluna kaydedilen Modül nesnesini gösteriyor.

Ana işlevi manuel olarak yürütün

Bir sonraki adım, Module.callMain() işlevini çalıştırarak mkbitmap'in main() işlevini manuel olarak çağırmaktır. callMain() işlevi, komut satırında ileteceğiniz şekilde 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']) çağrısı yapılır. 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 bir ekran görünen mkbitmap uygulaması, DevTools konsoluna kaydedilen mkbit eşlem sürüm numarasını gösteriyor.

Standart çıkışı yönlendir

Varsayılan olarak standart çıkış (stdout) konsoldur. Bununla birlikte, çıktıyı bir değişkene depolayan işlev gibi başka bir öğeye 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();

Mobil bit eşlem 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. Buna nasıl yaklaştığımı anlamak için önce mkbitmap ürününün girdisini nasıl beklediği ve çıkışını nasıl oluşturduğu hakkında biraz bilgi vermem gerekiyor.

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 sağlanırsa mkbitmap varsayılan olarak adını, son eki .pbm olarak değiştirilerek giriş dosyası adından alınan bir çıkış dosyası oluşturur. Örneğin, giriş dosyası adı example.bmp için çıkış dosyasının adı example.pbm olur.

Emscripten, yerel dosya sistemini simüle eden bir sanal dosya sistemi sağlar. Böylece eşzamanlı dosya API'lerini kullanan yerel kod çok az değişiklik yaparak veya hiç değişiklik yapmadan derlenip çalıştırılabilir. mkbitmap işlevinin 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 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. writeFile() kodunu 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 simgesini 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']) numaralı telefona 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ı beklenen 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();

Bellek dosya sistemindeki example.bmp gibi bir dizi dosyayı 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 işlemini yürütün. MEMFS'in '/' 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ö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();

Bellek dosya sistemindeki 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, bellek dosya sisteminden son adımda example.pbm oluşturulmasını sağlar. Tarayıcılar, doğrudan tarayıcı içinde görüntüleme için PBM dosyalarını genellikle desteklemediğinden işlev, File nesnesine dönüştürüp diske kaydettiğiniz Uint8Array değerini döndürür. (Bir dosyayı kaydetmenin daha şık yolları vardır, ancak dinamik olarak oluşturulan <a download> kullanmak en yaygın şekilde 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ü ekleyin

Bu noktada, giriş dosyasının kodu gömülür ve mkbitmap, varsayılan parametrelerle çalışır. Son adım, kullanıcının dinamik olarak bir giriş dosyası seçmesini sağlamak, mkbitmap parametrelerini değiştirmek ve ardından belirlenen seçeneklerle aracı çalıştırmaktır.

// 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 çıkış resminin önizlemesini bile gösterebilirsiniz. Bunu yapmanın bir yolu için aşağıdaki yerleştirilmiş demonun kaynak koduna bakın.

Sonuç

Tebrikler, mkbitmap öğesini WebAssembly'de başarıyla derlediniz ve tarayıcıda çalışmasını sağladınız. Bazı çıkmazlar vardı ve araç çalışana kadar aracı bir kereden fazla derlemeniz gerekti, ancak yukarıda da belirttiğim gibi bu, deneyimin 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 incelendi.