Emscripten’s embind

JS را به wasm شما متصل می کند!

در آخرین مقاله wam، من در مورد چگونگی کامپایل یک کتابخانه C در wasm صحبت کردم تا بتوانید از آن در وب استفاده کنید. یکی از چیزهایی که برای من (و برای بسیاری از خوانندگان) متمایز شد، روش خام و کمی ناخوشایند است که شما باید به صورت دستی اعلام کنید که از کدام عملکردهای ماژول wam خود استفاده می کنید. برای تازه کردن ذهن شما، این قطعه کدی است که در مورد آن صحبت می کنم:

const api = {
    version: Module.cwrap('version', 'number', []),
    create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
    destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};

در اینجا نام توابعی را که با EMSCRIPTEN_KEEPALIVE علامت گذاری کرده ایم، نوع بازگشت آنها و انواع آرگومان های آنها را اعلام می کنیم. پس از آن، می توانیم از متدهای موجود در شی api برای فراخوانی این توابع استفاده کنیم. با این حال، استفاده از wam به این روش رشته‌ها را پشتیبانی نمی‌کند و شما را ملزم می‌کند که تکه‌هایی از حافظه را به صورت دستی جابه‌جا کنید که استفاده از بسیاری از APIهای کتابخانه را بسیار خسته‌کننده می‌کند. آیا راه بهتری وجود ندارد؟ چرا بله وجود دارد، در غیر این صورت این مقاله در مورد چیست؟

مخفی کردن نام C++

در حالی که تجربه توسعه‌دهنده دلیل کافی برای ساخت ابزاری است که به این اتصال‌ها کمک می‌کند، در واقع یک دلیل مهم‌تر وجود دارد: وقتی کد C یا C++ را کامپایل می‌کنید، هر فایل جداگانه کامپایل می‌شود. سپس، یک لینکر تمام این فایل های به اصطلاح شی را با هم مونگ کرده و آنها را به یک فایل wam تبدیل می کند. با C، نام توابع هنوز در فایل شی برای استفاده از پیوند دهنده موجود است. تنها چیزی که برای فراخوانی یک تابع C نیاز دارید، نامی است که ما به عنوان رشته ای برای cwrap() ارائه می کنیم.

از طرف دیگر C++ از بارگذاری بیش از حد تابع پشتیبانی می کند، به این معنی که شما می توانید یک تابع را چندین بار تا زمانی که امضا متفاوت است (به عنوان مثال پارامترهای متفاوت تایپ شده) پیاده سازی کنید. در سطح کامپایلر، یک نام خوب مانند add به چیزی تبدیل می شود که امضای نام تابع پیوند دهنده را رمزگذاری می کند. در نتیجه، دیگر نمی‌توانیم تابع خود را با نام آن جستجو کنیم.

embid را وارد کنید

embind بخشی از زنجیره ابزار Emscripten است و مجموعه‌ای از ماکروهای C++ را در اختیار شما قرار می‌دهد که به شما امکان می‌دهد کدهای C++ را حاشیه‌نویسی کنید. می‌توانید از جاوا اسکریپت اعلام کنید که کدام توابع، فهرست‌ها، کلاس‌ها یا انواع مقادیر را می‌خواهید استفاده کنید. بیایید ساده با چند توابع ساده شروع کنیم:

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

در مقایسه با مقاله قبلی من، ما دیگر emscripten.h در نظر نمی گیریم، زیرا دیگر لازم نیست توابع خود را با EMSCRIPTEN_KEEPALIVE حاشیه نویسی کنیم. در عوض، ما یک بخش EMSCRIPTEN_BINDINGS داریم که در آن نام‌هایی را که می‌خواهیم توابع خود را در معرض جاوا اسکریپت قرار دهیم، فهرست می‌کنیم.

برای کامپایل این فایل، می‌توانیم از همان تنظیمات (یا در صورت تمایل، همان تصویر Docker) مانند مقاله قبلی استفاده کنیم. برای استفاده از embind، پرچم --bind را اضافه می کنیم:

$ emcc --bind -O3 add.cpp

اکنون تنها چیزی که باقی مانده این است که یک فایل HTML که ماژول wasm تازه ایجاد شده ما را بارگیری می کند، جمع آوری کنید:

<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
    console.log(Module.add(1, 2.3));
    console.log(Module.exclaim("hello world"));
};
</script>

همانطور که می بینید، ما دیگر cwrap() استفاده نمی کنیم. این فقط از جعبه کار می کند. اما مهمتر از آن، لازم نیست نگران کپی دستی تکه های حافظه برای کارکرد رشته ها باشیم! embind این را به صورت رایگان همراه با بررسی نوع به شما می دهد:

خطاهای DevTools زمانی که تابعی را با تعداد آرگومان اشتباه فراخوانی می‌کنید یا آرگومان‌ها دارای نوع اشتباه هستند.

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

اشیاء

بسیاری از سازنده ها و توابع جاوا اسکریپت از اشیاء گزینه استفاده می کنند. این یک الگوی خوب در جاوا اسکریپت است، اما درک دستی در wasm بسیار خسته کننده است. embind می تواند در اینجا نیز کمک کند!

به عنوان مثال، من با این تابع فوق العاده مفید C++ آمدم که رشته های من را پردازش می کند، و من فوراً می خواهم از آن در وب استفاده کنم. در اینجا نحوه انجام من این است:

#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() تعریف می کنم. در بلوک EMSCRIPTEN_BINDINGS ، می توانم از value_object استفاده کنم تا جاوا اسکریپت این مقدار C++ را به عنوان یک شی ببیند. اگر ترجیح دادم از این مقدار C++ به عنوان آرایه استفاده کنم، می‌توانم از value_array استفاده کنم. من همچنین تابع processMessage() را بایند می کنم و بقیه آن embind magic هستند. اکنون می توانم تابع processMessage() را از جاوا اسکریپت بدون هیچ کد boilerplate فراخوانی کنم:

console.log(Module.processMessage(
    "hello world",
    {
    reverse: false,
    exclaim: true,
    repeat: 3
    }
)); // Prints "hello world!hello world!hello world!"

کلاس ها

برای کامل‌تر شدن، باید به شما نشان دهم که چگونه embind به شما امکان می‌دهد کل کلاس‌ها را در معرض دید قرار دهید، که هم افزایی زیادی با کلاس‌های ES6 به ارمغان می‌آورد. احتمالاً تا الان می توانید شروع به دیدن یک الگو کنید:

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

در سمت جاوا اسکریپت، این تقریباً شبیه یک کلاس بومی است:

<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>

در مورد C چطور؟

embind برای C++ نوشته شده است و فقط در فایل های C++ قابل استفاده است، اما این بدان معنا نیست که شما نمی توانید در برابر فایل های C لینک دهید! برای ترکیب C و C++، فقط باید فایل های ورودی خود را به دو گروه جدا کنید: یکی برای C و دیگری برای فایل های C++ و پرچم های CLI را برای emcc به صورت زیر تقویت کنید:

$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp

نتیجه

embind در هنگام کار با wam و C/C++ پیشرفت‌های زیادی در تجربه توسعه‌دهنده به شما می‌دهد. این مقاله همه گزینه‌های پیشنهادی را پوشش نمی‌دهد. اگر علاقه مند هستید، توصیه می کنم با مستندات embind ادامه دهید. به خاطر داشته باشید که استفاده از embind می‌تواند هم ماژول wam و هم کد چسب جاوا اسکریپت را تا 11 کیلوبایت بزرگ‌تر کند در صورت gzip'd - به ویژه در ماژول‌های کوچک. اگر فقط یک سطح بسیار کوچک دارید، ممکن است embind بیش از ارزش آن در یک محیط تولید هزینه داشته باشد! با این حال، شما قطعا باید آن را امتحان کنید.