Emscripten'ın evi

JS'yi wasm'inize bağlar.

Son Wasm makalemde, web'de kullanabilmeniz için C kitaplığını wasm olarak nasıl derleyeceğinizden bahsettim. 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önüş 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. Bununla birlikte, wasm'ın bu şekilde kullanılması dizeleri desteklemez ve bellek parçalarını manuel olarak taşımanızı gerektirir. Bu durum, birçok kitaplık API'sinin kullanımını çok sıkıcı hale getirir. Daha iyi bir yol yok mu? Neden evet, aksi takdirde bu makalenin konusu ne olurdu?

C++ ad değiştirme

Geliştirici deneyimi bu bağlamalara yardımcı olacak bir araç oluşturmak için yeterli olsa da, aslında daha önemli bir neden vardır: C veya C++ kodunu derlediğinizde her dosya ayrı olarak 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 ile, işlevlerin adları bağlayıcının kullanması için nesne dosyasında kullanılmaya devam eder. 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 aşırı yüklenmesini destekler.Bu sayede, imza farklı olduğu sürece (ör. farklı yazılmış parametreler) aynı işlevi birden çok kez uygulayabilirsiniz. Derleyici düzeyinde, add gibi bir güzel 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 arayamayız.

Yerleştirmeyi girin

embind, Emscripten araç zincirinin bir parçasıdır ve size C++ kodlarına ek açıklama eklemenize olanak tanıyan bir dizi C++ makrosu sağlar. JavaScript'ten hangi işlevleri, enum'ları, sınıfları veya değer türlerini kullanmayı planladığınızı bildirebilirsiniz. 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ın listelendiği bir EMSCRIPTEN_BINDINGS bölümümüz var.

Bu dosyayı derlemek için önceki makalede yer alan kurulumu (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 yöntem hemen kullanmaya başlar. Ancak daha da önemlisi, dizelerin çalışması için bellek parçalarını manuel olarak kopyalamayla uğraşmamız gerekmiyor. embind, tür denetimleriyle birlikte bunu size ücretsiz olarak sağlar:

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

Bazen oldukça zorlanan wasm hatalarıyla uğraşmak yerine bazı hataları erkenden yakalayabildiğimiz için bu oldukça iyidir.

Nesneler

Birçok JavaScript kurucu ve işlevi seçenek nesneleri kullanır. Bu, JavaScript'te güzel bir kalıptır, ancak wasm'da manuel olarak gerçekleştirmek çok yorucudur. embind burada da yardımcı olabilir!

Örneğin, dizelerimi işleyen son derece kullanışlı bir C++ işlevi buldum ve bunu web'de acilen 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 bir dizi olarak kullanmayı tercih edersem value_array öğesini de kullanabilirim. processMessage() işlevini de bağlarım. Gerisi sihir. Artık processMessage() işlevini herhangi bir standart kod 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

Eksiksiz olması açısından, embind'nin sınıfların tamamını kullanmanıza nasıl olanak sağladığını da göstereceğim. Bu, ES6 sınıflarıyla büyük bir sinerji sağlar. Muhtemelen şimdiye kadar 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 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>

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++'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 belgeleriyle devam etmenizi öneririz. Embind'i kullandığınızda, gzip işlemi yapıldığında hem wasm modülünüzü hem de JavaScript yapışkan kodunuzu 11 bin'e kadar (özellikle de küçük modüllerde) büyütebileceğinizi unutmayın. Çok küçük bir wasm yüzeyiniz varsa embind, üretim ortamında değerinden daha fazla maliyete neden olabilir. Yine de kesinlikle denemelisiniz.