Emscripten'ın evi

JS'yi, wasm'ınıza bağlar!

Son wasm makalemde, web'de kullanmak üzere bir C kitaplığının wasm'a nasıl derleneceğinden söz etmiştim. Bana (ve birçok okuyucunun) dikkatini çeken şeylerden biri, wasm modülünüzün hangi işlevlerini kullandığınızı manuel olarak bildirmek zorunda kalmanız ve bu durumun kaba ve biraz tuhaf olmasıydı. Kafanızı tazelemek gerekirse, bahsettiğim kod snippet'i aşağıdaki gibidir:

const api = {
    version: Module.cwrap('version', 'number', []),
    create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
    destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};

Burada EMSCRIPTEN_KEEPALIVE ile işaretlediğimiz işlevlerin adlarını, döndürme türlerini ve bağımsız değişkenlerinin türlerini açıklıyoruz. Daha sonra, bu işlevleri çağırmak için api nesnesindeki yöntemleri kullanabiliriz. Ancak wasm'ın bu şekilde kullanılması dizeleri desteklemez ve bellek parçalarını manuel olarak taşımanızı gerektirir. Bu da birçok kitaplık API'sinin kullanımını oldukça yorucu hale getirir. Daha iyi bir yol var mı? Neden böyle bir soru var, yoksa bu makale ne hakkında olmalı?

C++ ad yönetimi

Geliştirici deneyimi bu bağlamalara yardımcı olacak bir araç derlemek için yeterli bir neden olsa da aslında daha acil bir neden vardır: C veya C++ kodunu derlediğinizde her dosya ayrı olarak derlenir. Daha sonra, bir bağlayıcı, tüm bu sözde nesne dosyalarını bir araya getirerek bir wasm dosyasına dönüştürme işlemini tamamlar. C ile işlevlerin adları, bağlayıcının kullanması için nesne dosyasında hâlâ mevcuttur. Bir C işlevini çağırabilmek için tek ihtiyacınız olan addır. Bu adı, cwrap() için bir dize olarak sağlarız.

Öte yandan C++, işlevlerin aşırı yüklenmesini destekler.Bu sayede, imza farklı olduğu sürece aynı işlevi birden çok kez uygulayabilirsiniz (ör. farklı yazılan parametreler). Derleyici düzeyinde add gibi güzel bir ad, bağlayıcının işlev adındaki imzayı kodlayan bir şeye karıştırılır. Sonuç olarak işlevimizi artık adıyla birlikte arayamazdık.

embind girin

embind, Emscripten araç zincirinin bir parçasıdır ve C++ koduna ek açıklama girmenize olanak tanıyan birçok C++ makrosu sağlar. JavaScript'ten kullanmayı planladığınız işlevleri, enum'ları, sınıfları veya değer türlerini belirtebilirsiniz. Bazı basit işlevlerle basit bir şekilde başlayalım:

#include <emscripten/bind.h>

using namespace emscripten;

double add(double a, double b) {
    return a + b;
}

std::string exclaim(std::string message) {
    return message + "!";
}

EMSCRIPTEN_BINDINGS(my_module) {
    function("add", &add);
    function("exclaim", &exclaim);
}

Önceki makalemle karşılaştırıldığında, işlevlerimize artık EMSCRIPTEN_KEEPALIVE ile ek açıklama eklemek zorunda kalmadığımızdan emscripten.h öğesini dahil etmiyoruz. Bunun yerine, işlevlerimizi JavaScript'e sunmak istediğimiz adları listeleyen bir EMSCRIPTEN_BINDINGS bölümümüz var.

Bu dosyayı derlemek için önceki makalede kullanılan kurulumu (veya isterseniz aynı Docker görüntüsünü) kullanabiliriz. Embind'i kullanmak için --bind işaretini ekliyoruz:

$ emcc --bind -O3 add.cpp

Şimdi, yeni oluşturulan wasm modülümüzü yükleyen bir HTML dosyasını çırpmak kaldı.

<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
    console.log(Module.add(1, 2.3));
    console.log(Module.exclaim("hello world"));
};
</script>

Gördüğünüz gibi artık cwrap() kullanmıyoruz. Bu, anında çalışır. Ancak daha önemlisi, dizelerin çalışması için bellek parçalarını manuel olarak kopyalamaktan endişe etmemiz gerekmiyor. embind, bunu tür denetimleriyle birlikte ücretsiz olarak sunar:

Bir işlevi yanlış sayıda bağımsız değişkenle çağırdığınızda veya bağımsız değişkenler yanlış türde

Bu, zaman zaman oldukça güç olmayan wasm hatalarıyla uğraşmak yerine bazı hataları erkenden yakalayabildiğimiz için çok güzel.

Nesneler

Birçok JavaScript oluşturucu ve işlevi, seçenek nesnelerini kullanır. Bu, JavaScript'te güzel bir kalıp, ancak wasm'da manuel olarak gerçekleştirmek çok zahmetli. Burada embind de yardımcı olabilir!

Örneğin, dizelerimi işleyen bu son derece kullanışlı C++ işlevini buldum ve acilen web'de kullanmak istiyorum. Bunu şu şekilde yaptım:

#include <emscripten/bind.h>
#include <algorithm>

using namespace emscripten;

struct ProcessMessageOpts {
    bool reverse;
    bool exclaim;
    int repeat;
};

std::string processMessage(std::string message, ProcessMessageOpts opts) {
    std::string copy = std::string(message);
    if(opts.reverse) {
    std::reverse(copy.begin(), copy.end());
    }
    if(opts.exclaim) {
    copy += "!";
    }
    std::string acc = std::string("");
    for(int i = 0; i < opts.repeat; i++) {
    acc += copy;
    }
    return acc;
}

EMSCRIPTEN_BINDINGS(my_module) {
    value_object<ProcessMessageOpts>("ProcessMessageOpts")
    .field("reverse", &ProcessMessageOpts::reverse)
    .field("exclaim", &ProcessMessageOpts::exclaim)
    .field("repeat", &ProcessMessageOpts::repeat);

    function("processMessage", &processMessage);
}

processMessage() işlevimin seçenekleri için bir yapı tanımlıyorum. EMSCRIPTEN_BINDINGS blokunda, JavaScript'in bu C++ değerini nesne olarak görmesini sağlamak için value_object kullanabilirim. Bu C++ değerini dizi olarak kullanmayı tercih edersem value_array kullanabilirim. Ayrıca processMessage() işlevini de bağlıyorum, gerisi de sihir sığdırmak. Artık processMessage() işlevini, herhangi bir ortak metin kodu olmadan JavaScript'ten çağırabilirim:

console.log(Module.processMessage(
    "hello world",
    {
    reverse: false,
    exclaim: true,
    repeat: 3
    }
)); // Prints "hello world!hello world!hello world!"

Sınıflar

Bütünsellik olsun diye embind özelliğinin tüm sınıfları ortaya çıkarmanıza nasıl olanak tanıdığını da göstermek isterim. Bu da ES6 sınıflarıyla büyük bir sinerji sağlar. Büyük ihtimalle artık bir kalıp görmeye başlayabilirsiniz:

#include <emscripten/bind.h>
#include <algorithm>

using namespace emscripten;

class Counter {
public:
    int counter;

    Counter(int init) :
    counter(init) {
    }

    void increase() {
    counter++;
    }

    int squareCounter() {
    return counter * counter;
    }
};

EMSCRIPTEN_BINDINGS(my_module) {
    class_<Counter>("Counter")
    .constructor<int>()
    .function("increase", &Counter::increase)
    .function("squareCounter", &Counter::squareCounter)
    .property("counter", &Counter::counter);
}

JavaScript tarafında, bu neredeyse yerel bir sınıf gibidir:

<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
    const c = new Module.Counter(22);
    console.log(c.counter); // prints 22
    c.increase();
    console.log(c.counter); // prints 23
    console.log(c.squareCounter()); // prints 529
};
</script>

Peki ya C?

embind C++ için yazılmıştır ve yalnızca C++ dosyalarında kullanılabilir. Ancak bu, C dosyalarına bağlantı oluşturamayacağınız anlamına gelmez! C ve C++'ı birlikte kullanmak için giriş dosyalarınızı yalnızca C için bir, C++ dosyaları için olmak üzere iki gruba ayırmanız gerekir. Ardından emcc için CLI işaretlerini şu şekilde güçlendirin:

$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp

Sonuç

embind, wasm ve C/C++ ile çalışırken geliştirici deneyiminde harika iyileştirmeler sağlar. Bu makalede, teklifleri içeren tüm seçenekler ele alınmamaktadır. Bu konu ilginizi çekiyorsa embind'in belgelerine göz atmanızı öneririz. embind'i kullandığınızda, gzip'lediğinizde (özellikle küçük modüllerde) hem wasm modülünüzü hem de JavaScript yapışkan kodunuzu 11.000'e kadar büyütebileceğinizi unutmayın. Yalnızca çok küçük bir wasm yüzeyiniz varsa embind, bir üretim ortamındaki değerinden daha yüksek bir maliyete sahip olabilir. Yine de, kesinlikle denemelisiniz.