Emscripten ile C++'ya JavaScript snippet'leri yerleştirme

Dış dünyayla iletişim kurmak için JavaScript kodunu WebAssembly kitaplığınıza nasıl yerleştireceğinizi öğrenin.

İngvar Stepanyan
Ingvar Stepanyan

Web ile WebAssembly entegrasyonu üzerinde çalışırken, web API'leri ve üçüncü taraf kitaplıkları gibi harici API'lere çağrı gönderebileceğiniz bir yöntem gerekir. Ardından, bu API'lerin döndürdüğü değerleri ve nesne örneklerini depolamak için bir yönteme ve depolanan bu değerleri daha sonra diğer API'lere iletmek için bir yönteme ihtiyacınız olur. Eşzamansız API'ler için ayrıca Asyncify ile eşzamanlı C/C++ kodunuzda sözleri beklemeniz ve işlem tamamlandıktan sonra sonucu okumanız gerekebilir.

Emscripten, bu tür etkileşimler için çeşitli araçlar sağlar:

  • C++'ta JavaScript değerlerini depolamak ve bunlar üzerinde çalışmak için emscripten::val.
  • JavaScript snippet'lerini yerleştirmek ve bunları C/C++ işlevleri olarak bağlamak için EM_JS.
  • EM_ASYNC_JS, EM_JS ile benzerdir, ancak eşzamansız JavaScript snippet'lerinin yerleştirilmesini kolaylaştırır.
  • Kısa snippet'leri yerleştirmek ve bunları bir işlev bildirmeksizin satır içinde yürütmek için EM_ASM.
  • Çok sayıda JavaScript işlevini tek bir kitaplık olarak tanımlamak istediğiniz ileri düzey senaryolar için --js-library.

Bu gönderide, benzer görevler için bunların tümünü nasıl kullanacağınızı öğreneceksiniz.

emscripten::val sınıfı

emcripten::val sınıfı Embind tarafından sağlanmaktadır. Genel API'leri çağırabilir, JavaScript değerlerini C++ örneklerine bağlayabilir, değerleri C++ ve JavaScript türleri arasında dönüştürebilir.

JSON'un bazı JSON öğelerini getirmek ve ayrıştırmak için Asyncify .await() ile nasıl kullanılacağını buradan öğrenebilirsiniz:

#include <emscripten/val.h>

using namespace emscripten;

val fetch_json(const char *url) {
  // Get and cache a binding to the global `fetch` API in each thread.
  thread_local const val fetch = val::global("fetch");
  // Invoke fetch and await the returned `Promise<Response>`.
  val response = fetch(url).await();
  // Ask to read the response body as JSON and await the returned `Promise<any>`.
  val json = response.call<val>("json").await();
  // Return the JSON object.
  return json;
}

// Example URL.
val example_json = fetch_json("https://httpbin.org/json");

// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();

Bu kod iyi çalışıyor, ancak birçok ara adım gerçekleştiriyor. val üzerindeki her işlemin aşağıdaki adımları gerçekleştirmesi gerekir:

  1. Bağımsız değişken olarak iletilen C++ değerlerini ara biçimlere dönüştürün.
  2. JavaScript'e gidin, bağımsız değişkenleri okuyup JavaScript değerlerine dönüştürün.
  3. İşlevi yürütme
  4. Sonucu JavaScript'ten ara biçime dönüştürün.
  5. Dönüştürülen sonucu C++'ya döndürün, C++ sonunda sonucu tekrar okur.

Her await(); WebAssembly modülünün çağrı yığınının tamamını gevşeterek, JavaScript'e geri dönerek, işlem tamamlandığında WebAssembly yığınını geri yükleyerek C++ tarafını duraklatmalıdır.

Bu tür kodlar için C++'tan hiçbir şey gerekmez. C++ kodu yalnızca bir dizi JavaScript işlemi için sürücü görevi görür. fetch_json öğesini JavaScript'e taşıyıp ara adımların ek yükünü aynı anda azaltabilseydiniz nasıl olurdu?

EM_JS makrosu

EM_JS macro, fetch_json öğesini JavaScript'e taşımanıza olanak tanır. Emscripten'deki EM_JS, JavaScript snippet'i tarafından uygulanan bir C/C++ işlevini bildirmenizi sağlar.

WebAssembly'nin kendisi gibi, yalnızca sayısal bağımsız değişkenleri ve dönüş değerlerini destekleyen bir sınırlama vardır. Diğer değerleri iletmek için bunları ilgili API'ler aracılığıyla manuel olarak dönüştürmeniz gerekir. Aşağıda birkaç örnek verilmiştir.

İletim numaraları için herhangi bir dönüştürme gerekmez:

// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
  return x + 1;
});

int x = add_one(41);

JavaScript'e ve JavaScript'ten dizeler iletirken preamble.js'deki karşılık gelen dönüştürme ve ayırma işlevlerini kullanmanız gerekir:

EM_JS(void, log_string, (const char *msg), {
  console.log(UTF8ToString(msg));
});

EM_JS(const char *, get_input, (), {
  let str = document.getElementById('myinput').value;
  // Returns heap-allocated string.
  // C/C++ code is responsible for calling `free` once unused.
  return allocate(intArrayFromString(str), 'i8', ALLOC_NORMAL);
});

Son olarak, daha karmaşık ve rastgele değer türleri için daha önce bahsedilen val sınıfı için JavaScript API'yi kullanabilirsiniz. Bunu kullanarak JavaScript değerlerini ve C++ sınıflarını ara işleyicilere ve arkaya dönüştürebilirsiniz:

EM_JS(void, log_value, (EM_VAL val_handle), {
  let value = Emval.toValue(val_handle);
  console.log(value);
});

EM_JS(EM_VAL, find_myinput, (), {
  let input = document.getElementById('myinput');
  return Emval.toHandle(input);
});

val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
log_value(obj.as_handle()); // logs { x: 1, y: 2 }

val myinput = val::take_ownership(find_input());
// Now you can store the `find_myinput` DOM element for as long as you like, and access it later like:
std::string value = input["value"].as<std::string>();

Bu API'ler göz önünde bulundurulduğunda fetch_json örneği, çoğu işi JavaScript'ten çıkmadan yapacak şekilde yeniden yazılabilir:

EM_JS(EM_VAL, fetch_json, (const char *url), {
  return Asyncify.handleAsync(async () => {
    url = UTF8ToString(url);
    // Invoke fetch and await the returned `Promise<Response>`.
    let response = await fetch(url);
    // Ask to read the response body as JSON and await the returned `Promise<any>`.
    let json = await response.json();
    // Convert JSON into a handle and return it.
    return Emval.toHandle(json);
  });
});

// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));

// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();

İşlevin giriş ve çıkış noktalarında hâlâ birkaç açık dönüşümümüz var, ancak geri kalanlar artık normal JavaScript kodu. val eşdeğerinin aksine, artık JavaScript motoru tarafından optimize edilebilir ve tüm eşzamansız işlemler için C++ tarafının yalnızca bir kez duraklatılmasını gerektirir.

EM_ASYNC_JS makrosu

Geriye, hoş görünmeyen tek bit kalan Asyncify.handleAsync sarmalayıcıdır. Bunun tek amacı, Asyncify ile async JavaScript işlevlerinin yürütülmesine izin vermektir. Aslında, bu kullanım alanı o kadar yaygındır ki artık bunları bir araya getiren özel bir EM_ASYNC_JS makrosu vardır.

fetch örneğinin son halini oluşturmak için bu dosyayı şu şekilde kullanabilirsiniz:

EM_ASYNC_JS(EM_VAL, fetch_json, (const char *url), {
  url = UTF8ToString(url);
  // Invoke fetch and await the returned `Promise<Response>`.
  let response = await fetch(url);
  // Ask to read the response body as JSON and await the returned `Promise<any>`.
  let json = await response.json();
  // Convert JSON into a handle and return it.
  return Emval.toHandle(json);
});

// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));

// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();

EM_ASM

JavaScript snippet'lerini bildirmek için EM_JS kullanılması önerilir. Verimlidir çünkü bildirilen snippet'leri diğer tüm JavaScript işlevlerinin içe aktarma işlemleri gibi doğrudan bağlar. Ayrıca tüm parametre türlerini ve adlarını açıkça belirtmenize olanak tanıyarak iyi bir ergonomik sunar.

Ancak bazı durumlarda console.log çağrısı, debugger; ifadesi veya benzer bir ifade için kısa snippet eklemek isteyebilir ve tamamen ayrı bir işlevin beyan edilmesiyle uğraşmak istemezsiniz. Nadiren de olsa EM_ASM macros family (EM_ASM, EM_ASM_INT ve EM_ASM_DOUBLE) daha basit bir seçim olabilir. Bu makrolar EM_JS makrosuna benzer, ancak bir işlev tanımlamak yerine kodu eklendikleri yerde satır içinde yürütürler.

İşlev prototipi bildirmedikleri için, dönüş türünü belirtmek ve bağımsız değişkenlere erişmek için farklı bir yola ihtiyaçları vardır.

Döndürülen türü seçmek için doğru makro adını kullanmanız gerekir. EM_ASM blokların void işlevleri gibi davranması beklenir, EM_ASM_INT blokları bir tam sayı değeri ve EM_ASM_DOUBLE bloklarının buna karşılık gelen kayan noktalı sayılar döndürmesi gerekir.

İletilen bağımsız değişkenler, JavaScript gövdesinde $0, $1 vb. adlar altında kullanılabilecektir. Genel olarak EM_JS veya WebAssembly'de olduğu gibi, bağımsız değişkenler yalnızca sayısal değerlerle (tam sayılar, kayan nokta sayıları, işaretçiler ve tutamaçlar) sınırlıdır.

Rastgele bir JS değerini konsola kaydetmek için EM_ASM makrosunu nasıl kullanabileceğinize dair bir örneği burada bulabilirsiniz:

val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
// executes inline immediately
EM_ASM({
  // convert handle passed under $0 into a JavaScript value
  let obj = Emval.fromHandle($0);
  console.log(obj); // logs { x: 1, y: 2 }
}, obj.as_handle());

--js-library

Son olarak, Emscripten, JavaScript kodunun ayrı bir dosyada, özel bir kitaplık biçiminde beyan edilmesini destekler:

mergeInto(LibraryManager.library, {
  log_value: function (val_handle) {
    let value = Emval.toValue(val_handle);
    console.log(value);
  }
});

Ardından, C++ tarafında karşılık gelen prototipleri manuel olarak tanımlamanız gerekir:

extern "C" void log_value(EM_VAL val_handle);

Her iki tarafta da beyan edildikten sonra JavaScript kitaplığı, --js-library option üzerinden ana koda bağlanarak prototipleri ilgili JavaScript uygulamalarına bağlayabilir.

Ancak bu modül biçimi standart değildir ve dikkatli bağımlılık ek açıklamaları gerektirir. Bu nedenle, çoğunlukla gelişmiş senaryolara ayrılmıştır.

Sonuç

Bu gönderide, WebAssembly ile çalışırken JavaScript kodunu C++'ya entegre etmenin çeşitli yollarını inceledik.

Bu tür snippet'ler eklemek, uzun işlem dizilerini daha net ve etkili bir şekilde ifade etmenize ve üçüncü taraf kitaplıklardan, yeni JavaScript API'lerinden ve hatta henüz C++ veya Embind ile ifade edilemeyen JavaScript söz dizimi özelliklerinden yararlanmanıza olanak tanır.