Bazen yalnızca C veya C++ kodu olarak kullanılabilen bir kitaplık kullanmak isteyebilirsiniz. Genellikle bu noktada pes edersiniz. Artık Emscripten ve WebAssembly (veya Wasm) sayesinde bu sorun ortadan kalktı.
Araç zinciri
Mevcut bazı C kodlarını Wasm'e nasıl derleyeceğimi öğrenmeyi hedef olarak belirledim. LLVM'ın Wasm arka ucu hakkında bazı söylentiler vardı. Bu konuyu incelemeye başladım. Bu şekilde basit programları derleyebilirsiniz. Ancak C'nin standart kitaplığını kullanmak veya hatta birden fazla dosyayı derlemek istediğinizde muhtemelen sorunla karşılaşırsınız. Bu da beni önemli bir ders almaya yönlendirdi:
Emscripten, C'den asm.js'e derleyici olarak kullanılıyordu. Ancak o zamandan beri Wasm'i hedefleyecek şekilde olgunlaştı ve dahili olarak resmi LLVM arka ucuna geçme aşamasında. Emscripten, C'nin standart kitaplığının Wasm uyumlu bir uygulamasını da sağlar. Emscripten'i kullanın. Çok sayıda gizli işlem gerçekleştirir, dosya sistemini taklit eder, bellek yönetimi sağlar, OpenGL'i WebGL ile sarar. Bunların çoğu, kendiniz geliştirmeniz gerekmeyen işlemlerdir.
Bu, gereksiz kodlar konusunda endişelenmeniz gerektiği gibi görünse de (ben kesinlikle endişelendim) Emscripten derleyicisi gereksiz her şeyi kaldırır. Denemelerde elde edilen Wasm modülleri, içerdikleri mantık için uygun boyuttadır ve Emscripten ile WebAssembly ekipleri, gelecekte bunları daha da küçültmek için çalışmaktadır.
Emscripten'i web sitesinde verilen talimatları uygulayarak veya Homebrew'i kullanarak edinebilirsiniz. Benim gibi docker'a alınmış komutları seviyorsanız ve WebAssembly ile oynamak için sisteminize bir şeyler yüklemek istemiyorsanız bunun yerine kullanabileceğiniz iyi korunmuş bir Docker resmi vardır:
$ docker pull trzeci/emscripten
$ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>
Basit bir şey derleme
C'de nth Fibonacci sayısını hesaplayan bir işlev yazmayla ilgili neredeyse standart bir örneği 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'yi biliyorsanız işlevin kendisi çok şaşırtıcı olmayacaktır. C dilini bilmeseniz bile JavaScript'i biliyorsanız burada neler olduğunu anlayabilirsiniz.
emscripten.h
, Emscripten tarafından sağlanan bir başlık dosyasıdır. Yalnızca EMSCRIPTEN_KEEPALIVE
makrosuna erişebilmek için buna ihtiyacımız var ancak çok daha fazla işlev sunar.
Bu makro, derleyiciye kullanılmamış görünse bile bir işlevi kaldırmamasını söyler. Bu makroyu atlarsak derleyici işlevi optimize eder. Sonuçta kimse bu işlevi kullanmıyordur.
Tüm bunları fib.c
adlı bir dosyaya kaydedelim. .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 bize bir 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şlev hakkında daha fazla bilgiyi aşağıda bulabilirsiniz. -O3
, derleyiciye agresif bir şekilde optimizasyon yapmasını söyler. Derleme süresini azaltmak için daha düşük sayılar seçebilirsiniz ancak derleyici kullanılmayan kodu kaldıramayacağından, bu da ortaya çıkan paketlerin daha büyük olmasını sağlar.
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ü yükleyip başlatır ve daha iyi bir API sağlar. Gerekirse yığın, yığın alanı ve C kodu yazarken genellikle işletim sistemi tarafından sağlanması beklenen diğer işlevleri de ayarlar. Bu nedenle, JavaScript dosyası biraz daha büyüktür ve 19 KB'tır (~5 KB gzip'lenmiştir).
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ükledikten sonra Module
global bir değişkeniniz olur. Parametreleri C'ye uygun bir değere dönüştüren ve sarmalanmış işlevi çağıran bir JavaScript doğal işlevi oluşturmak için cwrap
işlevini kullanın. cwrap
, işlev adını, döndürme türünü ve bağımsız değişken türlerini şu sırayla 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" değerini görürsünüz.
Kutsal kase: C kitaplığı derleme
Şimdiye kadar yazdığımız C kodu, Wasm'i göz önünde bulundurarak yazılmıştır. Ancak WebAssembly'in temel kullanım alanlarından biri, mevcut C kitaplığı ekosistemini alıp geliştiricilerin bunları web'de kullanmasına izin vermektir. Bu kitaplıklar genellikle C'nin standart kitaplığına, işletim sistemine, dosya sistemine ve diğer öğelere dayanır. Emscripten bu özelliklerin çoğunu sağlar ancak bazı sınırlamalar vardır.
Asıl hedefime dönelim: WebP'den Wasm'e kodlayıcı derleme. WebP codec'inin kaynağı C dilinde yazılmıştır ve GitHub'da, ayrıca kapsamlı API dokümanlarında bulunabilir. Bu oldukça iyi bir başlangıç noktası.
$ git clone https://github.com/webmproject/libwebp
Basit bir başlangıç yapmak için webp.c
adlı bir C dosyası yazarak WebPGetEncoderVersion()
sınıfını encode.h
'dan JavaScript'e göstermeye çalışalım:
#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 ihtiyaç duymadığımızdan, derlemek için libwebp'nin kaynak kodunu alıp alamayacağımızı test etmek üzere bu basit programı kullanabiliriz.
Bu programı derlemek için derleyiciye -I
işaretçisini kullanarak libwebp başlık dosyalarını nerede bulabileceğini söylememiz ve ayrıca ihtiyaç duyduğu tüm libwebp C dosyalarını iletmemiz gerekir. Dürüst olacağım: Bulabildiğimiz tüm C dosyalarını derleyiciye verdim ve gereksiz olan her şeyi kaldırması için derleyiciye güvendim. Bu yaklaşımın işe yaradığı anlaşılıyor.
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
Yeni modülümüzü yüklemek için 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üzeltme sürüm numarasını çıktıda görürüz:
JavaScript'ten Wasm'e resim alma
Kodlayıcının sürüm numarasını almak iyi bir şey olsa da gerçek bir resmi kodlamak daha etkileyici olur, değil mi? O halde bunu yapalım.
Yanıtlamamız gereken ilk soru şudur: Resmi Wasm alanına nasıl ekleriz?
libwebp'nin kodlama API'sine baktığımızda RGB, RGBA, BGR veya BGRA biçiminde bir bayt dizisi beklendiğini görüyoruz. Neyse ki Canvas API'de getImageData()
işlevi vardır. Bu işlev, bize RGBA biçimindeki resim verilerini içeren bir Uint8ClampedArray verir:
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 "yalnızca" JavaScript dünyasındaki verileri Wasm dünyasına kopyalamak gerekiyor. Bunun için iki ek işlev göstermemiz gerekiyor. Wasm alanındaki resim için bellek ayıran ve tekrar boşaltan birer işlev:
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, söz konusu arabelleğin ilk bellek hücresinin adresidir. İşaretçi JavaScript alanına döndürüldüğünde yalnızca bir sayı olarak değerlendirilir. İşlevi cwrap
kullanarak JavaScript'e açıkladıktan sonra, arabelleğin başlangıcını bulmak ve resim 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 kodlayın
Resim artık Wasm'de kullanılabilir. WebP kodlayıcısını 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ş resminin ve boyutlarının yanı sıra 0 ile 100 arasında bir kalite seçeneği alır. Ayrıca, WebP resmiyle işimiz bittiğinde WebPFree()
kullanarak serbest bırakmamı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, döndürme türü olarak diziler kullanamadığından (hafızayı dinamik olarak ayırmadığımız sürece) statik bir genel diziye başvurdum. Bunun saf C olmadığının farkındayım (aslında Wasm işaretçilerinin 32 bit genişliğinde olmasından yararlanıyor). Ancak basitleştirmek için bu kısayolun uygun 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 bu işlemler tamamlandığında kodlama işlevini çağırabilir, işaretçiyi ve resim boyutunu alıp kendi JavaScript alanımızdaki bir arabelleğe koyabilir ve bu süreçte 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'in hem giriş hem de çıkış resmini alacak kadar belleği 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! Bir WebP kodlayıcı derledik ve bir JPEG resmini WebP'ye kodladık. İşlemin çalıştığını kanıtlamak için sonuç arabelleğimizi bir blob'a dönüştürüp bir <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 WebP resminin ihtişamına göz atın.
Sonuç
Bir C kitaplığının tarayıcıda çalışmasını sağlamak kolay bir iş değildir ancak genel süreci ve veri akışının işleyiş şeklini anladığınızda işler kolaylaşır ve elde edeceğiniz sonuçlar şaşırtıcı olabilir.
WebAssembly, web'de işleme, sayısal işleme ve oyun için birçok yeni olanak sunuyor. Wasm'in her şeye uygulanması gereken bir çözüm olmadığını unutmayın. Ancak bu darboğazlardan birine rastladığınızda Wasm son derece faydalı bir araç olabilir.
Bonus içerik: Basit bir işlemi zor yoldan yapma
Oluşturulan JavaScript dosyasını önlemek isterseniz bunu deneyebilirsiniz. Fibonacci örneğine dönelim. Dosyayı kendimizin yükleyip çalıştırması için aşağıdakileri 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üller çalışamaz. Bir Wasm modülüne herhangi bir şey sağlamanın yolu, instantiateStreaming
işlevinin ikinci parametresi olan imports
nesnesini kullanmaktır. Wasm modülü, içe aktarma 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:
- Öncelikle
env.memory
var. Wasm modülü, dış dünyadan haberdar olmadığından çalışmak için biraz bellek almalıdır.WebAssembly.Memory
yazın. Bu, doğrusal bir bellek parçasını (isteğe bağlı olarak genişletilebilir) temsil eder. Boyutlandırma parametreleri "WebAssembly sayfası birimleri cinsinden"dir. Yani yukarıdaki kod, her biri 64 KiB boyutunda olan 1 sayfa bellek ayırır.maximum
seçeneği sağlanmadığı sürece bellek teorik olarak sınırsız şekilde büyüyebilir (Chrome'da şu anda 2 GB'lık katı bir sınır vardır). Çoğu WebAssembly modülünün maksimum değer ayarlaması yapması gerekmez. env.STACKTOP
, yığının nereden büyümeye başlaması gerektiğini tanımlar. İşlev çağrıları yapmak ve yerel değişkenler için bellek ayırmak için yığın gerekir. Küçük Fibonacci programımızda dinamik bellek yönetimi hilesi yapmadığımız için belleğin tamamını bir yığın olarak kullanabiliriz. Bu nedenleSTACKTOP = 0
.