Emscripten ve npm

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=... 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 olarak, tam teşekküllü bir malloc() uygulaması olan dlmalloc kullanılabilir. Yalnızca çok sayıda küçük nesneyi sık sık ayırıyorsanız veya iş parçacığı kullanmak istiyorsanız dlmalloc'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:

C++ ve Emscripten aracılığıyla 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 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:

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

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.