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 ile C/C++ kodu kullanabilirsiniz. Rust'ta yerleşik olarak wasm desteği vardır. Go ekibi de bu konu ü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 JavaScript, CSS, resim öğeleri, web odaklı bir derleme sistemi ve hatta React gibi bir çerçeve zaten mevcuttur. WebAssembly'i bu kuruluma nasıl entegre edersiniz? Bu makalede, örnek olarak C/C++ ve Emscripten ile bu işlemi yapacağız.
Docker
Emscripten ile çalışırken Docker'ın çok faydalı olduğunu gördüm. C/C++ kitaplıkları genellikle yazıldıkları işletim sistemiyle çalışacak şekilde yazılır. Tutarlı bir ortam oluşturmak son derece faydalıdır. Docker ile, Emscripten ile çalışmak üzere önceden ayarlanmış ve tüm araç ve bağımlılıkların yüklü olduğu sanallaştırılmış bir Linux sistemi elde edersiniz. Eksik bir şey varsa kendi makinenizi veya diğer projelerinizi nasıl etkileyeceğinden endişelenmenize gerek kalmadan kolayca yükleyebilirsiniz. Bir sorun olursa kapsülü atın ve 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 Kayıt Defteri'nde, trzeci tarafından oluşturulan ve yoğun olarak kullandığım bir Emscripten resmi var.
npm ile entegrasyon
Çoğu durumda, web projesinin giriş noktası npm'nin package.json
paketidir. 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 rollup gibi bir paketleyici tarafından işlenebilir. wasm dosyası ise resimler gibi diğer tüm 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ın oluşturulması 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ı ö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 kabuk komut dosyasıdır. --rm
, Docker'a çalışmayı bitirdikten sonra kapsayıcıyı silmesini söyler. Bu sayede, zaman içinde eski makine görüntüleri koleksiyonu oluşturmazsı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
'ye 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 "hızlı başarısız" moduna geçirir. Komut dosyasında 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. Bunlar, C derleyicisine (CFLAGS
), C++ derleyicisine (CXXFLAGS
) ve bağlayıcıya (LDFLAGS
) ek komut satırı parametreleri iletmenize olanak tanır. Tüm bu bileşenler, her şeyin aynı şekilde optimize edildiğinden emin olmak için OPTIMIZE
aracılığıyla optimizasyon ayarlarını 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 iyidir.-O3
: Performans için agresif bir şekilde optimizasyon yapın.-Os
: Performans ve ikincil ölçüt olarak boyut için agresif bir şekilde optimize edin.-Oz
: Gerekirse performanstan ödün vererek boyut için agresif bir şekilde optimizasyon yapın.
Web için çoğunlukla -Os
'ü öneririm.
emcc
komutunun kendi içinde birçok seçeneği vardır. emcc'nin "GCC veya clang gibi derleyiciler için doğrudan kullanılabilen bir değişim aracı" olması gerektiğini 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ı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. Web geliştiriciler için en önemli olduğunu düşündüğüm Emscripten işaretçilerinin listesini aşağıda bulabilirsiniz:
--bind
, embind'i etkinleştirir.-s STRICT=1
, desteği sonlandırılan tüm derleme seçenekleri için desteği sonlandırıyor. Bu sayede kodunuz ileriye dönük uyumlu bir şekilde derlenir.-s ALLOW_MEMORY_GROWTH=1
, gerekirse belleğin otomatik olarak büyütülmesine 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 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=...
, kullanılacakmalloc()
uygulamasını seçer.emmalloc
, özellikle Emscripten için küçük ve hızlı birmalloc()
uygulamasıdır. Alternatif, tam teşekküllü birmalloc()
uygulaması olandlmalloc
'tir. Yalnızca sık sık çok sayıda küçük nesne atıyorsanız veya mesaj dizileri kullanmak istiyorsanızdlmalloc
'e geçmeniz 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ıdır:
-s FILESYSTEM=0
, Emscripten ile ilgili bir işarettir ve C/C++ kodunuz dosya sistemi işlemleri kullandığında sizin için bir dosya sistemini taklit etme özelliğidir. Dosya sistemi emülasyonunun yapıştırıcı koda 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 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çine hata ayıklama bilgilerini eklemesini sağlar ve ayrıca wasm modülü için bir kaynak haritalar dosyası oluşturur. 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);
}
Ve bir 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 buradadır.)
Her şeyi derlemek için
$ npm install
$ npm run build
$ npm run serve
localhost:8080 adresine gittiğinizde DevTools 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'yi de kullanabilirsiniz. Web uygulamamda libvpx kullanmak istediğimi varsayalım. libvpx, .webm
dosyalarında kullanılan codec olan VP8 ile görüntüleri kodlayan 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.
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
'yi çalıştırdığınızda napa, libvpx GitHub deposunu libvpx
adı altında node_modules
'inize klonlar.
Artık derleme komut dosyanızı libvpx'i oluşturacak şekilde genişletebilirsiniz. libvpx, derlenmek için configure
ve make
'yi kullanır. Neyse ki Emscripten, configure
ve make
'in Emscripten 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ıkları iki bölüme ayrılır: kitaplığın kullanıma sunduğu veri yapılarını, sınıfları, sabitleri vb. tanımlayan üstbilgiler (geleneksel olarak .h
veya .hpp
dosyaları) ve asıl kitaplık (geleneksel olarak .so
veya .a
dosyaları). Kitaplığın VPX_CODEC_ABI_VERSION
sabit değerini kodunuzda kullanmak için #include
ifadesi 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
değerini nerede arayacağını bilmemesinden kaynaklanıyor.
-I
işareti bunun içindir. Derleyiciye, başlık 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
dosyasını şimdi çalıştırırsanız sürecin yeni bir .js
ve yeni bir .wasm
dosyası oluşturduğunu ve demo sayfasının gerçekten de sabit değeri döndürdüğünü görürsünüz:
Ayrıca derleme sürecinin uzun sürdüğünü fark edeceksiniz. Derleme sürelerinin uzun olmasının 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ü derlendiğinden bu işlem uzun sürer. my-module.cpp
'ünüzde yapılan küçük bir değişikliğin bile derlenmesi uzun zaman alır. libvpx'in derleme yapılarını ilk kez derlendikten sonra saklamak çok faydalı olacaktır.
Bunu yapmanı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 özet buradadır.)
eval
komutu, derleme komut dosyasına parametreler ileterek 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ş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ç duyulur. Bu bağımlılıklar, Docker görüntüsü tarafından sağlanan derleme ortamında yoksa bunları kendiniz eklemeniz gerekir. Örneğin, doxygen'i kullanarak libvpx dokümantasyonunu da oluşturmak istediğinizi varsayalım. Doxygen, Docker kapsayıcınızda kullanılamaz ancak apt
kullanarak yükleyebilirsiniz.
build.sh
'de bunu yaparsanız kitaplığınızı oluşturmak istediğinizde her seferinde doxygen'u yeniden indirip yüklemeniz gerekir. Bu durum, yalnızca gereksiz harcama yapmanıza değil, internete bağlı değilken projenizde çalışmanıza da engel olur.
Bu durumda kendi Docker görüntünüzü oluşturmanız mantıklı olacaktır. Docker görüntüleri, oluşturma adımlarını açıklayan bir Dockerfile
yazılarak oluşturulur. Dockerfile'ler oldukça güçlüdür ve çok sayıda komut içerir 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
'yi (tüm bu süre boyunca kullandığınız resim) seçtim. RUN
ile Docker'a, container'ın içinde kabuk komutları çalıştırmasını 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. build.sh
'ü ç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
'unuzu biraz ayarlamanı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 bir özet buradadır.)
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 mevcuttur. Bu komut, libvpx dokümanının da derlenmesine neden olur.
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ığı izolasyonla bu ikisini oldukça rahat bir şekilde çalıştırabilirsiniz. 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ı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 andaki katmanı yeniden kullanır.
Daha önce, uygulamanızı her oluşturduğunuzda 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'in derleme talimatlarını build.sh
'ünüzden Dockerfile
'e 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 buradadır.)
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. Bu nedenle artık napa'ya gerek yoktur.