Bazen yalnızca C veya C++ kodu olarak kullanılabilen bir kitaplık kullanmak isteyebilirsiniz. Geleneksel olarak bu noktada pes edersiniz. Ancak artık Emscripten ve WebAssembly (veya Wasm) sayesinde bu durum değişti.
Araç zinciri
Mevcut bazı C kodlarını Wasm'ye nasıl derleyeceğimi öğrenmeyi hedefledim. LLVM'nin Wasm arka ucuyla ilgili bazı söylentiler vardı. Bu nedenle, bu konuyu araştırmaya başladım. Bu şekilde derlenecek basit programlar elde edebilirsiniz ancak C'nin standart kitaplığını kullanmak veya birden fazla dosyayı derlemek istediğinizde sorun yaşayabilirsiniz. Bu durum, öğrendiğim en önemli derse yol açtı:
Emscripten, eskiden C'den asm.js'ye derleyici olarak kullanılıyordu ancak o zamandan beri Wasm'ı hedefleyecek şekilde gelişti ve dahili olarak resmi LLVM arka ucuna geçiş sürecinde. Emscripten, C'nin standart kitaplığının Wasm ile uyumlu bir uygulamasını da sağlar. Emscripten'i kullanın. Bu, çok fazla gizli işi beraberinde getirir, dosya sistemini taklit eder, bellek yönetimi sağlar, OpenGL'yi WebGL ile sarmalar. Bunlar, geliştirme sürecinde kendiniz deneyimlemenize gerek olmayan birçok şeydir.
Bu, şişkinlik konusunda endişelenmeniz gerektiği anlamına geliyor gibi görünse de (ben kesinlikle endişelendim) Emscripten derleyicisi, gerekmeyen her şeyi kaldırır. Denemelerimde, ortaya çıkan Wasm modülleri, içerdiği mantığa uygun boyuttadır. Emscripten ve WebAssembly ekipleri, bu modülleri gelecekte daha da küçültmek için çalışmaktadır.
Emscripten'i web sitesindeki talimatları uygulayarak veya Homebrew'u kullanarak edinebilirsiniz. Benim gibi Docker'a alınmış komutları kullanmayı seviyorsanız ve WebAssembly ile deneme yapmak için sisteminize bir şeyler yüklemek istemiyorsanız bunun yerine kullanabileceğiniz iyi bakılan bir Docker görüntüsü var:
$ docker pull trzeci/emscripten
$ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>
Basit bir şeyi derleme
C dilinde n. Fibonacci sayısını hesaplayan bir fonksiyon yazma örneğini ele alalım:
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
if(n <= 0){
return 0;
}
int i, t, a = 0, b = 1;
for (i = 1; i < n; i++) {
t = a + b;
a = b;
b = t;
}
return b;
}
C dilini biliyorsanız işlevin kendisi pek şaşırtıcı olmayacaktır. C bilmeseniz bile JavaScript biliyorsanız burada neler olduğunu anlayabilirsiniz.
emscripten.h
, Emscripten tarafından sağlanan bir başlık dosyasıdır. Bu makroya erişmek için bu izne ihtiyacımız var ancak EMSCRIPTEN_KEEPALIVE
çok daha fazla işlev sunuyor.
Bu makro, derleyiciye kullanılmıyor gibi görünse bile bir işlevi kaldırmamasını söyler. Bu makroyu atlarsak derleyici işlevi optimize ederek kaldırır. Sonuçta kimse bu işlevi kullanmıyordur.
Tüm bunları fib.c
adlı bir dosyaya kaydedelim. Bunu .wasm
dosyasına dönüştürmek için Emscripten'in derleyici komutunu emcc
kullanmamız gerekir:
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c
Bu komutu inceleyelim. emcc
, Emscripten'in derleyicisidir. fib.c
, C dosyamızdır. Şu ana kadar her şey yolunda. -s WASM=1
, Emscripten'e asm.js dosyası yerine Wasm dosyası vermesini söyler.
-s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'
, derleyiciye cwrap()
işlevini JavaScript dosyasında kullanılabilir durumda bırakmasını söyler. Bu işlevle ilgili daha fazla bilgiyi ilerleyen bölümlerde bulabilirsiniz. -O3
, derleyiciye agresif bir şekilde optimizasyon yapmasını söyler. Derleme süresini kısaltmak için daha düşük sayılar seçebilirsiniz ancak bu durumda derleyici kullanılmayan kodu kaldırmayabileceğinden sonuçta elde edilen paketler daha büyük olur.
Komutu çalıştırdıktan sonra a.out.js
adlı bir JavaScript dosyası ve a.out.wasm
adlı bir WebAssembly dosyası elde edersiniz. Wasm dosyası (veya "modül"), derlenmiş C kodumuzu içerir ve oldukça küçük olmalıdır. JavaScript dosyası, Wasm modülümüzün yüklenmesi ve başlatılmasının yanı sıra daha iyi bir API sağlanmasıyla ilgilenir. Gerekirse yığın, bellek ve C kodu yazarken işletim sistemi tarafından sağlanması beklenen diğer işlevlerin ayarlanmasını da sağlar. Bu nedenle, JavaScript dosyası biraz daha büyük olup 19 KB (~5 KB gzip'li) boyutundadır.
Basit bir şey çalıştırma
Modülünüzü yükleyip çalıştırmanın en kolay yolu, oluşturulan JavaScript dosyasını kullanmaktır. Bu dosyayı yüklediğinizde Module
global bir değişkeniniz olur. Parametreleri C diline uygun bir şeye dönüştürmek ve sarmalanmış işlevi çağırmak için cwrap
işlevini kullanarak JavaScript'e özgü bir işlev oluşturun. cwrap
, işlev adını, dönüş türünü ve bağımsız değişken türlerini sırasıyla bağımsız değişken olarak alır:
<script src="a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const fib = Module.cwrap('fib', 'number', ['number']);
console.log(fib(12));
};
</script>
Bu kodu çalıştırırsanız konsolda 12. Fibonacci sayısı olan "144"ü görmeniz gerekir.
Kutsal kâse: C kitaplığı derleme
Şimdiye kadar yazdığımız C kodu, Wasm göz önünde bulundurularak yazıldı. Ancak WebAssembly'nin temel kullanım alanlarından biri, mevcut C kitaplıkları ekosistemini alıp geliştiricilerin bunları web'de kullanmasına olanak tanımaktır. Bu kitaplıklar genellikle C'nin standart kitaplığına, bir işletim sistemine, bir dosya sistemine ve diğer öğelere bağlıdır. Emscripten, bazı sınırlamalar olsa da bu özelliklerin çoğunu sağlar.
İlk hedefime, yani WebP'den Wasm'ye kodlayıcı derlemeye geri dönelim. WebP codec'inin kaynağı C dilinde yazılmıştır ve API belgelerinin yanı sıra GitHub'da da mevcuttur. Bu oldukça iyi bir başlangıç noktasıdır.
$ git clone https://github.com/webmproject/libwebp
Basit bir başlangıç için WebPGetEncoderVersion()
öğesini encode.h
öğesinden JavaScript'e aktarmayı deneyelim. Bunun için webp.c
adlı bir C dosyası yazın:
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetEncoderVersion();
}
Bu işlevi çağırmak için herhangi bir parametreye veya karmaşık veri yapısına ihtiyacımız olmadığından, libwebp'nin kaynak kodunu derleyip derleyemeyeceğimizi test etmek için bu basit programı kullanabiliriz.
Bu programı derlemek için derleyiciye -I
işaretini kullanarak libwebp'nin başlık dosyalarını nerede bulabileceğini söylememiz ve ayrıca ihtiyacı olan tüm libwebp C dosyalarını iletmemiz gerekir. Dürüst olacağım: Bulabildiğim tüm C dosyalarını verdim ve derleyicinin gereksiz olan her şeyi kaldırmasına güvendim. Bu yöntem harika sonuç verdi.
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
Artık yepyeni modülümüzü yüklemek için yalnızca biraz HTML ve JavaScript'e ihtiyacımız var:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = async (_) => {
const api = {
version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
</script>
Düzeltilmiş sürüm numarasını çıkışta göreceğiz:
JavaScript'ten Wasm'ye resim alma
Kodlayıcının sürüm numarasını almak güzel bir şey olsa da gerçek bir görüntüyü kodlamak daha etkileyici olur, değil mi? O zaman hadi başlayalım.
Cevaplamamız gereken ilk soru şudur: Görüntüyü Wasm dünyasına nasıl aktarırız?
libwebp'nin kodlama API'sine baktığımızda, RGB, RGBA, BGR veya BGRA biçiminde bir bayt dizisi beklediğini görüyoruz. Neyse ki Canvas API'de, getImageData()
var. Bu, bize RGBA biçiminde görüntü verilerini içeren bir Uint8ClampedArray veriyor:
async function loadImage(src) {
// Load image
const imgBlob = await fetch(src).then((resp) => resp.blob());
const img = await createImageBitmap(imgBlob);
// Make canvas same size as image
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// Draw image onto canvas
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
Artık JavaScript dünyasındaki verileri Wasm dünyasına kopyalamak "yeterli". Bunun için iki ek işlevin kullanıma sunulması gerekir. Biri Wasm alanındaki resim için bellek ayıran, diğeri ise bu alanı tekrar boşaltan iki işlev vardır:
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
return malloc(width * height * 4 * sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
free(p);
}
create_buffer
, RGBA resmi için bir arabellek ayırır. Bu nedenle piksel başına 4 bayt ayrılır.
malloc()
tarafından döndürülen işaretçi, arabelleğin ilk bellek hücresinin adresidir. İşaretçi JavaScript alanına döndürüldüğünde yalnızca bir sayı olarak değerlendirilir. cwrap
kullanarak işlevi JavaScript'e aktardıktan sonra, arabelleğimizin başlangıcını bulmak ve görüntü verilerini kopyalamak için bu sayıyı kullanabiliriz.
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
const image = await loadImage('/image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);
Büyük final: Resmi kodlama
Resim artık Wasm'de kullanılabilir. WebP kodlayıcıyı işini yapması için çağırmanın zamanı geldi. WebP dokümanlarına baktığımızda WebPEncodeRGBA
, mükemmel bir seçim gibi görünüyor. İşlev, giriş resmine ve boyutlarına yönelik bir işaretçinin yanı sıra 0 ile 100 arasında bir kalite seçeneği alır. Ayrıca, WebP görüntüsüyle işimiz bittiğinde WebPFree()
kullanarak boşaltmamız gereken bir çıkış arabelleği de ayırır.
Kodlama işleminin sonucu, bir çıkış arabelleği ve uzunluğudur. C'deki işlevler, belleği dinamik olarak ayırmadığımız sürece dizi döndürme türüne sahip olamayacağından statik bir genel diziye başvurdum. Bunun temiz bir C olmadığını biliyorum (aslında Wasm işaretçilerinin 32 bit genişliğinde olmasından yararlanıyor), ancak işleri basit tutmak için bunun adil bir kısayol olduğunu düşünüyorum.
int result[2];
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* img_in, int width, int height, float quality) {
uint8_t* img_out;
size_t size;
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
result[0] = (int)img_out;
result[1] = size;
}
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t* result) {
WebPFree(result);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
return result[0];
}
EMSCRIPTEN_KEEPALIVE
int get_result_size() {
return result[1];
}
Tüm bunlar tamamlandıktan sonra kodlama işlevini çağırabilir, işaretçiyi ve resim boyutunu alabilir, bunları kendi JavaScript alanımızdaki bir arabelleğe yerleştirebilir ve bu işlemde ayırdığımız tüm Wasm alanı arabelleklerini serbest bırakabiliriz.
api.encode(p, image.width, image.height, 100);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result(resultPointer);
Resminizin boyutuna bağlı olarak, Wasm'ın hem giriş hem de çıkış resmini barındıracak kadar bellek büyütemediği bir hatayla karşılaşabilirsiniz:
Neyse ki bu sorunun çözümü hata mesajında yer alıyor. Derleme komutumuza -s ALLOW_MEMORY_GROWTH=1
eklememiz yeterlidir.
İşte bu kadar! WebP kodlayıcı derledik ve bir JPEG resmini WebP'ye dönüştürdük. Çalıştığını kanıtlamak için sonuç arabelleğimizi bir blob'a dönüştürebilir ve <img>
öğesinde kullanabiliriz:
const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img);
Yeni bir WebP resminin ihtişamına bakın.
Sonuç
C kitaplığını tarayıcıda çalıştırmak kolay olmasa da genel süreci ve veri akışının nasıl çalıştığını anladığınızda bu işlem kolaylaşır ve sonuçlar etkileyici olabilir.
WebAssembly, web'de işleme, sayısal hesaplama ve oyun için birçok yeni olasılık sunuyor. Wasm'ın her şeye uygulanması gereken bir çözüm olmadığını, ancak bu darboğazlardan birine rastladığınızda Wasm'ın inanılmaz derecede faydalı bir araç olabileceğini unutmayın.
Bonus içerik: Basit bir şeyi zor yoldan yapmak
Oluşturulan JavaScript dosyasını kullanmamayı denemek isterseniz bunu yapabilirsiniz. Fibonacci örneğine dönelim. Yükleyip kendimiz çalıştırmak için şunları yapabiliriz:
<!DOCTYPE html>
<script>
(async function () {
const imports = {
env: {
memory: new WebAssembly.Memory({ initial: 1 }),
STACKTOP: 0,
},
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/a.out.wasm'),
imports,
);
console.log(instance.exports._fib(12));
})();
</script>
Emscripten tarafından oluşturulan WebAssembly modüllerine bellek sağlamadığınız sürece bu modüllerin çalışabileceği bir bellek yoktur. Herhangi bir şey içeren bir Wasm modülü sağlamanın yolu, instantiateStreaming
işlevinin ikinci parametresi olan imports
nesnesini kullanmaktır. Wasm modülü, imports nesnesinin içindeki her şeye erişebilir ancak bunun dışındaki hiçbir şeye erişemez. Emscripting tarafından derlenen modüller, yükleme JavaScript ortamından birkaç şey bekler:
- İlk olarak
env.memory
var. Wasm modülü, dış dünyadan haberdar değildir. Bu nedenle, çalışmak için biraz belleğe ihtiyacı vardır.WebAssembly.Memory
yazın. (İsteğe bağlı olarak büyütülebilen) doğrusal bir bellek parçasını temsil eder. Boyutlandırma parametreleri "WebAssembly sayfaları birimleriyle" ifade edilir. Bu, yukarıdaki kodun 1 sayfa bellek ayırdığı ve her sayfanın 64 KiB boyutunda olduğu anlamına gelir.maximum
seçeneği sağlanmadığında bellek teorik olarak sınırsız büyür (Chrome'da şu anda 2 GB'lık bir sınır vardır). Çoğu WebAssembly modülünün maksimum değer ayarlaması gerekmez. env.STACKTOP
, yığının büyümeye başlaması gereken yeri tanımlar. Yığın, işlev çağrıları yapmak ve yerel değişkenler için bellek ayırmak üzere gereklidir. Küçük Fibonacci programımızda herhangi bir dinamik bellek yönetimi hilesi yapmadığımız için tüm belleği yığın olarak kullanabiliriz. Bu nedenleSTACKTOP = 0
.