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. 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 düşmüş 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 ileterek kullanırsınız. Tüm ayrıntılar için aracın man sayfasına bakın:
$ mkbitmap [options] [filename...]
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:
cd
, ardından paketin kaynak kodunu içeren dizine gidin ve./configure
yazın. Böylece paketi sisteminiz için yapılandırabilirsiniz.configure
çalıştırması biraz zaman alabilir. Çalışırken hangi özellikleri kontrol ettiğini belirten bazı mesajlar yazdırır.Paketi derlemek için
make
yazın.İ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.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ızcamake 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 şunlar belirtilmektedir:
Emscripten ile büyük projeler oluşturmak çok kolaydır. Emscripten, makefile'lerinizi
gcc
yerineemcc
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 durumu 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 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
dosyası 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
dizinine src/
girin. 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 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 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.
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 dahil etmediğ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 birkaç işaret daha ayarlayarak uygulamanız gerekir. Aşağıdaki işaretler diğer projeler için de yararlı olabilir.
- Dosya sistemi desteğinin dahil edilmesi için
-sFILESYSTEM=1
ayarını yapın. Module.FS
veModule.callMain
'in dışa aktarılması için-sEXPORTED_RUNTIME_METHODS=FS,callMain
değerini ayarlayın.- Modern bir ES6 modülü oluşturmak için
-sMODULARIZE=1
ve-sEXPORT_ES6
ayarlarını yapın. - İstemin görünmesine neden olan ilk çalıştırmayı önlemek için
-sINVOKE_RUN=0
değerini ayarlayın.
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
sınıfının main()
işlevi artık başlangıçta çağrılmadığı için istem de kaldırılır.
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();
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();
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. Bir filename
bağımsız değişkeni verilirse 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();
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();
Çı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();
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.