WebAssembly'yi bu kuruluma nasıl entegre edersiniz? Bu makalede, örnek olarak C/C++ ve Emscripten'i kullanarak bunu çözeceğiz.
WebAssembly (wasm), genellikle temel performans öğesi veya mevcut C++ kod tabanınızı web'de çalıştırmanın bir yolu olarak çerçevelenir. squoosh.app ile, wasm için en azından üçüncü bir bakış açısının olduğunu göstermek istedik: Diğer programlama dillerinin devasa ekosistemlerinden yararlanmak. Emscripten ile C/C++ kodunu kullanabilirsiniz, Rust'ta yerleşik olarak wasm desteği vardır, Go ekibi de bu konuda çalışmaktadır. Daha pek çok dilin takip edeceğinden eminim.
Bu senaryolarda wasm, uygulamanızın merkezinde değil, bir yapbozun parçasıdır, yani başka bir modüldür. Uygulamanızda zaten JavaScript, CSS, resim öğeleri, web merkezli derleme sistemi ve hatta React gibi bir çerçeve bulunuyor. WebAssembly'yi bu kuruluma nasıl entegre edebilirsiniz? Bu makalede, örnek olarak C/C++ ve Emscripten kullanarak bunu üzerinde çalışacağız.
Docker
Emscripten ile çalışırken Docker'ın çok değerli olduğunu düşünüyorum. C/C++ kitaplıkları genellikle oluşturuldukları işletim sistemiyle çalışacak şekilde yazılır. Tutarlı bir ortam oluşturmak çok faydalı. Docker sayesinde Emscripten ile çalışacak şekilde ayarlanmış, tüm araçlar ve bağımlılıklar yüklü olan sanal bir Linux sistemi alırsınız. Eksik bir öğe varsa, kendi makinenizi veya diğer projelerinizi nasıl etkileyeceği konusunda endişelenmenize gerek kalmadan onu yükleyebilirsiniz. Bir şeyler ters giderse container'ı atıp baştan başlayın. Bir kez çalışırsa çalışmaya devam edeceğinden ve aynı sonuçları verdiğinden emin olabilirsiniz.
Docker Registry, yoğun olarak kullanmakta olduğum trzeci imzalı bir Emscripten görüntüsüne sahip.
npm ile entegrasyon
Çoğu durumda, bir web projesinin giriş noktası npm'nin package.json
değeridir. Geleneksel olarak çoğu proje npm install &&
npm run build
ile oluşturulabilir.
Genel olarak, Emscripten tarafından oluşturulan derleme yapıları (bir .js
ve .wasm
dosyası) yalnızca başka bir JavaScript modülü ve sadece başka bir öğe olarak değerlendirilmelidir. JavaScript dosyası, webpack veya rollup gibi bir paketleyici tarafından işlenebilir ve wasm dosyası, resimler gibi daha büyük bir ikili program varlıkları gibi ele alınmalıdır.
Bu nedenle, "normal" derleme süreciniz devreye girmeden önce Emscripten derleme yapılarının derlenmesi gerekir:
{
"name": "my-worldchanging-project",
"scripts": {
"build:emscripten": "docker run --rm -v $(pwd):/src trzeci/emscripten
./build.sh",
"build:app": "<the old build command>",
"build": "npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
Yeni build:emscripten
görevi, Emscripten'i doğrudan çağırabilir ancak daha önce de belirtildiği gibi, derleme ortamının tutarlı olduğundan emin olmak için Docker'ı kullanmanızı öneririm.
docker run ... trzeci/emscripten ./build.sh
Docker'a trzeci/emscripten
görüntüsünü kullanarak yeni bir container hazırlamasını ve ./build.sh
komutunu çalıştırmasını söyler.
build.sh
, bundan sonra yazacağınız bir kabuk komut dosyasıdır. --rm
, Docker'a container'ın çalışmasını tamamladığında bu container'ı silmesini bildirir. Bu şekilde, zaman içinde eski makine görüntülerinden oluşan bir koleksiyon oluşturmazsınız. -v $(pwd):/src
, Docker'ın mevcut dizini ($(pwd)
) container içindeki /src
öğesine "yansıtmasını" istediğiniz anlamına gelir. Kapsayıcının içindeki /src
dizininde yer alan dosyalarda yaptığınız değişiklikler, gerçek projenize yansıtılır. Bu yansıtılan dizinlere "bağlama eklemeleri" adı verilir.
build.sh
konusunu inceleyelim:
#!/bin/bash
set -e
export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CXXFLAGS="${OPTIMIZE}"
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
src/my-module.cpp
# Create output folder
mkdir -p dist
# Move artifacts
mv my-module.{js,wasm} dist
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="
Burada keşfedilecek çok şey var.
set -e
, kabuğu "hata hızlı" moduna alır. Komut dosyasındaki herhangi bir komut hata döndürürse tüm komut dosyası hemen iptal edilir. Komut dosyasının son çıkışı her zaman bir başarı mesajı veya derlemenin başarısız olmasına neden olan hata olacağı için bu son derece faydalı olabilir.
export
ifadeleriyle, birkaç ortam değişkeninin değerini tanımlarsınız. Bunlar; C derleyiciye (CFLAGS
), C++ derleyiciye (CXXFLAGS
) ve bağlayıcıya (LDFLAGS
) ek komut satırı parametreleri iletmenizi sağlar.
Her şeyin aynı şekilde optimize edildiğinden emin olmak için tüm bu araçlar, optimize edici ayarlarını OPTIMIZE
aracılığıyla alır. OPTIMIZE
değişkeni için birkaç olası değer vardır:
-O0
: Herhangi bir optimizasyon yapma. Ölü kod ortadan kaldırılmaz ve Emscripten, yaydığı JavaScript kodunu küçültmez. Hata ayıklama için idealdir.-O3
: Performans için agresif optimizasyon yapın.-Os
: İkincil kriter olarak performans ve boyut için yüksek düzeyde optimizasyon yapın.-Oz
: Boyut için agresif şekilde optimizasyon yaparak gerekirse performanstan ödün verin.
Web için çoğunlukla -Os
kullanmanızı öneririm.
emcc
komutunun kendine ait pek çok seçeneği vardır. emcc'nin "GCC veya clang gibi derleyiciler için açılır yedek" olarak kabul edildiğini unutmayın. Dolayısıyla, GCC'den bildiğiniz tüm işaretler büyük olasılıkla emcc tarafından da uygulanır. -s
işareti, Emscripten'ı özel olarak yapılandırmamıza olanak tanıması açısından özeldir. Mevcut tüm seçenekleri Emscripten'in settings.js
dosyasında bulabilirsiniz, ancak bu dosya oldukça zorlayıcı olabilir. Web geliştiricileri için en önemli olduğunu düşündüğüm Emscripten işaretlerinin bir listesini aşağıda bulabilirsiniz:
--bind
, embind'i etkinleştirir.-s STRICT=1
, kullanımdan kaldırılan tüm derleme seçenekleri için desteği keser. Bu, kodunuzun ileriye dönük bir şekilde derlenmesini sağlar.-s ALLOW_MEMORY_GROWTH=1
, gerektiğinde belleğin otomatik olarak artırılmasına izin verir. Bu yazmanın yazıldığı sırada Emscripten başlangıçta 16 MB bellek ayırmıştır. Kodunuz bellek parçalarını ayırdıkça bu seçenek, bu işlemlerin bellek tükendiğinde wasm modülünün tamamının başarısız olmasına neden olup olmayacağına veya yapışkan kodunun, ayırmaya uyum sağlamak için toplam belleği genişletmesine izin verilip verilmeyeceğini belirler.-s MALLOC=...
, hangimalloc()
uygulamasının kullanılacağını seçer.emmalloc
, özellikle Emscripten için kullanılan küçük ve hızlı birmalloc()
uygulamasıdır. Alternatifi, tam kapsamlı birmalloc()
uygulaması olandlmalloc
'tır. Yalnızca çok sayıda küçük nesneyi sık sık ayırıyorsanız veya iş parçacığı kullanmak istiyorsanızdlmalloc
öğesine geçiş yapmanız gerekir.-s EXPORT_ES6=1
, JavaScript kodunu tüm paketleyicilerle çalışan varsayılan bir dışa aktarma işlemiyle bir ES6 modülüne dönüştürür. Ayrıca-s MODULARIZE=1
ayarlanmasını da gerektirir.
Aşağıdaki işaretler her zaman gerekli değildir veya yalnızca hata ayıklama amaçları için faydalıdır:
-s FILESYSTEM=0
, Emscripten ile ilgili bir işarettir ve C/C++ kodunuz dosya sistemi işlemlerini kullandığında sizin için bir dosya sistemini emüle edebilme yeteneğine sahiptir. Dosya sistemi emülasyonunun yapıştırıcı koduna eklenip eklenmeyeceğine karar vermek için derlediği kod üzerinde bazı analizler yapar. Ancak bazen bu analizde hata oluşabilir ve ihtiyacınız olmayabilecek bir dosya sistemi emülasyonu için oldukça yüksek miktarda 70 KB ek yapıştırıcı kodu ödersiniz.-s FILESYSTEM=0
ile Emscripten'ı bu kodu eklememeye zorlayabilirsiniz.-g4
, Emscripten'ın.wasm
bölümüne hata ayıklama bilgileri eklemesini sağlar ve ayrıca wasm modülü için bir kaynak eşleme dosyası oluşturur. Emscripten ile hata ayıklama hakkında daha fazla bilgiyi hata ayıklama bölümlerinde bulabilirsiniz.
İşlem tamam! Bu kurulumu test etmek için küçük bir my-module.cpp
oluşturalım:
#include <emscripten/bind.h>
using namespace emscripten;
int say_hello() {
printf("Hello from your wasm module\n");
return 0;
}
EMSCRIPTEN_BINDINGS(my_module) {
function("sayHello", &say_hello);
}
Bir de index.html
:
<!doctype html>
<title>Emscripten + npm example</title>
Open the console to see the output from the wasm module.
<script type="module">
import wasmModule from "./my-module.js";
const instance = wasmModule({
onRuntimeInitialized() {
instance.sayHello();
}
});
</script>
(Tüm dosyaları içeren gist belgesini burada bulabilirsiniz.)
Her şeyi derlemek için
$ npm install
$ npm run build
$ npm run serve
localhost:8080'e gittiğinizde Geliştirici Araçları konsolunda şu çıkışı göstermelisiniz:
C/C++ kodunu bağımlılık olarak ekleme
Web uygulamanız için bir C/C++ kitaplığı oluşturmak istiyorsanız ilgili kodun, projenizin bir parçası olması gerekir. Kodu projenizin deposuna manuel olarak ekleyebilir veya bu tür bağımlılıkları yönetmek için npm'yi de kullanabilirsiniz. Web uygulamamda libvpx kullanmak istediğimi düşünelim. libvpx, resimleri .webm
dosyalarında kullanılan codec olan VP8 ile kodlamak için bir C++ kitaplığıdır.
Ancak, libvpx npm'de değil ve bir package.json
öğesine sahip değil. Bu yüzden doğrudan npm kullanarak yükleyemiyorum.
Bu bilmeceden çıkmak için napa aracı vardır. napa, herhangi bir git kod deposu URL'sini node_modules
klasörünüze bağımlılık olarak yüklemenize olanak tanır.
Napa'yı bağımlılık olarak yükleyin:
$ npm install --save napa
ve napa
dosyasını yükleme komut dosyası olarak çalıştırdığınızdan emin olun:
{
// ...
"scripts": {
"install": "napa",
// ...
},
"napa": {
"libvpx": "git+https://github.com/webmproject/libvpx"
}
// ...
}
npm install
çalıştırdığınızda napa, libvpx GitHub deposunu node_modules
cihazınıza libvpx
adı altında klonlama işlemini üstlenir.
Artık libvpx derlemek için derleme komut dosyanızı genişletebilirsiniz. libvpx derlemek için configure
ve make
kullanır. Neyse ki Emscripten, configure
ve make
uygulamalarının Emscripten'in derleyicisini kullanmasına yardımcı olabilir. Bunun için emconfigure
ve emmake
sarmalayıcı komutları kullanılır:
# ... above is unchanged ...
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
(
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
# ... below is unchanged ...
Bir C/C++ kitaplığı iki bölüme ayrılır: bir kitaplığın sunduğu veri yapılarını, sınıfları, sabit değerleri vb. tanımlayan başlıklar (geleneksel olarak .h
veya .hpp
dosyaları) ve gerçek kitaplık (geleneksel olarak .so
veya .a
dosyaları). Kodunuzda kitaplığın VPX_CODEC_ABI_VERSION
sabitini kullanmak için kitaplığın başlık dosyalarını #include
ifadesi kullanarak eklemeniz gerekir:
#include "vpxenc.h"
#include <emscripten/bind.h>
int say_hello() {
printf("Hello from your wasm module with libvpx %d\n", VPX_CODEC_ABI_VERSION);
return 0;
}
Sorun, derleyicinin vpxenc.h
öğesini nerede arayacağını bilmemesidir.
-I
işaretinin amacı da budur. Derleyiciye başlık dosyaları için hangi dizinlerin kontrol edileceğini söyler. Ayrıca, derleyiciye gerçek kitaplık dosyasını da vermeniz gerekir:
# ... above is unchanged ...
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s ASSERTIONS=0 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
-I ./node_modules/libvpx \
src/my-module.cpp \
build-vpx/libvpx.a
# ... below is unchanged ...
npm run build
uygulamasını şimdi çalıştırırsanız işlemin yeni bir .js
ve yeni bir .wasm
dosyası oluşturduğunu ve demo sayfasının gerçekten sabit değer oluşturduğunu görürsünüz:
Ayrıca, derleme işleminin uzun sürdüğünü de fark edeceksiniz. Uzun derleme sürelerinin nedeni değişiklik gösterebilir. libvpx'te bu işlem uzun sürer. Çünkü kaynak dosyalar değişmese de derleme komutunuzu her çalıştırdığınızda hem VP8 hem de VP9 için bir kodlayıcı ve kod çözücü derlenir. my-module.cpp
üzerinde yapılacak küçük bir değişikliğin bile oluşturulması uzun zaman alır. libvpx'in derleme yapılarını ilk defa derlendikten sonra tutmak çok faydalı olur.
Bunu başarmanın yollarından biri ortam değişkenlerini kullanmaktır.
# ... above is unchanged ...
eval $@
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
test -n "$SKIP_LIBVPX" || (
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
# ... below is unchanged ...
(Tüm dosyaları içeren gist burada bulabilirsiniz.)
eval
komutu, parametreleri derleme komut dosyasına ileterek ortam değişkenlerini ayarlamamıza olanak tanır. $SKIP_LIBVPX
ayarlanmışsa test
komutu, libvpx oluşturma işlemini atlar (herhangi bir değere).
Artık modülünüzü derleyebilirsiniz ancak libvpx'i yeniden oluşturma adımını atlayabilirsiniz:
$ npm run build:emscripten -- SKIP_LIBVPX=1
Derleme ortamını özelleştirme
Bazen kitaplıklar oluşturmak için ek araçlara ihtiyaç duyar. Docker görüntüsü tarafından sağlanan derleme ortamında bu bağımlılıklar yoksa bunları kendiniz eklemeniz gerekir. Örneğin, doxygen kullanarak libvpx belgelerini de oluşturmak istediğinizi varsayalım. Doxygen, Docker container'ınızın içinde bulunmaz ancak apt
aracılığıyla yükleyebilirsiniz.
build.sh
uygulamanızda bunu yaparsanız, kitaplığınızı her oluşturmak istediğinizde doxygen'i yeniden indirip yeniden yüklemeniz gerekir. Bu hem israfa yol açar hem de çevrimdışıyken projeniz üzerinde çalışmanızı engeller.
Burada kendi Docker görüntünüzü oluşturmanız mantıklıdır. Docker görüntüleri, derleme adımlarını açıklayan bir Dockerfile
yazılarak oluşturulur. Docker dosyaları oldukça güçlüdür ve çok sayıda komuta sahiptir. Ancak çoğu zaman FROM
, RUN
ve ADD
kullanarak işin altından kalkabilirsiniz. Bu durumda:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen
FROM
ile hangi Docker görüntüsünü başlangıç noktası olarak kullanmak istediğinizi belirtebilirsiniz. Temel olarak trzeci/emscripten
seçeneğini tercih ettim. En başından beri kullandığınız resim. RUN
ile Docker'a container içinde kabuk komutları çalıştırma talimatı verirsiniz. Bu komutların container'da yaptığı değişiklikler artık Docker görüntüsünün bir parçasıdır. Docker görüntünüzün derlendiğinden ve build.sh
çalıştırmadan önce kullanılabilir olduğundan emin olmak için package.json
üzerinde biraz ayarlama yapmanız gerekir:
{
// ...
"scripts": {
"build:dockerimage": "docker image inspect -f '.' mydockerimage || docker build -t mydockerimage .",
"build:emscripten": "docker run --rm -v $(pwd):/src mydockerimage ./build.sh",
"build": "npm run build:dockerimage && npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
(Tüm dosyaları içeren gist burada bulabilirsiniz.)
Bu işlem, henüz oluşturulmamışsa Docker görüntünüzü oluşturur. Daha sonra her şey eskisi gibi çalışır, ancak artık derleme ortamında doxygen
komutu kullanılabilir. Bu da libvpx belgelerinin de derlenmesine neden olur.
Sonuç
C/C++ kodu ve npm'nin doğal olarak uygun olmaması şaşırtıcı değildir ancak bazı ek araçlar ve Docker'ın sağladığı izolasyon sayesinde bunların oldukça rahat çalışmasını sağlayabilirsiniz. Bu kurulum her proje için işe yaramayabilir ancak ihtiyaçlarınıza göre ayarlayabileceğiniz iyi bir başlangıç noktasıdır. İyileştirmeleriniz varsa lütfen paylaşın.
Ek: Docker görüntü katmanlarından yararlanma
Alternatif bir çözüm, Docker ve Docker'ın önbelleğe alma konusundaki akıllı yaklaşımı sayesinde bu sorunların daha fazlasını kapsüllemektir. Docker, Dockerfile'ları adım adım yürütür ve her adımın sonucuna kendi görüntüsünü atar. Bu ara resimlere genellikle "katmanlar" denir. Dockerfile'daki bir komut değişmediyse Dockerfile'ı yeniden oluştururken siz bu adımı yeniden çalıştırmaz. Bunun yerine, resmin son oluşturulduğu sıradaki katmanı yeniden kullanır.
Önceden, uygulamanızı her derlediğinizde libvpx'i yeniden oluşturmamak için biraz çaba sarf etmeniz gerekiyordu. Bunun yerine, Docker'ın önbelleğe alma mekanizmasından yararlanmak için libvpx'e yönelik derleme talimatlarını build.sh
cihazınızdan Dockerfile
içine taşıyabilirsiniz:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen git && \
mkdir -p /opt/libvpx/build && \
git clone https://github.com/webmproject/libvpx /opt/libvpx/src
RUN cd /opt/libvpx/build && \
emconfigure ../src/configure --target=generic-gnu && \
emmake make
(Tüm dosyaları içeren gist burada bulabilirsiniz.)
docker build
çalıştırırken bağlama ekleme işlemleriniz olmadığından git ve clone libvpx'i manuel olarak yüklemeniz gerektiğini unutmayın. Yan etki olarak, artık
napa gerekmiyor.