Emscripten ve npm

WebAssembly'yi bu kuruluma nasıl entegre edersiniz? Bu makalede, örnek olarak C/C++ ve Emscripten'i kullanarak bunu işleyeceğiz.

WebAssembly (Wasm) genellikle bir performans temel öğesi olarak veya mevcut C++ kod tabanından biri haline geldi. squoosh.app ile kullanıcılara en azından üçüncü bir perspektif daha olduğunu belirtiyor: ekosistemleri anlamına gelir. Entegre Emscripten, C/C++ kodunu kullanabilirsiniz. Rust wasm desteği yerleşik olarak bulunur ve Go ekibi de üzerinde çalışıyor. Ben ve başka birçok dil de gelecektir.

Bu senaryolarda wasm, uygulamanızın merkezinde değil, bir bulmacadan ziyade bir parça: Bir başka modül. Uygulamanızda zaten JavaScript, CSS, resim öğeleri, web merkezli bir derleme sistemi ve belki React gibi bir çerçeve de olabilir. Peki, bu kuruluma WebAssembly entegre edilsin mi? Bu makalede, proje yaşam döngüsü C/C++ ve Emscripten örnek olarak verilebilir.

Docker

Emscripten ile çalışırken Docker'ın çok değerli olduğunu düşünüyorum. C/C++ kitaplıklar genellikle temel aldıkları işletim sistemiyle çalışacak şekilde yazılır. Tutarlı bir ortama sahip olmak inanılmaz derecede faydalı. Docker sayesinde kurulumu yapılmış ve Emscripten ile birlikte çalışacak şekilde ayarlanmış yüklü tüm araçlar ve bağımlılıklar. Eksik bir şey varsa kendi makinenizi nasıl etkileyeceği konusunda endişelenmenize gerek kalmadan işleyeceğiz. Bir şeyler ters giderse kabı atın ve bitti. Bir kez çalışırsa çalışmaya devam edeceğinden emin olabilirsiniz. aynı sonuçları vermelidir.

Docker Registry için emscripten resmi tarafından kullandığım trzeci.

npm ile entegrasyon

Çoğu durumda, bir web projesine giriş noktası npm'dir. package.json Geleneksel olarak çoğu proje npm install && npm run build ile oluşturulabilir.

Genel olarak, Emscripten tarafından üretilen yapı yapıları (.js ve .wasm dosyası) yalnızca başka bir JavaScript modülü olarak ve öğe. JavaScript dosyası, webpack veya rollup gibi bir paketleyici tarafından işlenebilir. ve wasm dosyası, resim.

Bu nedenle, Emscripten derleme yapılarının "normal" ve geliştirme süreci devreye girer:

{
    "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 belirttiğim gibi, derleme ortamının doğru olduğundan emin olmak için tutarlıdır.

docker run ... trzeci/emscripten ./build.sh, Docker'a yeni bir komut dosyası başlatmasını söyler container'ı eklemek için trzeci/emscripten görüntüsünü kullanın ve ./build.sh komutunu çalıştırın. build.sh, bundan sonra yazacağınız bir kabuk komut dosyasıdır. --rm anlatıyor Docker'ın, çalışması tamamlandıktan sonra container'ı silmesini. Bu sayede, projenizin zamanla eski makine görüntüleri koleksiyonunu ortaya çıkarır. -v $(pwd):/src şu anlama gelir: Docker'ın "yansıtmasını" istiyorsanız içindeki /src konumuna geçerli dizin ($(pwd)) yerleştirilmelidir. Dizinin içindeki /src dizininden dosyalarda yaptığınız tüm değişiklikler yansıtılacağından emin olun. Bu yansıtılan dizinler "bağlama düzenekleri" olarak adlandırılır.

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 incelenmesi gereken çok şey var.

set -e, kabuğu "başarısız" durumuna geçirir yatırım yapmanız önemlidir. Komut dosyasında herhangi bir komut varsa hata döndürürse komut dosyasının tamamı hemen iptal edilir. Bu özellik, son derece faydalıdır çünkü komut dosyasının son çıktısı her zaman başarılı olur. mesajını veya derlemenin başarısız olmasına neden olan hatayı içerir.

export ifadeleriyle birkaç ortamın değerlerini tanımlarsınız değişkenlerine karşılık gelir. Ek komut satırı parametrelerini C'ye aktarmanıza olanak tanır. derleyici (CFLAGS), C++ derleyici (CXXFLAGS) ve bağlayıcı (LDFLAGS). Tüm kullanıcılar, aşağıdakilerin sağlandığından emin olmak için optimize edici ayarlarını OPTIMIZE üzerinden alır: her şey aynı şekilde optimize edilir. Birkaç olası değer vardır OPTIMIZE değişkeni için:

  • -O0: Hiçbir optimizasyon yapmayın. Ölü kod elenmez ve Emscripten, kaynaklandığı JavaScript kodunu da küçültmez. Hata ayıklama için uygundur.
  • -O3: Performans için agresif şekilde optimizasyon yapar.
  • -Os: İkincil olarak performans ve boyut için agresif bir şekilde optimizasyon yapın ölçütü olarak kullanabilirsiniz.
  • -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. ECC'nin "GCC veya clang gibi derleyiciler için kullanıma sunulan bir alternatif olması gerekir". Yani tüm veya GCC'den öğrenebileceğiniz işaretler, muhtemelen emcc tarafından olur. -s işareti, Emscripten'i yapılandırmamıza olanak tanıması açısından özeldir. merak ediyor. Mevcut tüm seçenekleri Emscripten'in settings.js, ama bu dosya oldukça zorlayıcı olabilir. Emscripten flag'lerinin listesi önemli olduğunu düşünüyorum:

  • --bind şunu etkinleştirir: embind.
  • -s STRICT=1, kullanımdan kaldırılan tüm derleme seçeneklerini desteklemez. Bu sayede bir şekilde derlendiğinden emin olun.
  • -s ALLOW_MEMORY_GROWTH=1, şu durumlarda belleğin otomatik olarak büyütülmesine izin verir gerekir. Emscripten, yazının yazıldığı sırada 16 MB bellek tahsis eder. ilk adımıdır. Kodunuz bellek parçalarını ayırdığından bu seçenek, bu işlemler, bellek yetersiz olduğunda wasm modülünün tamamının başarısız olmasına ya da birleştirici kodun toplam belleği 50, 00 ABD doları veya kabul etmesini sağlar.
  • -s MALLOC=..., hangi malloc() uygulamasının kullanılacağını seçer. emmalloc Emscripten için özel olarak küçük ve hızlı bir malloc() uygulamasıdır. İlgili içeriği oluşturmak için kullanılan alternatifi olan dlmalloc, tam kapsamlı bir malloc() uygulamasıdır. Yalnızca siz çok sayıda küçük nesne ayırıyorsanız dlmalloc öğesine geçmeniz gerekir veya ileti dizisi görünümü kullanmak istiyorsanız.
  • -s EXPORT_ES6=1, JavaScript kodunu ES6 modülüne dönüştürür. tüm paketleyicilerle çalışan varsayılan dışa aktarım olabilir. Ayrıca -s MODULARIZE=1 tarafından şunun için de gereklidir: ayarlayabilirsiniz.

Aşağıdaki işaretler her zaman gerekli değildir veya yalnızca hata ayıklamaya yardımcı olur amaçlar:

  • -s FILESYSTEM=0, Emscripten ile ilgili bir işarettir ve C/C++ kodunuz dosya sistemi işlemlerini kullandığında sizin için bir dosya sistemi emülasyonu oluşturabilir. Test sırasında dosya sistemi emülasyonunu birleştirmenize olanak tanımaz. Ancak bazen bu analizde hata olabilir ve 70 kB'lık hacme ek olarak tutkal için dosya sistemi emülasyonunun kodunu ekleyebilirsiniz. -s FILESYSTEM=0 ile Emscripten'i bu kodu dahil etmemeye zorlayabilirsiniz.
  • -g4, Emscripten'in .wasm ve wasm modülü için bir kaynak eşleme dosyası da oluşturur. Daha fazla bilgi için Emscripten ile hata ayıklama hata ayıklama bölümünü inceleyin.

İş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>

(Burada bir özet ekleyin.)

Her şeyi derlemek için

$ npm install
$ npm run build
$ npm run serve

localhost:8080 adresine gittiğinizde Geliştirici Araçları konsolu:

C++ ve Emscripten aracılığıyla yazdırılan bir iletiyi 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, projenizin bir parçasıdır. Kodu projenizin deposuna manuel olarak ekleyebilirsiniz ya da bu tür bağımlılıkları yönetmek için npm kullanabilirsiniz. Örneğin, web uygulamamda libvpx'i kullanmak istiyorum. libvpx .webm dosyalarında kullanılan codec'i olan VP8 ile resimleri kodlamak için kullanılan bir C++ kitaplığıdır. Ancak, libvpx npm'de değil ve bir package.json içermediğinden emin olamıyorum npm'yi kullanarak doğrudan yükleyin.

Bu ikilemden kurtulmak için napa. napa, herhangi bir Git'i yüklemenize olanak tanır. depo URL'sini node_modules klasörünüze bağımlılık olarak ekleyebilirsiniz.

Bağımlılık olarak napa'yı yükleyin:

$ npm install --save napa

ve napa öğesini bir 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'ı klonlama işlemini gerçekleştirir kod deposuna libvpx adı ile node_modules ekleyin.

Derleme komut dosyanızı artık libvpx'i derleyecek şekilde genişletebilirsiniz. libvpx configure kullanır make ve oluşturulacak. Neyse ki Emscripten, configure ve make, Emscripten'in derleyicisini kullanır. Bu amaçla sarmalayıcılar emconfigure ve emmake komutlarını içerir:

# ... 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: başlıklar (geleneksel olarak .h veya .hpp dosyaları) içeren standart bir yapılandırma dosyasıdır. kitaplığını gösterir ve gerçek kitaplığı (geleneksel olarak .so veya .a dosyaları) içerir. Alıcı: kodunuzda kitaplığın VPX_CODEC_ABI_VERSION sabitini kullanırsanız bir #include ifadesi kullanarak kitaplığın başlık dosyalarını dahil etmek için:

#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 işlevi budur. Derleyiciye, hangi dizinlerin başlık dosyalarını kontrol edin. Buna ek olarak, derleyiciye gerçek kitaplık dosyası:

# ... 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 bu işlemin yeni bir .js oluşturduğunu görürsünüz. ve yeni bir .wasm dosyası ekleyerek demo sayfasının gerçekten de sabit değer çıktısını verir:

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

Ayrıca derleme işleminin uzun sürdüğünü de fark edeceksiniz. Çalışmanın uzun derleme süreleri değişiklik gösterebilir. libvpx'de bu çok uzun sürer çünkü her çalıştırdığınızda hem VP8 hem de VP9 için bir kodlayıcı ve bir kod çözücü derler. kaynak dosyalar değişmemiş olsa bile derleme komutunuzu kullanmanız gerekir. Küçük bir hesap bile my-module.cpp üzerinde yapacağınız değişikliklerin oluşturulması uzun sürer. Her ne kadar libvpx derleme yapılarını, yüklendikten sonra inşa etmek istiyorum.

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 ...

(Kısa bir özet içerir.)

eval komutu, parametreleri ileterek ortam değişkenlerini ayarlamamıza olanak tanır. derleme komut dosyasına geçelim. Aşağıdaki durumlarda test komutu libvpx'i derlemeyi atlar: $SKIP_LIBVPX herhangi bir değere ayarlanmış.

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 ekleyin. Örneğin, diyelim ki bir hafta içinde doxygen kullanarak libvpx ile ilgili dokümanlar Doxsijen Docker container'ınızın içinde bulunur ancak bunu apt kullanarak yükleyebilirsiniz.

Bu işlemi build.sh içinde yapmanız durumunda, uygulamanızı yeniden indirip yeniden yüklerdiniz doxygen'i kullanabilirsiniz. Bu, proje yönetiminin hem de çevrimdışıyken projenizde çalışmanıza engel olur.

Burada kendi Docker görüntünüzü oluşturmanız önerilir. Docker görüntüleri, bir Dockerfile yazmanız gerekir. Dockerfile'lar oldukça güçlü ve birçok uygulamaya komutlarına sahip olmakla birlikte, zamandan tasarruf etmek için FROM, RUN ve ADD kullanmaya başlayabilirsiniz. Bu durumda:

FROM trzeci/emscripten

RUN apt-get update && \
    apt-get install -qqy doxygen

FROM ile, başlangıç olarak kullanmak istediğiniz Docker görüntüsünü belirtebilirsiniz. puan. Temel olarak trzeci/emscripten seçeneğini seçtim (kullandığınız resim) emin olmanız gerekir. RUN ile Docker'a kabuk komutlarını emin olun. Bu komutların kapsayıcıda yaptığı değişiklikler artık Docker görüntüsü vardır. Docker görüntünüzün oluşturulduğundan ve build.sh çalıştırmadan önce package.json ayarınızı yapmanız gerekir. bit:

{
    // ...
    "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",
    // ...
    },
    // ...
}

(Kısa bir özet içerir.)

Bu işlem Docker görüntünüzü ancak henüz oluşturulmadıysa oluşturur. Sonra her şey eskisi gibi çalışıyor, ancak şimdi derleme ortamında doxygen var komutu kullanılabilir durumda olduğundan, libvpx belgelerinin olur.

Sonuç

C/C++ kodu ile npm'nin doğal olarak uygun olmaması şaşırtıcı değildir, ancak bazı ek araçlar ve yalıtım ile oldukça rahat çalışmasını tek bir web sitesidir. Bu kurulum her projede kullanılamaz ancak iyi bir başlangıç noktası ekleyebilirsiniz. Mevcut lütfen paylaşın.

Ek: Docker görüntü katmanlarından yararlanma

Alternatif bir çözüm de bu sorunların daha fazlasını Docker ve Docker'ın önbelleğe alma konusundaki akıllı yaklaşımı. Docker, Dockerfile'ları adım adım yürütür her adımın sonucunu kendine ait bir resim atar. Bu orta düzey görseller genellikle "katmanlar" olarak adlandırılır. Dockerfile'daki bir komut değişmediyse Docker Dockerfile'ı yeniden oluştururken bu adımı yeniden çalıştırmaz. Bunun yerine resmin son oluşturulduğu andaki katmanı yeniden kullanır.

Daha önce, her seferinde libvpx'i yeniden oluşturmamak için biraz çaba göstermeniz gerekiyordu. yardımcı olursunuz. Bunun yerine, libvpx'teki yapı talimatlarını taşıyabilirsiniz Docker'ın önbelleğe alma özelliğinden yararlanmak için build.sh üzerinden Dockerfile ile aktarma mekanizma:

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

(Kısa bir özet içerir.)

docker build çalıştırılırken bağlama bağlantıları. Herhangi bir yan etki olarak, artık çok kolay.