WebAssembly nedir ve nereden çıktı? başlıklı makalede, Günümüzdeki WebAssembly'ye nasıl ulaştığımızı 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. Uygulama veya kitaplığın ilk çalıştırmada 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 komutunun gökten düşmüş gibi gösterilmek yerine gerçek ilerleme sürecim, bazı hayal kırıklıkları da dahil olmak üzere anlatılı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 dönüştürmektir. Özellikle de SVGcode'un temelini oluşturan izleme programı potrace için. 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...]
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:
cdkomutunu kullanarak paketin kaynak kodunu içeren dizine gidin ve./configurekomutunu yazarak paketi sisteminiz için yapılandırın.configurekomutunun çalıştırılması biraz zaman alabilir. Çalışırken hangi özellikleri kontrol ettiğini belirten bazı mesajlar yazdırır.Paketi derlemek için
makeyazın.İsteğe bağlı olarak, pakette bulunan tüm kendi kendine testleri çalıştırmak için
make checkyazın. Bu testler genellikle yeni oluşturulmuş, yüklenmemiş ikili dosyaları kullanır.Programları, veri dosyalarını ve belgeleri yüklemek için
make installyazı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ızcamake installaş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ısaltılmış olan, makinemdeki dört adımın çıktısını 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'.
Ç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 belirtilir:
Emscripten ile büyük projeler oluşturmak çok kolaydır. Emscripten, makefile'larınızı
gccyerineemcckullanacak ş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 de node mkbitmap.js --version komutunu çalıştırarak komut satırında Node.js ile dosyayı 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.

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çinde <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
FSnesnesinin 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.
- Dosya sistemi desteğinin dahil edilmesi için
-sFILESYSTEM=1'ı ayarlayın. Module.FSveModule.callMaindışa aktarılacak şekilde-sEXPORTED_RUNTIME_METHODS=FS,callMaindeğerini ayarlayın.- Modern bir ES6 modülü oluşturmak için
-sMODULARIZE=1ve-sEXPORT_ES6değerlerini ayarlayın. - İstemin görünmesine neden olan ilk çalıştırmayı önlemek için
-sINVOKE_RUN=0'ı ayarlayın.
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 script.js ES modülünü yükleyecek şekilde değiştirin. Ardından, mkbitmap.js modülünü bu ES modülünden 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.

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

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

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ştirilerek 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();

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

Çı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 değeri 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();

Etkileşimli kullanıcı arayüzü ekleme
Bu noktaya kadar giriş dosyası sabit kodluydu ve mkbitmap, varsayılan parametrelerle çalışıyordu. 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.