Emscripten ve npm

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=..., hangi malloc() uygulamasının kullanılacağını seçer. emmalloc, özellikle Emscripten için kullanılan küçük ve hızlı bir malloc() uygulamasıdır. Alternatifi, tam kapsamlı bir malloc() uygulaması olan dlmalloc'tır. Yalnızca çok sayıda küçük nesneyi sık sık ayırıyorsanız veya iş parçacığı kullanmak istiyorsanız dlmalloc öğ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++ ve Emscripten aracılığıyla yazdırılmış bir ileti gösteren Geliştirici Araçları.

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:

Geliştirici Araçları, libvpx&#39;in emscripten aracılığıyla basılan ABI sürümünü gösteriyor.

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.