Emscripten ve npm

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=..., hangi malloc() uygulamasının kullanılacağını seçer. emmalloc, özellikle Emscripten için küçük ve hızlı bir malloc() uygulamasıdır. Alternatif, tam kapsamlı bir malloc() uygulaması olan dlmalloc'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:

C++ ve Emscripten üzerinden yazdırılan bir mesajı gösteren Geliştirici Araçları.

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:

emscripten aracılığıyla yazdırılan libvpx&#39;in ABI sürümünü gösteren DevTools.

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.