جاسازی قطعات جاوا اسکریپت در C++ با Emscripten

یاد بگیرید که چگونه کد جاوا اسکریپت را در کتابخانه WebAssembly خود برای برقراری ارتباط با دنیای خارج جاسازی کنید.

هنگام کار بر روی ادغام WebAssembly با وب، به راهی برای فراخوانی APIهای خارجی مانند وب API و کتابخانه های شخص ثالث نیاز دارید. سپس به راهی برای ذخیره مقادیر و نمونه‌های شیء که آن APIها برمی‌گردانند، و راهی برای انتقال آن مقادیر ذخیره‌شده به سایر APIها بعداً نیاز دارید. برای APIهای ناهمزمان، ممکن است لازم باشد در کد C/C++ همگام خود با Asyncify منتظر وعده‌ها باشید و پس از پایان عملیات، نتیجه را بخوانید.

Emscripten چندین ابزار برای چنین تعاملاتی فراهم می کند:

  • emscripten::val برای ذخیره و عملکرد مقادیر جاوا اسکریپت در C++.
  • EM_JS برای جاسازی قطعات جاوا اسکریپت و اتصال آنها به عنوان توابع C/C++.
  • EM_ASYNC_JS که شبیه EM_JS است، اما جاسازی قطعه‌های جاوا اسکریپت ناهمزمان را آسان‌تر می‌کند.
  • EM_ASM برای جاسازی قطعات کوتاه و اجرای آنها به صورت درون خطی، بدون اعلام تابع.
  • --js-library برای سناریوهای پیشرفته که می خواهید تعداد زیادی از توابع جاوا اسکریپت را با هم به عنوان یک کتابخانه واحد اعلام کنید.

در این پست یاد خواهید گرفت که چگونه از همه آنها برای کارهای مشابه استفاده کنید.

emscripten::val class

کلاس emcripten::val توسط Embind ارائه می شود. می تواند API های سراسری را فراخوانی کند، مقادیر جاوا اسکریپت را به نمونه های C++ متصل کند و مقادیر را بین انواع C++ و جاوا اسکریپت تبدیل کند.

در اینجا نحوه استفاده از آن با .await() Asyncify برای واکشی و تجزیه برخی از JSON آورده شده است:

#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>();

این کد به خوبی کار می کند، اما مراحل متوسط ​​زیادی را انجام می دهد. هر عملیات روی val باید مراحل زیر را انجام دهد:

  1. مقادیر C++ ارسال شده به عنوان آرگومان را به فرمت متوسط ​​تبدیل کنید.
  2. به جاوا اسکریپت بروید، آرگومان ها را بخوانید و به مقادیر جاوا اسکریپت تبدیل کنید.
  3. تابع را اجرا کنید
  4. نتیجه را از جاوا اسکریپت به فرمت متوسط ​​تبدیل کنید.
  5. نتیجه تبدیل شده را به C++ برگردانید و C++ در نهایت آن را دوباره می خواند.

هر await() همچنین باید با باز کردن کل پشته تماس ماژول WebAssembly، بازگشت به جاوا اسکریپت، انتظار و بازیابی پشته WebAssembly پس از اتمام عملیات، سمت C++ را متوقف کند.

چنین کدهایی به هیچ چیز از C++ نیاز ندارند. کد ++C فقط به عنوان یک درایور برای یک سری عملیات جاوا اسکریپت عمل می کند. اگر بتوانید fetch_json به جاوا اسکریپت منتقل کنید و همزمان سربار مراحل میانی را کاهش دهید چه؟

ماکرو EM_JS

EM_JS macro به شما امکان می دهد fetch_json به جاوا اسکریپت منتقل کنید. EM_JS در Emscripten به شما امکان می دهد یک تابع C/C++ را که توسط یک قطعه جاوا اسکریپت پیاده سازی می شود، اعلام کنید.

مانند خود WebAssembly، محدودیتی برای پشتیبانی از آرگومان های عددی و مقادیر بازگشتی دارد. برای ارسال مقادیر دیگر، باید آنها را به صورت دستی از طریق APIهای مربوطه تبدیل کنید. در اینجا چند نمونه آورده شده است.

ارسال اعداد نیازی به تبدیل ندارد:

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

int x = add_one(41);

هنگام ارسال رشته ها به و از جاوا اسکریپت، باید از توابع تبدیل و تخصیص مربوطه از preamble.js استفاده کنید:

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);
});

در نهایت، برای انواع پیچیده‌تر، دلخواه‌تر، می‌توانید از JavaScript API برای کلاس val استفاده کنید. با استفاده از آن، می توانید مقادیر جاوا اسکریپت و کلاس های C++ را به دسته های میانی و برگشتی تبدیل کنید:

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>();

با در نظر گرفتن این APIها، مثال fetch_json می تواند برای انجام بیشتر کارها بدون ترک جاوا اسکریپت بازنویسی شود:

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>();

ما هنوز چند تبدیل صریح در نقاط ورودی و خروجی تابع داریم، اما بقیه اکنون کدهای جاوا اسکریپت معمولی هستند. بر خلاف val equivalent، اکنون می‌توان آن را توسط موتور جاوا اسکریپت بهینه کرد و فقط نیاز به یک‌بار مکث سمت C++ برای همه عملیات‌های async دارد.

ماکرو EM_ASYNC_JS

تنها بیت باقی مانده که زیبا به نظر نمی رسد، پوشش Asyncify.handleAsync است—تنها هدف آن اجازه اجرای توابع جاوا اسکریپت async با Asyncify است. در واقع، این مورد آنقدر رایج است که اکنون یک ماکرو تخصصی EM_ASYNC_JS وجود دارد که آنها را با هم ترکیب می کند.

در اینجا نحوه استفاده از آن برای تولید نسخه نهایی مثال fetch آورده شده است:

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 روشی توصیه شده برای اعلان قطعات جاوا اسکریپت است. این کارآمد است زیرا قطعات اعلام شده را مستقیماً مانند سایر وارد کردن تابع جاوا اسکریپت متصل می کند. همچنین ارگونومی خوبی را با این امکان به شما می دهد که به صراحت تمام انواع پارامترها و نام ها را اعلام کنید.

با این حال، در برخی موارد، می‌خواهید یک قطعه سریع برای تماس console.log ، یک debugger; بیانیه یا چیزی مشابه و نمی‌خواهم با اعلان یک تابع مجزا زحمت بکشم. در این موارد نادر، یک EM_ASM macros family ( EM_ASM ، EM_ASM_INT و EM_ASM_DOUBLE ) ممکن است انتخاب ساده‌تری باشد. این ماکروها شبیه ماکرو EM_JS هستند، اما به جای تعریف یک تابع، کد را به صورت درون خطی در جایی که درج می کنند، اجرا می کنند.

از آنجایی که آنها یک نمونه اولیه تابع را اعلام نمی کنند، به روش دیگری برای تعیین نوع بازگشتی و دسترسی به آرگومان ها نیاز دارند.

برای انتخاب نوع بازگشت باید از نام ماکرو مناسب استفاده کنید. انتظار می‌رود بلوک‌های EM_ASM مانند توابع void عمل کنند، بلوک‌های EM_ASM_INT می‌توانند یک مقدار صحیح را برگردانند، و بلوک‌های EM_ASM_DOUBLE به ترتیب اعداد ممیز شناور را برمی‌گردانند.

هر آرگومان تصویب شده با نام‌های $0 ، $1 و غیره در بدنه جاوا اسکریپت در دسترس خواهد بود. مانند EM_JS یا WebAssembly به طور کلی، آرگومان ها فقط به مقادیر عددی محدود می شوند - اعداد صحیح، اعداد ممیز شناور، اشاره گرها و دسته ها.

در اینجا مثالی از نحوه استفاده از ماکرو EM_ASM برای ثبت یک مقدار JS دلخواه در کنسول آورده شده است:

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

در نهایت، Emscripten از اعلان کد جاوا اسکریپت در یک فایل جداگانه در قالب کتابخانه شخصی خود پشتیبانی می کند:

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

سپس باید نمونه های اولیه مربوطه را به صورت دستی در سمت C++ اعلام کنید:

extern "C" void log_value(EM_VAL val_handle);

پس از اعلام در هر دو طرف، کتابخانه جاوا اسکریپت را می توان از طریق --js-library option با کد اصلی به هم پیوند داد و نمونه های اولیه را با پیاده سازی های جاوا اسکریپت مربوطه به هم متصل کرد.

با این حال، این قالب ماژول غیر استاندارد است و نیاز به حاشیه نویسی وابستگی دقیق دارد. به این ترتیب، بیشتر برای سناریوهای پیشرفته رزرو شده است.

نتیجه

در این پست ما به روش های مختلفی برای ادغام کد جاوا اسکریپت در C++ در هنگام کار با WebAssembly نگاه کرده ایم.

گنجاندن چنین قطعه‌هایی به شما امکان می‌دهد توالی‌های طولانی از عملیات را به روشی تمیزتر و کارآمدتر بیان کنید و از کتابخانه‌های شخص ثالث، APIهای جاوا اسکریپت جدید و حتی ویژگی‌های نحوی جاوا اسکریپت که هنوز از طریق C++ یا Embind قابل بیان نیستند استفاده کنید.