JS'yi wasm'nize bağlar.
Son wasm makalemde, web'de kullanabilmeniz için bir C kitaplığını wasm'ye nasıl derleyeceğinizden bahsetmiştim. Bana (ve birçok okuyucuya) garip gelen bir nokta, wasm modülünüzün hangi işlevlerini kullandığınızı manuel olarak bildirmeniz gerektiğiydi. Hatırlatmak gerekirse bahsettiğim kod snippet'i şu:
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 tanımlıyoruz. Ardından, bu işlevleri çağırmak için api
nesnesindeki yöntemleri kullanabiliriz. Ancak wasm'ı 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? Evet, aksi takdirde bu makale ne hakkında olurdu?
C++ ad değiştirme
Geliştirici deneyimi, bu bağlamalara yardımcı olan bir araç oluşturmak için yeterli bir neden olsa da aslında daha acil bir neden vardır: C veya C++ kodu derlediğinizde her dosya ayrı ayrı derlenir. Ardından, bir bağlayıcı, bu sözde nesne dosyalarının tümünü bir araya getirip wasm dosyasına dönüştürür. C ile, işlevlerin adları bağlayıcının kullanması için nesne dosyasında hala kullanılabilir. Bir C işlevini çağırabilmek için tek ihtiyacınız olan şey, cwrap()
işlevine dize olarak sağladığımız addır.
Diğer yandan C++, işlev aşırı yüklemesini destekler.Bu da imza farklı olduğu sürece (ör. farklı türde parametreler) aynı işlevi birden çok kez uygulayabileceğiniz anlamına gelir. Derleyici düzeyinde, add
gibi güzel bir ad, bağlayıcı için işlev adında imzayı kodlayan bir şeye bozulur. Bu nedenle, işlevimizi artık adıyla arayamayız.
embind'i girin
embind, Emscripten araç zincirinin bir parçasıdır ve C++ kodunu açıklama eklemenize olanak tanıyan bir dizi C++ makrosu sağlar. JavaScript'ten hangi işlevleri, numaralandırmaları, sınıfları veya değer türlerini kullanmayı planladığınızı belirtebilirsiniz. Basit fonksiyonlarla 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 makaleme kıyasla, artık işlevlerimizi EMSCRIPTEN_KEEPALIVE
ile ek açıklamalı hale getirmemiz gerekmediğinden emscripten.h
öğesini dahil etmiyoruz.
Bunun yerine, işlevlerimizi JavaScript'e sunmak istediğimiz adları listelediğimiz bir EMSCRIPTEN_BINDINGS
bölümümüz var.
Bu dosyayı derlemek için önceki makalede kullanılan kurulumun aynısını (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 yapmamız gereken tek şey, yeni oluşturduğumuz wasm modülünü 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ı için bellek parçalarını manuel olarak kopyalama konusunda endişelenmemize gerek kalmaz. embind, tür kontrolleriyle birlikte bunu ücretsiz olarak sunar:

Bu, bazen oldukça hantal olan wasm hatalarıyla uğraşmak yerine bazı hataları erken yakalayabildiğimiz için oldukça iyi bir durum.
Nesneler
Birçok JavaScript oluşturucusu ve işlevi, seçenek nesnelerini kullanır. Bu, JavaScript'te güzel bir kalıptır ancak wasm'de manuel olarak gerçekleştirmek son derece sıkıcıdır. Bu konuda da embind yardımcı olabilir.
Örneğin, dizelerimi işleyen son derece kullanışlı bir C++ işlevi geliştirdim ve bunu web'de acilen kullanmak istiyorum. Bu işlemi ş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
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 kullanmayı tercih edersem value_array
öğesini de kullanabilirim. Ayrıca processMessage()
işlevini de bağlıyorum ve gerisi sihirli bir şekilde bağlanıyor. Artık processMessage()
işlevini JavaScript'ten herhangi bir standart kod olmadan ç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ı için, embind'in tüm sınıfları kullanıma sunmanıza nasıl olanak tanıdığını da göstermem gerekiyor. Bu, ES6 sınıflarıyla büyük bir sinerji yaratır. Muhtemelen artık bir kalıp görmeye başlıyorsunuzdur:
#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++'ı karıştırmak için giriş dosyalarınızı iki gruba ayırmanız yeterlidir: Biri C, diğeri C++ dosyaları için. Ayrıca, emcc
için CLI işaretlerini aşağıdaki gibi artırın:
$ 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ını incelemenizi öneririz. embind kullanmanın, gzip ile sıkıştırıldığında hem wasm modülünüzü hem de JavaScript yapıştırma kodunuzu 11k'ya kadar büyütebileceğini (en çok da küçük modüllerde) unutmayın. Yalnızca çok küçük bir wasm yüzeyiniz varsa embind, üretim ortamında değerinden daha pahalıya mal olabilir. Bununla birlikte, kesinlikle denemenizi öneririz.