WebAssembly'yi bu kuruluma nasıl entegre edersiniz? Bu makalede, örnek olarak C/C++ ve Emscripten ile bu durumu ele alacağız.
WebAssembly (wasm) genellikle bir performans öğesi veya mevcut C++ kod tabanınızı web'de çalıştırmanın bir yolu olarak tanımlanır. squoosh.app ile wasm için en azından üçüncü bir bakış açısı olduğunu göstermek istedik: diğer programlama dillerinin büyük ekosistemlerinden yararlanmak. Emscripten ile C/C++ kodu kullanabilirsiniz. Rust'ta wasm desteği yerleşik olarak bulunur ve Go ekibi de bu destek üzerinde çalışmaktadır. Diğer dillerin de yakında ekleneceğinden eminim.
Bu senaryolarda wasm, uygulamanızın merkezinde yer almaz. Bunun yerine, başka bir modül olan bir yapboz parçasıdır. Uygulamanızda zaten JavaScript, CSS, resim öğeleri, web odaklı bir derleme sistemi ve hatta React gibi bir çerçeve var. WebAssembly'yi bu kuruluma nasıl entegre edersiniz? Bu makalede, örnek olarak C/C++ ve Emscripten ile bu konuyu ele alacağız.
Docker
Emscripten ile çalışırken Docker'ın çok değerli olduğunu gördüm. C/C++ kitaplıkları genellikle üzerinde oluşturuldukları işletim sistemiyle çalışacak şekilde yazılır. Tutarlı bir ortamın olması son derece faydalıdır. Docker ile Emscripten ile çalışmaya hazır, tüm araçların ve bağımlılıkların yüklendiği sanallaştırılmış bir Linux sistemi elde edersiniz. Eksik bir şey varsa kendi makinenizi veya diğer projelerinizi nasıl etkileyeceği konusunda endişelenmenize gerek kalmadan yükleyebilirsiniz. Bir sorun olursa kabı atın ve baştan başlayın. Bir kez çalışırsa çalışmaya devam edeceğinden ve aynı sonuçları vereceğinden emin olabilirsiniz.
Docker Registry'de trzeci tarafından oluşturulan ve yoğun olarak kullandığım bir Emscripten görüntüsü var.
npm ile entegrasyon
Çoğu durumda, bir web projesine giriş noktası npm'nin package.json
olur. Çoğu proje, kural olarak npm install &&
npm run build
ile oluşturulabilir.
Genel olarak, Emscripten tarafından üretilen derleme yapıları (bir .js
ve bir .wasm
dosyası) yalnızca başka bir JavaScript modülü ve başka bir öğe olarak değerlendirilmelidir. JavaScript dosyası, webpack veya rollup gibi bir paketleyici tarafından işlenebilir. Wasm dosyası ise resimler gibi diğer büyük ikili öğeler gibi ele alınmalıdır.
Bu nedenle, "normal" derleme süreciniz başlamadan önce Emscripten derleme yapılarını oluşturmanız 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 doğrudan Emscripten'i çağırabilir ancak daha önce de belirtildiği gibi, derleme ortamının tutarlı olduğundan emin olmak için Docker'ı kullanmanızı öneririz.
docker run ... trzeci/emscripten ./build.sh
, Docker'a trzeci/emscripten
görüntüsünü kullanarak yeni bir container oluşturmasını ve ./build.sh
komutunu çalıştırmasını söyler.
build.sh
, bir sonraki adımda yazacağınız bir kabuk komut dosyasıdır. --rm
, Docker'a kapsayıcıyı çalışmayı tamamladıktan sonra silmesini söyler. Bu sayede, zaman içinde eski makine görüntülerinden oluşan bir koleksiyon oluşturmazsınız. -v $(pwd):/src
, Docker'ın geçerli dizini ($(pwd)
) kapsayıcı içindeki /src
dizinine "yansıtmasını" istediğiniz anlamına gelir. Kapsayıcının içindeki /src
dizinindeki dosyalarda yaptığınız tüm değişiklikler gerçek projenize yansıtılır. Bu yansıtılmış dizinlere "bağlama noktaları" denir.
Şimdi build.sh
'a göz atalım:
#!/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 incelenecek çok şey var.
set -e
, kabuğu "hızlı hata" moduna geçirir. Komut dosyasındaki komutlardan herhangi biri hata döndürürse komut dosyasının tamamı hemen durdurulur. Bu, komut dosyasının son çıktısı her zaman başarılı bir mesaj veya derlemenin başarısız olmasına neden olan hata olacağından son derece faydalı olabilir.
export
ifadeleriyle birkaç ortam değişkeninin değerlerini tanımlarsınız. Bunlar, C derleyicisine (CFLAGS
), C++ derleyicisine (CXXFLAGS
) ve bağlayıcıya (LDFLAGS
) ek komut satırı parametreleri iletmenize olanak tanır.
Her şeyin aynı şekilde optimize edildiğinden emin olmak için tümü OPTIMIZE
üzerinden optimize edici ayarlarını alır. OPTIMIZE
değişkeni için olası birkaç değer vardır:
-O0
: Optimizasyon yapmayın. Ölü kodlar ortadan kaldırılmaz ve Emscripten, oluşturduğu JavaScript kodunu da küçültmez. Hata ayıklama için uygundur.-O3
: Performans için agresif bir şekilde optimizasyon yapın.-Os
: İkincil ölçüt olarak performans ve boyut için agresif bir şekilde optimize edin.-Oz
: Gerekirse performanstan ödün vererek boyut için agresif bir şekilde optimize edin.
Web için çoğunlukla -Os
'ı öneririm.
emcc
komutunun kendi içinde çok sayıda seçeneği vardır. emcc'nin "GCC veya clang gibi derleyicilerin yerine kullanılabilen bir derleyici" olduğunu unutmayın. Bu nedenle, GCC'den bildiğiniz tüm işaretler büyük olasılıkla emcc tarafından da uygulanır. -s
işareti, Emscripten'i özel olarak yapılandırmamıza olanak tanıdığı için özeldir. Mevcut tüm seçenekleri Emscripten'in settings.js
dosyasında bulabilirsiniz ancak bu dosya oldukça karmaşık olabilir. Web geliştiriciler için en önemli olduğunu düşündüğüm Emscripten işaretlerinin listesini aşağıda bulabilirsiniz:
--bind
enables embind.-s STRICT=1
, kullanımdan kaldırılan tüm derleme seçenekleri için desteği sonlandırır. Bu sayede kodunuzun ileriye dönük uyumlu bir şekilde oluşturulması sağlanır.-s ALLOW_MEMORY_GROWTH=1
gerektiğinde belleğin otomatik olarak genişletilmesine olanak tanır. Bu makalenin yazıldığı sırada Emscripten, başlangıçta 16 MB bellek ayırır. Kodunuz bellek parçaları ayırırken bu seçenek, bellek tükendiğinde bu işlemlerin tüm wasm modülünün başarısız olmasına neden olup olmayacağına veya yapıştırma kodunun, ayırmayı karşılamak için toplam belleği genişletmesine izin verilip verilmeyeceğine karar verir.-s MALLOC=...
hangimalloc()
uygulamasının kullanılacağını seçer.emmalloc
, özellikle Emscripten için küçük ve hızlı birmalloc()
uygulamasıdır. Alternatif olarak, tam teşekküllü birmalloc()
uygulaması olandlmalloc
kullanılabilir. Yalnızca çok sayıda küçük nesneyi sık sık ayırıyorsanız veya iş parçacığı kullanmak istiyorsanızdlmalloc
'a geçmeniz gerekir.-s EXPORT_ES6=1
, JavaScript kodunu herhangi bir paketleyiciyle çalışan varsayılan dışa aktarma içeren bir ES6 modülüne dönüştürür.-s MODULARIZE=1
ayarının da yapılması gerekir.
Aşağıdaki işaretler her zaman gerekli değildir veya yalnızca hata ayıklama amacıyla faydalıdır:
-s FILESYSTEM=0
, Emscripten ile ilgili bir işaret olup C/C++ kodunuz dosya sistemi işlemlerini kullandığında sizin için bir dosya sistemini taklit etme özelliğini ifade eder. Dosya sistemi emülasyonunun yapıştırma koduna dahil edilip edilmeyeceğine karar vermek için derlediği kod üzerinde bazı analizler yapar. Ancak bazen bu analiz yanlış sonuç verebilir ve ihtiyacınız olmayabilecek bir dosya sistemi emülasyonu için 70 kB'lık ek yapıştırma kodu ödemeniz gerekebilir.-s FILESYSTEM=0
ile Emscripten'in bu kodu dahil etmemesini sağlayabilirsiniz.-g4
, Emscripten'in.wasm
dosyasına hata ayıklama bilgilerini eklemesini ve wasm modülü için bir kaynak haritaları dosyası oluşturmasını sağlar. Emscripten ile hata ayıklama hakkında daha fazla bilgiyi hata ayıklama bölümünde 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);
}
Ayrıca 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 bir gist burada.)
Her şeyi oluşturmak için şu komutu çalıştırın:
$ npm install
$ npm run build
$ npm run serve
localhost:8080'e gittiğinizde, DevTools konsolunda aşağıdaki çıkış gösterilir:

Bağımlılık olarak C/C++ kodu ekleme
Web uygulamanız için bir C/C++ kitaplığı oluşturmak istiyorsanız kodunun 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 kullanabilirsiniz. Web uygulamamda libvpx kullanmak istediğimi varsayalım. libvpx, .webm
dosyalarında kullanılan VP8 kodekiyle görüntüleri kodlamak için kullanılan bir C++ kitaplığıdır.
Ancak libvpx, npm'de bulunmuyor ve package.json
dosyası yok. Bu nedenle, npm kullanarak doğrudan yükleyemiyorum.
Bu sorundan kurtulmak için napa'yı kullanabilirsiniz. Napa, herhangi bir Git 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
komutunu çalıştırdığınızda napa, libvpx GitHub deposunu libvpx
adıyla node_modules
dizininize klonlar.
Artık libvpx'i oluşturmak için derleme komut dosyanızı genişletebilirsiniz. libvpx, oluşturulmak için configure
ve make
kullanır. Neyse ki Emscripten, configure
ve make
öğelerinin Emscripten'in derleyicisini kullanmasını sağlayabilir. Bu amaçla 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: Kitaplığın sunduğu veri yapılarını, sınıfları, sabitleri vb. tanımlayan başlıklar (geleneksel olarak .h
veya .hpp
dosyaları) ve gerçek kitaplık (geleneksel olarak .so
veya .a
dosyaları). Kitaplığın VPX_CODEC_ABI_VERSION
sabitini kodunuzda kullanmak için #include
ifadesini kullanarak kitaplığın üstbilgi dosyalarını 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ı budur. Derleyiciye, üstbilgi dosyaları için hangi dizinlerin kontrol edileceğini bildirir. 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
komutunu şimdi çalıştırırsanız işlemin yeni bir .js
ve yeni bir .wasm
dosyası oluşturduğunu, demo sayfasının da gerçekten sabiti çıkardığını görürsünüz:

Ayrıca derleme işleminin uzun sürdüğünü de fark edeceksiniz. Uzun derleme sürelerinin nedeni değişebilir. libvpx söz konusu olduğunda, kaynak dosyalar değişmemiş olsa bile derleme komutunuzu her çalıştırdığınızda hem VP8 hem de VP9 için bir kodlayıcı ve kod çözücü derlediğinden bu işlem uzun zaman alır. my-module.cpp
ile ilgili küçük bir değişiklik bile uzun sürede oluşturulur. libvpx'in derleme yapılarını ilk kez derlendikten sonra saklamak çok faydalı olacaktır.
Bunu başarmanın bir yolu 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 bir gist burada.)
eval
komutu, derleme komut dosyasına parametreler ileterek ortam değişkenlerini ayarlamamıza olanak tanır. test
komutu, $SKIP_LIBVPX
ayarlanmışsa (herhangi bir değere) libvpx'in oluşturulmasını atlar.
Artık modülünüzü derleyebilirsiniz ancak libvpx'i yeniden oluşturmayı atlayabilirsiniz:
$ npm run build:emscripten -- SKIP_LIBVPX=1
Derleme ortamını özelleştirme
Bazen kitaplıklar oluşturulmak için ek araçlara ihtiyaç duyar. Bu bağımlılıklar, Docker görüntüsü tarafından sağlanan derleme ortamında eksikse bunları kendiniz eklemeniz gerekir. Örneğin, doxygen kullanarak libvpx'in dokümanlarını da oluşturmak istediğinizi varsayalım. Doxygen, Docker kapsayıcınızda kullanılamaz ancak apt
kullanarak yükleyebilirsiniz.
Bunu build.sh
içinde yaparsanız kitaplığınızı oluşturmak istediğiniz her seferde doxygen'i yeniden indirip yeniden yüklemeniz gerekir. Bu yalnızca israf olmaz, aynı zamanda çevrimdışıyken projeniz üzerinde çalışmanızı da engeller.
Bu durumda kendi Docker görüntünüzü oluşturmanız mantıklı olacaktı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 birçok komuta sahiptir ancak çoğu zaman yalnızca FROM
, RUN
ve ADD
komutlarını kullanarak işinizi halledebilirsiniz. Bu durumda:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen
FROM
ile başlangıç noktası olarak kullanmak istediğiniz Docker görüntüsünü belirtebilirsiniz. Temel olarak trzeci/emscripten
resmini seçtim. Bu, kullandığınız resim. RUN
ile Docker'a container'ın içinde kabuk komutları çalıştırmasını söylersiniz. Bu komutların kapsayıcıda yaptığı değişiklikler artık Docker görüntüsünün bir parçasıdır. build.sh
komutunu çalıştırmadan önce Docker görüntünüzün oluşturulduğundan ve kullanılabilir olduğundan emin olmak için package.json
komutunuzu biraz değiştirmeniz 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 bir gist burada.)
Bu işlem, Docker görüntünüzü oluşturur ancak yalnızca henüz oluşturulmamışsa. Ardından her şey eskisi gibi çalışır ancak artık derleme ortamında doxygen
komutu kullanılabilir. Bu komut, libvpx belgelerinin de oluşturulmasına neden olur.
Sonuç
C/C++ kodu ve npm'nin doğal olarak uyumlu olmaması şaşırtıcı değildir ancak Docker'ın sağladığı izolasyon ve bazı ek araçlarla bu ikiliyi oldukça rahat bir şekilde kullanabilirsiniz. Bu kurulum her proje için uygun olmayabilir ancak ihtiyaçlarınıza göre ayarlayabileceğiniz iyi bir başlangıç noktasıdır. İyileştirme önerileriniz varsa lütfen paylaşın.
Ek: Docker görüntü katmanlarından yararlanma
Alternatif bir çözüm olarak, bu sorunların daha fazlasını Docker ve Docker'ın akıllı önbelleğe alma yaklaşımıyla kapsayabilirsiniz. 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 görüntülere genellikle "katmanlar" denir. Dockerfile'daki bir komut değişmediyse Docker, Dockerfile'ı yeniden oluşturduğunuzda bu adımı aslında yeniden çalıştırmaz. Bunun yerine, resmin son oluşturulma zamanındaki katmanı yeniden kullanır.
Daha önce, uygulamanızı her oluşturduğunuzda libvpx'i yeniden oluşturmamak için biraz çaba göstermeniz gerekiyordu. Bunun yerine, Docker'ın önbelleğe alma mekanizmasından yararlanmak için libvpx'in oluşturma talimatlarını build.sh
dosyanızdan Dockerfile
dosyasına 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 bir gist burada.)
docker build
çalıştırılırken bağlama montajları olmadığından git'i manuel olarak yüklemeniz ve libvpx'i klonlamanız gerektiğini unutmayın. Yan etki olarak, artık napa'ya gerek yoktur.