Wasm'a C kitaplığı kullanma

Bazen yalnızca C veya C++ kodu olarak kullanılabilen bir kitaplığı kullanmak isteyebilirsiniz. Genelde bu aşamada vazgeçersiniz. Ama artık değil çünkü Emscripten ve WebAssembly (ya da Wasm)!

Araç zinciri

Kendimi, mevcut C kodunu derleyerek kendimi Wasm. LLVM'nin Wasm arka ucunda biraz gürültü olduğu için Bu konuyu araştırmaya başladım. Bu sırada derlemesi için basit programlar edinebilirsiniz Bu şekilde, ikinci olarak C'nin standart kitaplığını kullanmak veya dosyası yüklerseniz muhtemelen sorunlarla karşılaşırsınız. Bu şekilde Google’ın aldığım ders:

Emscripten, eskiden C-to-asm.js derleyicisi olarak eskiden olsa da zaman içinde Wasm'i hedefliyor ve geçiş sürecinde dahili olarak resmi LLVM arka ucuna gider. Emscripten ayrıca C'nin standart kitaplığının Wasm ile uyumlu uygulaması. Emscripten'i kullanın. Google birçok gizli çalışma barındırır bir dosya sistemi emüle eder, bellek yönetimi sağlar, OpenGL'i WebGL ile sarmalar. hiçbir şeyi kendiniz geliştirirken yaşamazsınız.

Şişkinlikten endişelenmeniz gerekiyor gibi görünse de. Kesinlikle endişeliyim. — Emscripten derleyicisi, gerekli olmayan her şeyi kaldırır. Şu kılavuzda: elde edilen Wasm modüllerini mantığa uygun şekilde emin olmak için çalışıyor. Emscripten ve WebAssembly ekipleri, daha küçük hale gelecektir.

Emscripten'i, web sitesini ziyaret ederek veya Homebrew kullanarak Şu sitenin hayranıysanız: benim gibi yuvaya yerleştirilmiş komutlar var ve sisteminize bir şey yüklemek istemiyorum. bir WebAssembly deneyimi yaşamak için Kullanabileceğiniz Docker görüntüsü aşağıdaki adımları uygulayabilirsiniz:

    $ docker pull trzeci/emscripten
    $ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>

Basit bir şeyler derleme

C ile yazılacak fonksiyona ilişkin neredeyse standart bir örneği ele alalım: n'inci fibonacci sayısını hesaplar:

    #include <emscripten.h>

    EMSCRIPTEN_KEEPALIVE
    int fib(int n) {
      if(n <= 0){
        return 0;
      }
      int i, t, a = 0, b = 1;
      for (i = 1; i < n; i++) {
        t = a + b;
        a = b;
        b = t;
      }
      return b;
    }

C'yi biliyorsanız, fonksiyonun kendisi çok da şaşırtıcı olmamalıdır. Şu an C'yi bilemedim ama JavaScript'i biliyorum. Umarım bu becerileri, Bunun nasıl bir şey olduğunu anlamalısınız.

emscripten.h, Emscripten tarafından sağlanan bir başlık dosyasıdır. Bu bilgilere ihtiyaç duyduğumuzda EMSCRIPTEN_KEEPALIVE makrosuna erişimi vardır, ancak çok daha fazla işlev sunar. Bu makro, derleyiciye, bir işlevi görünse bile kaldırmamasını söyler kullanılmıyor. Bu makroyu atlarsak derleyici, işlevi optimize ederek Sonuçta kimse kullanmıyor.

Tüm bunları fib.c adlı bir dosyaya kaydedelim. Bunu bir .wasm dosyasına dönüştürmek için Emscripten'in derleyici komutu emcc olana dönmem gerekiyor:

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c

Şimdi bu komutu inceleyelim. emcc, Emscripten'ın derleyicisidir. fib.c, C'mizdir dosyası olarak kaydedebilirsiniz. Şu ana kadar her şey yolunda. -s WASM=1, Emscripten'den bize bir Wasm dosyası vermesini söylüyor yerine asm.js dosyası kullanmayın. -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]', derleyiciye JavaScript dosyasında cwrap() işlevi mevcut. Bu işlev hakkında daha fazla bilgi daha sonra. -O3, derleyiciye agresif bir şekilde optimizasyon yapmasını söyler. Daha düşük bir tutar seçebilirsiniz sayısını artırır, ancak bu da sonuçta elde edilen paketlerin daha büyük olduğundan, derleyici kullanılmayan kodu kaldırmayabilir.

Komutu çalıştırdıktan sonra elinizde a.out.js ve a.out.wasm adlı bir WebAssembly dosyası içerir. Wasm dosyası (veya "modül"), derlenmiş C kodumuzu içerir ve oldukça küçük olmalıdır. İlgili içeriği oluşturmak için kullanılan JavaScript dosyası, Wasm modülümüzün yüklenmesi ve ilk kullanıma hazırlanmasıyla ve daha iyi bir API sağlamak. Gerekirse, genellikle reklam öğesi tarafından sağlanması beklenen diğer işlevlerin işletim sistemi olarak nitelendirilirler. Dolayısıyla, JavaScript dosyası biraz daha büyüktür, 19 KB'lık (~5 KB gzip ile sıkıştırılmış) olmalıdır.

Basit bir şey çalıştırma

Modülünüzü yükleyip çalıştırmanın en kolay yolu, oluşturulan JavaScript'i kullanmaktır dosyası olarak kaydedebilirsiniz. Bu dosyayı yüklediğinizde Module genel arasında yer alır. Tekliflerinizi otomatikleştirmek ve optimize etmek için cwrap JavaScript yerel işlevi oluşturmak için sarmalanmış işlevi çağırmaktır. cwrap İşlev adı, dönüş türü ve bağımsız değişken türleri aşağıdaki sırayla gösterilmiştir:

    <script src="a.out.js"></script>
    <script>
      Module.onRuntimeInitialized = _ => {
        const fib = Module.cwrap('fib', 'number', ['number']);
        console.log(fib(12));
      };
    </script>

Şu durumda: bu kodu çalıştırın "144"ü görmeniz gerekir. bu, 12. Fibonacci numarasıdır.

Kutsal kapı: C kitaplığı derleme

Şimdiye kadar, yazdığımız C kodu Wasm düşünülerek yazılmıştı. Çekirdek kullanım alanı ise mevcut C ekosistemini alıp geliştiricilerin bunları web'de kullanmasını sağlar. Bu kütüphaneler genellikle C'nin standart kitaplığını, işletim sistemini, dosya sistemini ve sahip olmalıyız. Emscripten, bu özelliklerin çoğunu sağlar ancak bazı özellikler sınırlamalara dikkat edin.

Asıl hedefime geri dönelim: WebP'den Wasm'a yönelik bir kodlayıcı derleme. İlgili içeriği oluşturmak için kullanılan WebP codec'inin kaynağı C dilinde yazılmıştır ve GitHub'ın yanı sıra API belgeleri. Bu oldukça iyi bir başlangıç noktasıdır.

    $ git clone https://github.com/webmproject/libwebp

Basit bir başlangıç için, size WebPGetEncoderVersion() verilerini göstermeyi deneyelim webp.c adlı bir C dosyası yazarak JavaScript'e encode.h:

    #include "emscripten.h"
    #include "src/webp/encode.h"

    EMSCRIPTEN_KEEPALIVE
    int version() {
      return WebPGetEncoderVersion();
    }

Bu, libwebp'nin kaynak kodunu alıp alamayacağımızı test etmek için basit bir programdır için hiçbir parametre veya karmaşık veri yapısına ihtiyaç duymadığımızdan, işlevi çağırır.

Bu programı derlemek için, derleyiciye programı nerede bulacağını söylememiz gerekir libwebp'nin başlık dosyalarını yönetmek için -I işaretini kullanır ve ayrıca libwebp'yi inceleyeceğiz. Dürüst olmak gerekirse tüm C bulduğum her şeyi ayırt etmek için derleyiciye başvurdum. gerekmez. Mükemmel bir iş çıkarıyordu.

    $ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
        -I libwebp \
        webp.c \
        libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c

Şimdi şık, yeni modülümüzü yüklemek için yalnızca bazı HTML ve JavaScript'lere ihtiyacımız var:

<script src="/a.out.js"></script>
<script>
  Module.onRuntimeInitialized = async (_) => {
    const api = {
      version: Module.cwrap('version', 'number', []),
    };
    console.log(api.version());
  };
</script>

Düzeltme sürüm numarasını çıkış:

Geliştirici Araçları konsolunun doğru sürümü gösteren ekran görüntüsü
sayı.

JavaScript'ten Wasm'a resim alma

Kodlayıcının sürüm numarasını almak harikadır ama gerçek bir kodlayıcı olur, değil mi? Haydi başlayalım.

Cevaplamamız gereken ilk soru şudur: Resmi Wasm arazisine nasıl katarız? Her bir encoding API of libwebp varsa RGB, RGBA, BGR veya BGRA'daki bir bayt dizisi. Neyse ki Canvas API'sinin getImageData() Bu da bize Uint8ClampedArray resim verilerini içeren RGBA:

async function loadImage(src) {
  // Load image
  const imgBlob = await fetch(src).then((resp) => resp.blob());
  const img = await createImageBitmap(imgBlob);
  // Make canvas same size as image
  const canvas = document.createElement('canvas');
  canvas.width = img.width;
  canvas.height = img.height;
  // Draw image onto canvas
  const ctx = canvas.getContext('2d');
  ctx.drawImage(img, 0, 0);
  return ctx.getImageData(0, 0, img.width, img.height);
}

Artık "yalnızca" verilerin JavaScript bölgesinden Wasm'a kopyalanması arazi. Bunun için iki ek işlevden yararlanmamız gerekiyor. Ayrılan ve tekrar boşaltan bir başka resim belleği göreceksiniz:

    EMSCRIPTEN_KEEPALIVE
    uint8_t* create_buffer(int width, int height) {
      return malloc(width * height * 4 * sizeof(uint8_t));
    }

    EMSCRIPTEN_KEEPALIVE
    void destroy_buffer(uint8_t* p) {
      free(p);
    }

create_buffer, RGBA resmi için bir arabellek (yani piksel başına 4 bayt) ayırır. malloc() tarafından döndürülen işaretçi, şunun ilk bellek hücresinin adresidir: zaman alabilir. İşaretçi JavaScript alanına döndürüldüğünde, şu şekilde kabul edilir: yeterlidir. cwrap kullanarak işlevi JavaScript'e gösterdikten sonra, bu sayıyı arabelleğimizin başlangıcını bulmak ve resim verilerini kopyalamak için kullanırız.

const api = {
  version: Module.cwrap('version', 'number', []),
  create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
  destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
const image = await loadImage('/image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);

Grand Finale: Resmi kodlayın

Resim artık Wasm arazisinde kullanılabilir. Şimdi, WebP kodlayıcıyı çağırarak işini yapıyor! Her bir WebP dokümanları, WebPEncodeRGBA mükemmel bir seçim gibi görünüyor. Fonksiyon, bir işaretçiyi giriş resmine götürür ve ve 0 ile 100 arasında bir kalite seçeneğine yer vermelidir. Aynı zamanda bir çıkış tamponu oluşturduk. Bu çabayı tamamladığımızda WebPFree() kullanarak WebP görüntüsüyle yapılıyor.

Kodlama işleminin sonucu, çıktı arabelleği ve uzunluğu olur. Çünkü C işlevindeki işlevler dönüş türü olarak dizi içeremez (bellek ayırmadığı sürece dinamik olarak) statik bir global diziye başvurdum. Biliyorum, temiz C değil (aslında, Bunun nedeni, Wasm işaretçilerinin 32 bit genişliğinde olması gerekir.), ancak verileri koruyarak basit sanırım bu adil bir kısayol.

    int result[2];
    EMSCRIPTEN_KEEPALIVE
    void encode(uint8_t* img_in, int width, int height, float quality) {
      uint8_t* img_out;
      size_t size;

      size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);

      result[0] = (int)img_out;
      result[1] = size;
    }

    EMSCRIPTEN_KEEPALIVE
    void free_result(uint8_t* result) {
      WebPFree(result);
    }

    EMSCRIPTEN_KEEPALIVE
    int get_result_pointer() {
      return result[0];
    }

    EMSCRIPTEN_KEEPALIVE
    int get_result_size() {
      return result[1];
    }

Tüm bunları yaptıktan sonra, kodlama işlevini çağırabilir, işaretçi ve resim boyutunuz, kendi JavaScript alanı tamponumıza yerleştirin ve süreçte ayırdığımız tüm Wasm-land tamponlarını serbest bırakın.

    api.encode(p, image.width, image.height, 100);
    const resultPointer = api.get_result_pointer();
    const resultSize = api.get_result_size();
    const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
    const result = new Uint8Array(resultView);
    api.free_result(resultPointer);

Resminizin boyutuna bağlı olarak, Wasm'in , bellek hem giriş hem de çıkış resmini barındıracak kadar büyütülemiyor:

Geliştirici Araçları konsolunda hatanın gösterildiği ekran görüntüsü.

Neyse ki bu sorunun çözümü hata mesajındadır! Yapmamız gereken -s ALLOW_MEMORY_GROWTH=1 öğesini derleme komutumuza ekleyin.

İşte bu kadar! Bir WebP kodlayıcısı derledik ve bir JPEG görüntüsünün kodunu WebP İşe yaradığını kanıtlamak için sonuç tamponumuzu bir blob'a dönüştürüp bir <img> öğesinde yapalım:

const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img);

Yeni bir WebP imajıyla ihtişamını yaşayın.

Geliştirici Araçları&#39;nın ağ paneli ve oluşturulan resim.

Sonuç

Tarayıcıda çalışmak için bir C kütüphanesi edinmek için parkta yürümek değil, süreci kavradığınıza göre veri akışının nasıl işlediğini daha kolay hale getirir ve sonuçlar akıllara kıvılcımlar getirebilir.

WebAssembly, web'de işleme, veri işleme ve çıtır çıtır çıtırlar ve oyun oynamak. Wasm'ın herkesin kullanımına açık olması her şeye uygulanır, ancak bu darboğazlardan birine ulaştığınızda Wasm son derece faydalı bir araç.

Bonus içerik: Basit bir şeyi zor yoldan çalıştırmak

Oluşturulan JavaScript dosyasından kaçınmayı denemek isterseniz . Fibonacci örneğine dönelim. Kendimiz yükleyip çalıştırmak için şunları yapın:

<!DOCTYPE html>
<script>
  (async function () {
    const imports = {
      env: {
        memory: new WebAssembly.Memory({ initial: 1 }),
        STACKTOP: 0,
      },
    };
    const { instance } = await WebAssembly.instantiateStreaming(
      fetch('/a.out.wasm'),
      imports,
    );
    console.log(instance.exports._fib(12));
  })();
</script>

Emscripten tarafından oluşturulan WebAssembly modüllerinin çalışacak belleği yok bu verileri aklınızdan çıkarmayın. Wasm modülünü sağlama şeklinizle herhangi bir şey, imports nesnesini kullanmaktır. Bu nesne, instantiateStreaming işlevi. Wasm modülü, Google Cloud'un içindeki içe aktarma nesnesi bulunur, ancak bunun dışında başka hiçbir şey yoktur. Kurallara göre modüller Emscripting tarafından derlendi, yüklenen JavaScript'ten birkaç şey bekler ortam:

  • İlki env.memory. Wasm modülü dışarıdan habersiz diğer bir deyişle, üzerinde çalışacak hafızaya ihtiyacı var. Girin WebAssembly.Memory. Doğrusal belleğin (isteğe bağlı olarak büyütülebilir) bir parçasını temsil eder. Boyutlandırma parametreleri "WebAssembly sayfaları birimleri içinde" içindedir. Bu, yukarıdaki kodun 1 sayfa bellek ayırır ve her sayfa 64 boyutunda olur KiB. maximum sağlamadan seçeneğinde, bellek büyüme açısından teorik olarak sınırsızdır (Chrome şu anda 2 GB'lık bir kesin sınır). Çoğu WebAssembly modülünün bir daha fazla bilgi edineceksiniz.
  • env.STACKTOP, yığının büyümeye başlaması gereken yeri tanımlar. Grup fonksiyon çağrıları yapmak ve yerel değişkenlere bellek tahsis etmek için gereklidir. Küçük oyunlarımızda dinamik bellek yönetimi mağduriyeti yapmak için belleğin tamamını bir yığın olarak kullanabiliriz. STACKTOP = 0