Emscripten'ın evi

JS'yi wasm'inize bağlar.

Son wasm makalemde, bir C kitaplığını web'de kullanabilmeniz için wasm'e nasıl derleyeceğinizden bahsetmiştim. Benim (ve birçok okuyucunun) dikkatini çeken bir nokta, wasm modülünüzün hangi işlevlerini kullandığınızı manuel olarak beyan etmeniz gereken kaba ve biraz garip yöntemdi. Bahsettiğim kod snippet'ini hatırlatmak isterim:

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ürülen türlerini ve bağımsız değişkenlerinin türlerini açıklarız. Ardından, bu işlevleri çağırmak için api nesnesindeki yöntemleri kullanabiliriz. Ancak wasm'i bu şekilde kullanmak dizeleri desteklemez ve bellek parçalarını manuel olarak taşımanızı gerektirir. Bu da birçok kitaplık API'sinin kullanımını çok sıkıcı hale getirir. Daha iyi bir yol yok mu? Elbette var. Aksi takdirde bu makale ne hakkında olurdu?

C++ ad değiştirme

Geliştirici deneyimi, bu bağlamalarda yardımcı olacak bir araç oluşturmak için yeterli bir neden olsa da aslında daha acil bir neden var: C veya C++ kodunu derlediğinizde her dosya ayrı ayrı derlenir. Ardından, bir bağlayıcı tüm bu nesne dosyalarını bir araya getirip bir wasm dosyasına dönüştürür. C'de, işlevlerin adları bağlayıcının kullanması için nesne dosyasında kalır. Bir C işlevini çağırabilmek için tek ihtiyacınız işlevin adı. Bu adı cwrap() için dize olarak sağlıyoruz.

Öte yandan C++, işlev yükü atamayı destekler.Yani imza farklı olduğu sürece (ör. farklı türde parametreler) aynı işlevi birden çok kez uygulayabilirsiniz. Derleyici düzeyinde, add gibi güzel bir ad, bağlayıcı için işlev adındaki imzayı kodlayan bir adla karmaşıklaştırılır. Bu nedenle, işlevimizi artık adıyla arayamayız.

Yerleştirmeyi girin

embind, Emscripten araç zincirinin bir parçasıdır ve C++ koduna ek açıklama eklemenize olanak tanıyan bir dizi C++ makrosu sağlar. JavaScript'den kullanmayı planladığınız işlevleri, enum'ları, sınıfları veya değer türlerini belirtebilirsiniz. Basit işlevlerle 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 makalemize kıyasla, artık işlevlerimizi EMSCRIPTEN_KEEPALIVE ile açıklamamız gerekmediğinden emscripten.h eklemiyoruz. Bunun yerine, işlevlerimizi JavaScript'e göstermek istediğimiz adları listelediğimiz bir EMSCRIPTEN_BINDINGS bölümüne sahibiz.

Bu dosyayı derlemek için önceki makalede kullandığımız ayarları (veya isterseniz aynı Docker görüntüsünü) kullanabiliriz. Embind'i kullanmak için --bind işaretini ekleriz:

$ emcc --bind -O3 add.cpp

Şimdi tek yapmanız gereken, yeni oluşturulan wasm modülümüzü yükleyen bir HTML dosyası oluşturmak:

<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 özellik, kutudan çıkar çıkmaz çalışır. Ancak daha da önemlisi, dizelerin çalışmasını sağlamak için bellek parçalarını manuel olarak kopyalama konusunda endişelenmenize gerek yok. embind, tür kontrolleriyle birlikte bunu ü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 olduğunda DevTools hataları

Bazen oldukça zor olan wasm hatalarıyla uğraşmak yerine bazı hataları erken yakalayabildiğimiz için bu oldukça faydalı.

Nesneler

Birçok JavaScript kurucu ve işlevi seçenek nesneleri kullanır. JavaScript'te güzel bir kalıptır ancak wasm'de manuel olarak uygulanması son derece zahmetli bir işlemdir. embind burada da yardımcı olabilir.

Örneğin, dizelerini işleyen bu inanılmaz yararlı 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ıyorum. EMSCRIPTEN_BINDINGS bloğunda, JavaScript'in bu C++ değerini nesne olarak görmesini sağlamak için value_object kullanabilirim. Bu C++ değerini dizi olarak kullanmak isteseydim value_array değerini de kullanabilirdim. processMessage() işlevini de bağlarım. Gerisi sihir. Artık processMessage() işlevini herhangi bir standart kod olmadan JavaScript'ten çağırabiliyorum:

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

Sınıflar

Tamlık açısından, embind'in tüm sınıfları nasıl göstermenize olanak tanıdığını da göstermemiz gerekir. Bu, ES6 sınıflarıyla çok fazla sinerji sağlar. Muhtemelen bir kalıp görmeye başlamışsınızdır:

#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 gibi görünür:

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

C ne olacak?

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++'yu karıştırmak için giriş dosyalarınızı iki gruba ayırmanız yeterlidir: Biri C, diğeri C++ dosyaları için. Ardından, emcc için CLI işaretlerini aşağıdaki gibi ekleyin:

$ 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 büyük iyileştirmeler sağlar. Bu makalede, embind'in sunduğu tüm seçenekler ele alınmamaktadır. İlgileniyorsanız embind'in dokümanlarına göz atmanızı öneririz. Embind'i kullanırken, özellikle küçük modüllerde gzip'lendiğinde hem wasm modülünüzün hem de JavaScript yapıştırma kodunuzun 11 bin kadar daha büyük olabileceğini unutmayın. Çok küçük bir wasm yüzeyiniz varsa embind, üretim ortamında değerinden daha fazla maliyete neden olabilir. Yine de denemeye değer.