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

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

Ingvar Stepanyan
Ingvar Stepanyan

WebAssembly'i web ile entegre ederken web API'leri ve üçüncü taraf kitaplıkları gibi harici API'leri çağırmanın bir yoluna ihtiyacınız vardır. Ardından, bu API'lerin döndürdüğü değerleri ve nesne örneklerini depolayacak ve daha sonra bu depolanan değerleri diğer API'lere iletecek 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ıktan sonra sonucu okumanız da gerekebilir.

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

  • C++'ta JavaScript değerlerini depolamak ve bunlar üzerinde çalıştırmak için emscripten::val.
  • EM_JS JavaScript snippet'lerini yerleştirmek ve bunları C/C++ işlevi olarak bağlamak için.
  • EM_JS'ye benzer ancak eşzamansız JavaScript snippet'lerini yerleştirmeyi kolaylaştıran EM_ASYNC_JS.
  • EM_ASM kısa snippet'leri yerleştirmek ve işlev tanımlamadan satır içi olarak yürütmek için kullanılır.
  • Ç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 ç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 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++'ya döndürün ve C++ sonunda sonucu geri okur.

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

Bu tür kodlar için C++'dan herhangi bir şeye ihtiyaç yoktur. C++ kodu yalnızca bir dizi JavaScript işlemi için sürücü görevi görür. fetch_json'ü JavaScript'e taşıyıp aynı zamanda ara adımların yükünü azaltabilseydiniz ne 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şlevi tanımlamanıza olanak tanır.

WebAssembly'in kendisi gibi, yalnızca sayısal bağımsız değişkenleri ve döndürülen değerleri destekleme sınırlamasına sahiptir. 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.

Geçilen sayıların dönüştürülmesi 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 dize aktarırken preamble.js'deki ilgili dönüşüm ve tahsis 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, rastgele değer türleri için daha önce bahsedilen val sınıfı için JavaScript API'sini 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, çoğu işi JavaScript'den ayrılmadan 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 birkaç açık dönüşümümüz hâlâ var ancak geri kalanı 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ı gerekir.

EM_ASYNC_JS makrosu

Geriye kalan ve pek hoş görünmeyen tek şey Asyncify.handleAsync sarmalayıcısıdır. Tek amacı, async JavaScript işlevlerinin Asyncify ile yürütülmesine izin vermektir. 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 nihai sürümünü oluşturmak için bu işlevi nasıl kullanabileceğinizi aşağıda görebilirsiniz:

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

EM_JS, JavaScript snippet'lerini tanımlamak için önerilen yöntemdir. Tanımlanmış 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 eklemek ve tamamen ayrı bir işlev tanımlamak istemiyorsanız bu işlemi yapabilirsiniz. Bu nadir durumlarda, EM_ASM macros family (EM_ASM, EM_ASM_INT ve EM_ASM_DOUBLE) daha basit bir seçenek olabilir. Bu makrolar EM_JS makrosuna benzer ancak bir işlev tanımlamak yerine, kodları eklendikleri satırda yürütürler.

İş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öndürülen türün seçilmesi 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 buna göre kayan noktalı sayılar döndürür.

İletilen tüm 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 (tam sayılar, kayan noktalı sayılar, işaretçiler ve tutamaçlar) sınırlıdır.

Aşağıda, rastgele bir JS değerini konsola kaydetmek için EM_ASM makrosunu nasıl kullanabileceğinize dair 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 kodunu kendi özel kitaplık biçiminde ayrı bir dosyada tanımlamayı 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);

Her iki tarafta da tanımlandıktan sonra JavaScript kitaplığı, prototipleri ilgili JavaScript uygulamalarıyla bağlayarak --js-library option aracılığıyla ana kodla bağlanabilir.

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ş 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'leri dahil etmek, uzun işlem dizilerini daha temiz ve verimli bir şekilde ifade etmenize, üçüncü taraf kitaplıklarına, yeni JavaScript API'lerine ve hatta henüz C++ veya Embind ile ifade edilememiş JavaScript söz dizimi özelliklerine erişmenize olanak tanır.