WebAssembly'i bu kuruluma nasıl entegre edersiniz? Bu makalede, örnek olarak C/C++ ve Emscripten'i kullanarak bu işlemi gerçekleştireceğiz.
WebAssembly (wasm), genellikle bir performans ilkelliği veya mevcut C++ kod tabanınızı web'de çalıştırmanın bir yolu olarak sunulur. squoosh.app ile, wasm için en azından üçüncü bir bakış açısı olduğunu göstermek istedik: diğer programlama dillerinin dev ekosistemlerinden yararlanma. Emscripten sayesinde, C/C++ kodunu ve yerleşik Rust wasm desteği içerir. Go ekibi de bunun üzerinde çalışmaktadır. Diğer dillerin de ekleneceğinden eminiz.
Bu senaryolarda wasm, uygulamanızın ana unsuru değil, bir bulmaca parçasıdır: başka bir modüldür. Uygulamanızda zaten JavaScript, CSS, görüntü öğeleri, web merkezli bir derleme sistemi ve belki React gibi bir çerçeve var. WebAssembly'yi bu kuruluma nasıl entegre edersiniz? Bu makalede, bunu örnek olarak C/C++ ve Emscripten ile birlikte inceleyeceğiz.
Docker
Emscripten ile çalışırken Docker'ın çok faydalı olduğunu gördüm. C/C++ kitaplıkları genellikle üzerine inşa edildikleri işletim sistemiyle çalışacak şekilde yazılır. Tutarlı bir ortama sahip olmak inanılmaz derecede faydalı. Docker sayesinde, Emscripten ile çalışacak şekilde ayarlanmış, tüm araçlar ve bağımlılıklar yüklü olan sanal bir Linux sistemine sahip olursunuz. Eksik bir şey varsa bunu kendi makinenizi ya da diğer projelerinizi nasıl etkileyeceği konusunda endişelenmenize gerek kalmadan yükleyebilirsiniz. Bir şeyler ters giderse kabı atın ve işleme baştan başlayın. Bir kez işe yaradıysa çalışmaya devam edeceğinden ve aynı sonuçları vereceğinden emin olabilirsiniz.
Docker Registry, trzeci tarafından yoğun şekilde kullandığım bir Emscripten görüntüsü içeriyor.
npm ile entegrasyon
Çoğu durumda, bir web projesine giriş noktası npm'dir (package.json
). Kural olarak, çoğu proje npm install &&
npm run build
ile derlenebilir.
Genel olarak, Emscripten tarafından üretilen derleme yapıları (.js
ve .wasm
dosyası) başka bir JavaScript modülü ve başka bir öğe olarak değerlendirilmelidir. JavaScript dosyası, webpack veya toplayıcı gibi bir paketleyici tarafından işlenebilir. Wasm dosyası, resimler gibi daha büyük bir ikili program gibi ele alınmalıdır.
Bu nedenle, Emscripten derleme yapılarının "normal" derleme süreciniz başlamadan önce 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 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ı öneririm.
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
, bundan sonra yazacağınız bir kabuk komut dosyasıdır. --rm
, Docker'a çalışmayı tamamladıktan sonra kapsayıcıyı silmesini söyler. Bu sayede, zaman içinde eski makine
görüntülerinden oluşan bir koleksiyon oluşturamazsınız. -v $(pwd):/src
, Docker'ın mevcut dizini ($(pwd)
) kapsayıcı içindeki /src
dizine "yansıtmasını" istediğiniz anlamına gelir. Kapsayıcı içindeki /src
dizininde dosyalarda yaptığınız tüm değişiklikler gerçek projenize yansıtılır. Bu yansıtılmış dizinlere "bağlantı bağlama" denir.
build.sh
ürününe 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 çok fazla analiz edilecek konu var.
set -e
, kabuğu "başarısız" moduna geçirir. Komut dosyasındaki herhangi bir komut hata döndürürse komut dosyasının tamamı 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ğından bu, son derece yararlı olabilir.
export
ifadeleriyle birkaç ortam değişkeninin değerlerini tanımlarsınız. Bu değişkenler, ek komut satırı parametrelerini C derleyiciye (CFLAGS
), C++ derleyicisine (CXXFLAGS
) ve bağlayıcıya (LDFLAGS
) iletmenizi sağlar. Her şeyin aynı şekilde optimize edilmesini sağlamak için hepsi optimize edici ayarlarını OPTIMIZE
üzerinden alır. OPTIMIZE
değişkeni için birkaç olası değer vardır:
-O0
: Hiçbir optimizasyon yapmaz. Kullanılmayan kod kaldırılmaz ve Emscripten, oluşturduğu JavaScript kodunu 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 optimizasyon yapın.-Oz
: Boyut için agresif bir şekilde optimizasyon yapar, gerekirse performanstan ödün verir.
Web için çoğunlukla -Os
öneririm.
emcc
komutunun kendine özgü birçok seçeneği vardır. emcc'nin "GCC veya clang gibi derleyiciler için alternatif olarak sunulan bir seçenek" olduğunu 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'i özel olarak yapılandırmamıza olanak tanıması açısından özeldir. Kullanılabilir tüm seçenekleri Emscripten'in settings.js
dosyasında bulabilirsiniz ancak bu dosya oldukça karmaşık olabilir. Aşağıda web geliştiricileri için en önemli olduğunu
düşündüğüm Emscripten işaretlerinin bir listesini bulabilirsiniz:
--bind
, embind özelliğini etkinleştirir.-s STRICT=1
, desteği sonlandırılan tüm derleme seçenekleri için desteği sonlandırıyor. 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 makalenin yazıldığı sırada Emscripten başlangıçta 16 MB bellek ayırır. Kodunuz bellek parçaları ayarken bu işlemlerin, bellek tükendiğinde wasm modülünün tamamının başarısız olmasına yol açıp açmayacağına veya yapıştırıcı kodunun, ayırmayı karşılamak için toplam belleği genişletmesine izin verilip verilmeyeceğine bu seçenek 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, tam kapsamlı birmalloc()
uygulaması olandlmalloc
'dir.dlmalloc
öğesine yalnızca sık sık çok sayıda küçük nesne ayırıyorsanız veya iş parçacığı kullanmak istiyorsanız geçiş yapmanız gerekir.-s EXPORT_ES6=1
, JavaScript kodunu herhangi bir paketleyiciyle çalışan varsayılan bir dışa aktarma yöntemine sahip bir ES6 modülüne dönüştürür. Ayrıca-s MODULARIZE=1
değerinin ayarlanması gerekir.
Aşağıdaki işaretler her zaman gerekli değildir veya yalnızca hata ayıklama amacıyla faydalı olur:
-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 etme özelliğidir. Dosya sistemi emülasyonunun yapışkan koda dahil edilip edilmeyeceğini belirlemek için derlediği kodda 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 oldukça büyük bir ek yapıştırıcı kod ödemeniz gerekebilir.-s FILESYSTEM=0
ile Emscripten'i bu kodu dahil etmemeye zorlayabilirsiniz.-g4
, Emscripten'in.wasm
içinde hata ayıklama bilgilerini içermesini sağlar ve wasm modülü için bir kaynak eşleme dosyası yayınlar. 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 çok 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 özet dosyasını burada bulabilirsiniz.)
Her şeyi derlemek için
$ npm install
$ npm run build
$ npm run serve
localhost:8080 adresine gittiğinizde Geliştirici Araçları konsolunda aşağıdaki çıkışı görürsünüz:
Bağımlılık olarak C/C++ kodu ekleme
Web uygulamanız için C/C++ kitaplığı oluşturmak istiyorsanız bu kitaplığın 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 kullanabilirsiniz. Diyelim ki web uygulamamda libvpx'i kullanmak istiyorum. libvpx, .webm
dosyalarında kullanılan codec'i olan VP8 ile resimleri kodlamak için bir C++ kitaplığıdır.
Ancak libvpx, npm'de bulunmadığı ve package.json
içermediği için doğrudan npm kullanarak yükleyemiyorum.
Bu çıkmazdan çıkmak için napa vardır. napa, herhangi bir git deposu URL'sini node_modules
klasörünüze bağımlılık olarak yüklemenize olanak tanır.
Bağımlılık olarak napa'yı 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 node_modules
cihazınıza libvpx
adı altında klonlama işlemini gerçekleştirir.
Derleme komut dosyanızı artık libvpx'i derleyecek şekilde genişletebilirsiniz. libvpx derleme için configure
ve make
kullanır. Neyse ki Emscripten, configure
ve make
uygulamalarının Emscripten'in derleyicisini kullanmasını sağlayabilir. Bu amaçla emconfigure
ve emmake
sarmalayıcı komutları vardı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 ...
C/C++ kitaplığı iki bölüme ayrılır: Bir 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ı). Kodunuzda kitaplığın VPX_CODEC_ABI_VERSION
sabitini kullanmak için kitaplığın başlık dosyalarını #include
ifadesiyle dahil etmeniz 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şareti bunun içindir. Derleyiciye başlık dosyalarını hangi dizinlerin
kontrol edeceğ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
öğesini ş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 üreteceğini 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 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ü derlendiğinden bu işlem uzun sürer. my-module.cpp
üzerinde yapacağınız küçük bir değişikliğin bile uygulanması uzun sürer. libvpx derleme yapılarını ilk kez oluşturuldukları andan itibaren tutmak çok faydalı olur.
Bunu sağlamanın bir yolu da 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 özet örneğini burada bulabilirsiniz.)
eval
komutu, parametreleri derleme komut dosyasına geçirerek ortam değişkenlerini ayarlamamıza olanak tanır. $SKIP_LIBVPX
ayarlanmışsa (herhangi bir değere) test
komutu libvpx'i oluşturmayı 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 derleme için ek araçlara ihtiyaç duyar. Bu bağımlılıklar, Docker görüntüsü tarafından sağlanan derleme ortamında yoksa bunları kendiniz eklemeniz gerekir. Örnek olarak, doxygen kullanarak libvpx ile ilgili belgeleri de oluşturmak istediğinizi varsayalım. Doxygen, Docker kapsayıcınızda kullanılamaz ancak apt
kullanarak yükleyebilirsiniz.
Bu işlemi build.sh
içinde yapmanız durumunda, kitaplığınızı oluşturmak istediğiniz her seferde doxygen'i yeniden indirip yeniden yüklerdiniz. Bu hem israf yaratır hem de çevrimdışıyken projenizde çalışmanıza engel olur.
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
yazarak oluşturulur. Docker dosyaları oldukça güçlüdür ve birçok komut içerir ancak çoğu zaman yalnızca FROM
, RUN
ve ADD
kullanarak bunlardan kurtulabilirsiniz. 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
seçeneğini seçtim. 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 kapsayıcı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 build.sh
çalıştırmadan önce oluşturulduğundan ve kullanılabilir olduğundan emin olmak için package.json
öğenizi bir bit oranında düzenlemeniz 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 özet buradadır.)
Bu işlem, Docker görüntünüzü oluşturur ancak yalnızca henüz oluşturulmamışsa. Bu durumda her şey önceki gibi çalışır ancak şimdi derleme ortamında doxygen
komutu kullanılabilir. Bu komut, libvpx belgelerinin de derlenmesini sağlar.
Sonuç
C/C++ kodunun ve npm'nin doğal bir uyum içinde olmaması şaşırtıcı değildir ancak bazı ek araçlar ve Docker'ın sağladığı yalıtımla bu uyumu oldukça rahat bir şekilde sağlayabilirsiniz. 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ştirmeleriniz varsa lütfen paylaşın.
Ek: Docker görüntü katmanlarını kullanma
Alternatif bir çözüm, Docker ve Docker'ın önbelleğe alma konusundaki akıllı yaklaşımıyla bu sorunların daha fazlasını kapsamaktır. Docker, Dockerfile'leri adım adım yürütür ve her bir adımın sonucuna kendi görüntüsünü atar. Bu ara görüntülere genellikle "katmanlar" denir. Dockerfile'deki bir komut değişmediyse Docker, Dockerfile'i yeniden derlediğiniz sırada bu adımı yeniden çalıştırmaz. Bunun yerine, resmin son oluşturulduğu zamandaki katmanı yeniden kullanır.
Önceden, uygulamanızı her derlediğinizde 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 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 bir özet örneğini burada bulabilirsiniz.)
docker build
'ü çalıştırırken bind bağlama işlemi yapmadığınız için git'i manuel olarak yüklemeniz ve libvpx'i klonlamanız gerektiğini unutmayın. Yan etki olarak da artık napa'ya
ihtiyacınız yok.