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

Dış dünyayla iletişim kurmak için WebAssembly kitaplığınıza JavaScript kodu yerleştirmeyi öğrenin.

Ingvar Stepanyan
Ingvar Stepanyan

WebAssembly entegrasyonu üzerinde çalışırken, web API'leri ve üçüncü taraf kitaplıklar gibi harici API'leri çağırmak için bir yönteme ihtiyacınız vardır. Bu durumda, 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 vardır. Eşzamansız API'ler için Asyncify ile eşzamanlı C/C++ kodunuzda vaatleri beklemeniz ve işlem tamamlandığında sonucu okumanız gerekebilir.

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

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

Bu gönderide, benzer görevler için bu özelliklerin hepsini nasıl kullanacağınızı öğreneceksiniz.

emscripten::val sınıfı

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

Bazı JSON'ları getirmek ve ayrıştırmak için bunu, Asyncify'ın .await() özelliğiyle nasıl kullanacağınız aşağıda açıklanmıştır:

#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 sonuç verir ancak birçok ara adım gerçekleştirir. 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 bir biçime 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++'a döndürün; C++ en sonunda bu sonucu yeniden okur.

Her await() ayrıca WebAssembly modülünün çağrı yığınının tamamını kaldırarak, JavaScript'e dönerek, bekleyerek ve işlem tamamlandığında WebAssembly yığınını geri yükleyerek C++ tarafını duraklatması gerekir.

Bu tür bir kod için C++'tan herhangi 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 tanımlamanızı sağlar.

WebAssembly'nin kendisi gibi, sadece sayısal bağımsız değişkenleri ve döndürülen değerleri desteklemeyle ilgili 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.

Aktarılan sayılar için dönüşüm 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 dizeleri iletirken preamble.js'deki karşılık gelen dönüşüm 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. Bu aracı kullanarak, JavaScript değerlerini ve C++ sınıflarını ara herkese açık kullanıcı adlarına ve geri dönüşlere 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 bulundurularak fetch_json örneği, JavaScript'ten ayrılmadan çoğu işi 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 bir kez duraklatılması yeterlidir.

EM_ASYNC_JS makrosu

Güzel görünmeyen tek parça Asyncify.handleAsync sarmalayıcıdır. Tek amacı async JavaScript işlevlerinin Asyncify ile çalıştırılmasına olanak tanımaktır. Hatta bu kullanım alanı o kadar yaygındır ki artık bunları birleştiren özel bir EM_ASYNC_JS makrosu bulunmaktadır.

fetch örneğinin son sürümünü üretmek için bunu ş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 önerilen yöntem EM_JS şeklindedir. Bildirilen snippet'leri diğer tüm JavaScript işlevi içe aktarma işlemleri gibi doğrudan bağladığı için verimlidir. Ayrıca tüm parametre türlerini ve adlarını açık bir şekilde tanımlamanıza olanak tanıyarak iyi ergonomik bilgiler sağlar.

Ancak bazı durumlarda, console.log çağrısı, debugger; ifadesi veya benzer bir ifade için hızlı bir snippet ekleyebilir ve tamamen ayrı bir işlev tanımlamak zorunda kalmazsınız. 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ür.

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

Dönüş türünü seçmek için doğru makro adını kullanmanız gerekir. EM_ASM bloklarının void işlevleri gibi davranması beklenir. EM_ASM_INT blokları tam sayı değeri döndürebilir ve EM_ASM_DOUBLE blokları da kayan nokta sayılarını uygun şekilde döndürür.

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

Aşağıda, rastgele bir JS değerini konsola günlüğe kaydetmek için bir EM_ASM makrosu nasıl kullanabileceğinize ilişkin bir örnek verilmiştir:

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 kendi kitaplık biçiminde ayrı bir dosyada tanımlanmasını destekler:

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

Daha sonra, karşılık gelen prototipleri C++ tarafında manuel olarak bildirmeniz gerekir:

extern "C" void log_value(EM_VAL val_handle);

JavaScript kitaplığı, her iki tarafta da belirtildikten sonra --js-library option aracılığıyla ana koda bağlanarak prototipleri karşılık gelen JavaScript uygulamalarına bağlayabilir.

Bununla birlikte, bu modül biçimi standart değildir ve bağımlılık ek açıklamalarının dikkatli bir şekilde ele alınmasını gerektirir. Bu nedenle, çoğunlukla gelişmiş senaryolar için ayrılmıştır.

Sonuç

Bu yayında, WebAssembly ile çalışırken JavaScript kodunu C++'a entegre etmenin çeşitli yollarını inceledik.

Bu tür snippet'lerin dahil edilmesi, uzun işlem sıralarını daha net ve verimli bir şekilde ifade etmenize ve üçüncü taraf kitaplıklardan, yeni JavaScript API'lerinden, hatta henüz C++ veya Embind ile ifade edilemeyen JavaScript söz dizimi özelliklerinden yararlanmanızı sağlar.