استفاده از APIهای وب ناهمزمان از WebAssembly

APIهای I/O در وب ناهمزمان هستند، اما در اکثر زبان‌های سیستم همزمان هستند. هنگام کامپایل کد به WebAssembly، باید یک نوع API را به دیگری متصل کنید - و این پل Asyncify است. در این پست، زمان و نحوه استفاده از Asyncify و نحوه عملکرد آن در زیر کاپوت را خواهید آموخت.

I/O در زبان های سیستم

من با یک مثال ساده در C شروع می کنم. بگویید، شما می خواهید نام کاربر را از یک فایل بخوانید و با یک "سلام، (نام کاربری)" به آنها سلام کنید! پیام:

#include <stdio.h>

int main() {
    FILE *stream = fopen("name.txt", "r");
    char name[20+1];
    size_t len = fread(&name, 1, 20, stream);
    name[len] = '\0';
    fclose(stream);
    printf("Hello, %s!\n", name);
    return 0;
}

در حالی که مثال کار زیادی انجام نمی دهد، چیزی را نشان می دهد که در یک برنامه کاربردی با هر اندازه ای پیدا خواهید کرد: برخی از ورودی ها را از دنیای خارجی می خواند، آنها را به صورت داخلی پردازش می کند و خروجی ها را به دنیای بیرون می نویسد. تمام این تعاملات با دنیای خارج از طریق چند تابع که معمولاً توابع ورودی-خروجی نامیده می شوند، اتفاق می افتد که به I/O نیز کوتاه می شوند.

برای خواندن نام از C، حداقل به دو تماس ورودی/خروجی ضروری نیاز دارید: fopen ، برای باز کردن فایل، و fread برای خواندن داده‌ها از آن. هنگامی که داده ها را بازیابی کردید، می توانید از تابع I/O دیگری printf برای چاپ نتیجه در کنسول استفاده کنید.

این عملکردها در نگاه اول بسیار ساده به نظر می رسند و لازم نیست دو بار در مورد ماشین آلات مربوط به خواندن یا نوشتن داده فکر کنید. با این حال، بسته به محیط، ممکن است چیزهای زیادی در داخل رخ دهد:

  • اگر فایل ورودی در یک درایو محلی قرار دارد، برنامه باید یک سری از دسترسی‌های حافظه و دیسک را انجام دهد تا فایل را پیدا کند، مجوزها را بررسی کند، آن را برای خواندن باز کند، و سپس بلوک به بلوک را بخواند تا تعداد بایت‌های درخواستی بازیابی شود. . این می تواند بسیار کند باشد، بسته به سرعت دیسک شما و اندازه درخواستی.
  • یا، فایل ورودی ممکن است در محل نصب شده شبکه قرار داشته باشد، در این صورت، پشته شبکه نیز درگیر خواهد شد و پیچیدگی، تأخیر و تعداد تکرارهای احتمالی برای هر عملیات را افزایش می دهد.
  • در نهایت، حتی printf تضمینی برای چاپ چیزها در کنسول نیست و ممکن است به یک فایل یا یک مکان شبکه هدایت شود، در این صورت باید از همان مراحل بالا عبور کند.

به طور خلاصه، I/O می تواند کند باشد و نمی توانید با یک نگاه سریع به کد، مدت زمان یک تماس خاص را پیش بینی کنید. در حالی که این عملیات در حال اجرا است، کل برنامه شما ثابت و بدون پاسخ به کاربر ظاهر می شود.

این به C یا C++ نیز محدود نمی شود. اکثر زبان‌های سیستم تمام ورودی/خروجی را به شکلی از APIهای همزمان ارائه می‌کنند. برای مثال، اگر مثال را به Rust ترجمه کنید، API ممکن است ساده‌تر به نظر برسد، اما همان اصول اعمال می‌شود. شما فقط یک تماس برقرار می کنید و به طور همزمان منتظر می مانید تا نتیجه را برگرداند، در حالی که تمام عملیات گران قیمت را انجام می دهد و در نهایت نتیجه را در یک فراخوانی برمی گرداند:

fn main() {
    let s = std::fs::read_to_string("name.txt");
    println!("Hello, {}!", s);
}

اما چه اتفاقی می‌افتد وقتی سعی می‌کنید هر یک از آن نمونه‌ها را در WebAssembly کامپایل کنید و آنها را به وب ترجمه کنید؟ یا برای ارائه یک مثال خاص، عملیات "خواندن فایل" به چه چیزی می تواند ترجمه شود؟ نیاز به خواندن داده ها از مقداری حافظه دارد.

مدل ناهمزمان وب

وب دارای انواع مختلفی از گزینه های ذخیره سازی مختلف است که می توانید با آنها نقشه برداری کنید، مانند ذخیره سازی در حافظه (اشیاء JS)، localStorage ، IndexedDB ، ذخیره سازی سمت سرور، و یک API دسترسی به سیستم فایل جدید.

با این حال، تنها دو مورد از این API ها - ذخیره سازی در حافظه و localStorage - می توانند به طور همزمان استفاده شوند، و هر دو محدودترین گزینه ها در مورد ذخیره سازی و مدت زمان هستند. همه گزینه های دیگر فقط API های ناهمزمان را ارائه می دهند.

این یکی از ویژگی های اصلی اجرای کد در وب است: هر عملیات وقت گیر، که شامل هر ورودی/خروجی می شود، باید ناهمزمان باشد.

دلیل آن این است که وب از نظر تاریخی تک رشته ای است و هر کد کاربری که رابط کاربری را لمس می کند باید روی همان رشته ای اجرا شود که UI است. باید با سایر وظایف مهم مانند چیدمان، رندر و مدیریت رویداد برای زمان CPU رقابت کند. شما نمی خواهید که یک قطعه جاوا اسکریپت یا WebAssembly بتواند عملیات "خواندن فایل" را شروع کند و همه چیزهای دیگر - کل برگه، یا در گذشته، کل مرورگر - را برای بازه ای از میلی ثانیه تا چند ثانیه مسدود کند. ، تا زمانی که تمام شود.

درعوض، کد فقط مجاز است که یک عملیات ورودی/خروجی را به همراه یک فراخوان پس از اتمام اجرا کند. چنین تماس‌هایی به عنوان بخشی از حلقه رویداد مرورگر اجرا می‌شوند. من در اینجا وارد جزئیات نمی‌شوم، اما اگر علاقه‌مند به یادگیری نحوه عملکرد حلقه رویداد در زیر هود هستید، Tasks، microtasks، صف‌ها و زمان‌بندی‌ها را بررسی کنید که این موضوع را عمیقاً توضیح می‌دهد.

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

نکته مهمی که در مورد این مکانیسم باید به خاطر بسپارید این است که، در حالی که کد جاوا اسکریپت (یا WebAssembly) سفارشی شما اجرا می شود، حلقه رویداد مسدود می شود و در حالی که وجود دارد، هیچ راهی برای واکنش به هیچ کنترل کننده خارجی، رویداد، I/O وجود ندارد. و غیره. تنها راه برای بازگرداندن نتایج I/O این است که یک تماس برگشتی ثبت کنید، اجرای کد خود را به پایان برسانید و کنترل را به مرورگر برگردانید تا بتواند کارهای معلق را پردازش کند. هنگامی که I/O به پایان رسید، کنترل کننده شما به یکی از آن وظایف تبدیل می شود و اجرا می شود.

برای مثال، اگر می‌خواهید نمونه‌های بالا را در جاوا اسکریپت مدرن بازنویسی کنید و تصمیم به خواندن یک نام از یک URL راه دور دارید، از Fetch API و syntax async-await استفاده می‌کنید:

async function main() {
  let response = await fetch("name.txt");
  let name = await response.text();
  console.log("Hello, %s!", name);
}

حتی اگر همزمان به نظر می رسد، در زیر کاپوت هر await اساساً قند نحوی برای پاسخگویی به تماس ها وجود دارد:

function main() {
  return fetch("name.txt")
    .then(response => response.text())
    .then(name => console.log("Hello, %s!", name));
}

در این مثال بدون قند، که کمی واضح تر است، یک درخواست شروع می شود و پاسخ ها با اولین تماس مشترک مشترک می شوند. هنگامی که مرورگر پاسخ اولیه را دریافت کرد - فقط هدرهای HTTP - به طور ناهمزمان این تماس را فراخوانی می کند. بازگشت به تماس شروع به خواندن متن به‌عنوان متن با استفاده از response.text() می‌کند و با یک تماس دیگر مشترک نتیجه می‌شود. در نهایت، هنگامی که fetch تمام محتویات را بازیابی کرد، آخرین تماس را فراخوانی می کند که "Hello, (نام کاربری)" را چاپ می کند! به کنسول

به دلیل ماهیت ناهمزمان این مراحل، تابع اصلی می‌تواند به محض برنامه‌ریزی ورودی/خروجی، کنترل را به مرورگر بازگرداند و کل رابط کاربری را برای کارهای دیگر، از جمله رندر، اسکرول و غیره، پاسخگو و در دسترس بگذارد. I/O در پس زمینه اجرا می شود.

به عنوان مثال پایانی، حتی APIهای ساده مانند "خواب"، که باعث می شود برنامه برای تعداد مشخصی از ثانیه منتظر بماند، نیز نوعی عملیات I/O هستند:

#include <stdio.h>
#include <unistd.h>
// ...
printf("A\n");
sleep(1);
printf("B\n");

مطمئناً، می‌توانید آن را به روشی بسیار ساده ترجمه کنید که تا زمانی که زمان منقضی شود، موضوع فعلی مسدود شود:

console.log("A");
for (let start = Date.now(); Date.now() - start < 1000;);
console.log("B");

در واقع، این دقیقاً همان کاری است که Emscripten در اجرای پیش‌فرض «خواب» انجام می‌دهد، اما بسیار ناکارآمد است، کل رابط کاربری را مسدود می‌کند و اجازه نمی‌دهد تا در این میان هیچ رویداد دیگری مدیریت شود. به طور کلی، این کار را در کد تولید انجام ندهید.

در عوض، یک نسخه اصطلاحی تر از "خواب" در جاوا اسکریپت شامل فراخوانی setTimeout() و اشتراک با یک handler است:

console.log("A");
setTimeout(() => {
    console.log("B");
}, 1000);

چه چیزی در همه این مثال ها و API ها مشترک است؟ در هر مورد، کد اصطلاحی در زبان اصلی سیستم از یک API مسدود کننده برای I/O استفاده می کند، در حالی که یک مثال معادل برای وب به جای آن از یک API ناهمزمان استفاده می کند. هنگام کامپایل کردن در وب، باید به نحوی بین این دو مدل اجرایی تغییر شکل دهید و WebAssembly هنوز توانایی داخلی برای انجام این کار ندارد.

پر کردن شکاف با Asyncify

اینجاست که Asyncify وارد می‌شود. Asyncify یک ویژگی زمان کامپایل است که توسط Emscripten پشتیبانی می‌شود که اجازه می‌دهد کل برنامه را متوقف کرده و بعداً به‌صورت ناهمزمان از سرگیری شود.

یک نمودار فراخوانی که یک جاوا اسکریپت -> WebAssembly -> Web API -> فراخوانی کار async را توصیف می کند، جایی که Asyncify نتیجه کار async را دوباره به WebAssembly متصل می کند.

استفاده در C / C ++ با Emscripten

اگر می‌خواهید از Asyncify برای اجرای یک خواب ناهمزمان برای مثال آخر استفاده کنید، می‌توانید این کار را به صورت زیر انجام دهید:

#include <stdio.h>
#include <emscripten.h>

EM_JS(void, async_sleep, (int seconds), {
    Asyncify.handleSleep(wakeUp => {
        setTimeout(wakeUp, seconds * 1000);
    });
});
…
puts("A");
async_sleep(1);
puts("B");

EM_JS یک ماکرو است که به شما اجازه می دهد تا قطعات جاوا اسکریپت را طوری تعریف کنید که انگار توابع C هستند. در داخل، از یک تابع Asyncify.handleSleep() استفاده کنید که به Emscripten می‌گوید برنامه را به حالت تعلیق درآورد و یک کنترلر wakeUp() ارائه می‌کند که باید پس از پایان عملیات ناهمزمان فراخوانی شود. در مثال بالا، handler به setTimeout() ارسال می‌شود، اما می‌توان از آن در هر زمینه دیگری که پاسخ تماس را می‌پذیرد استفاده کرد. در نهایت، می‌توانید async_sleep() در هر جایی که می‌خواهید صدا بزنید، درست مانند sleep() معمولی یا هر API همزمان دیگر.

هنگام کامپایل کردن چنین کدی، باید به Emscripten بگویید تا ویژگی Asyncify را فعال کند. این کار را با عبور -s ASYNCIFY و همچنین -s ASYNCIFY_IMPORTS=[func1, func2] با لیستی آرایه مانند از توابع که ممکن است ناهمزمان باشند انجام دهید.

emcc -O2 \
    -s ASYNCIFY \
    -s ASYNCIFY_IMPORTS=[async_sleep] \
    ...

این امر به Emscripten اجازه می دهد تا بداند که هر فراخوانی به آن توابع ممکن است نیاز به ذخیره و بازیابی حالت داشته باشد، بنابراین کامپایلر کد پشتیبانی را در اطراف چنین تماس هایی تزریق می کند.

اکنون، وقتی این کد را در مرورگر اجرا می‌کنید، گزارش خروجی یکپارچه‌ای را می‌بینید که انتظارش را دارید، با B پس از تأخیر کوتاه بعد از A می‌آید.

A
B

شما می توانید مقادیر را از توابع Asyncify نیز برگردانید . کاری که باید انجام دهید این است که نتیجه handleSleep() را برگردانید و نتیجه را به wakeUp() ارسال کنید. به عنوان مثال، اگر به‌جای خواندن از یک فایل، می‌خواهید شماره‌ای را از یک منبع راه دور واکشی کنید، می‌توانید از قطعه‌ای مانند تصویر زیر برای صدور درخواست استفاده کنید، کد C را به حالت تعلیق درآورید و پس از بازیابی بدنه پاسخ، از سر بگیرید. - همه به طور یکپارچه انجام می شود، گویی تماس همزمان است.

EM_JS(int, get_answer, (), {
     return Asyncify.handleSleep(wakeUp => {
        fetch("answer.txt")
            .then(response => response.text())
            .then(text => wakeUp(Number(text)));
    });
});
puts("Getting answer...");
int answer = get_answer();
printf("Answer is %d\n", answer);

در واقع، برای API های مبتنی بر Promise مانند fetch() ، حتی می توانید به جای استفاده از API مبتنی بر callback، Asyncify را با ویژگی async-await جاوا اسکریپت ترکیب کنید. برای آن، به جای Asyncify.handleSleep() ، Asyncify.handleAsync() را فراخوانی کنید. سپس، به جای برنامه‌ریزی یک فراخوان wakeUp() ، می‌توانید یک تابع جاوا اسکریپت async را ارسال کنید و از await و return در داخل استفاده کنید، و باعث می‌شود کد حتی طبیعی‌تر و همزمان‌تر به نظر برسد، در حالی که هیچ یک از مزایای I/O ناهمزمان را از دست ندهید.

EM_JS(int, get_answer, (), {
     return Asyncify.handleAsync(async () => {
        let response = await fetch("answer.txt");
        let text = await response.text();
        return Number(text);
    });
});

int answer = get_answer();

در انتظار مقادیر پیچیده

اما این مثال همچنان شما را فقط به اعداد محدود می کند. اگر بخواهید مثال اصلی را پیاده سازی کنید، جایی که من سعی کردم نام کاربر را از یک فایل به عنوان رشته دریافت کنم؟ خوب، شما هم می توانید این کار را انجام دهید!

Emscripten یک ویژگی به نام Embind را ارائه می دهد که به شما امکان می دهد تبدیل بین مقادیر جاوا اسکریپت و C++ را مدیریت کنید. از Asyncify نیز پشتیبانی می کند، بنابراین می توانید await() در Promise s خارجی فراخوانی کنید و دقیقاً مانند await در کد جاوا اسکریپت async-await عمل می کند:

val fetch = val::global("fetch");
val response = fetch(std::string("answer.txt")).await();
val text = response.call<val>("text").await();
auto answer = text.as<std::string>();

هنگام استفاده از این روش، حتی نیازی نیست که ASYNCIFY_IMPORTS به عنوان یک پرچم کامپایل ارسال کنید، زیرا قبلاً به طور پیش فرض گنجانده شده است.

خوب، پس همه اینها در Emscripten عالی کار می کند. در مورد دیگر زنجیره‌های ابزار و زبان‌ها چطور؟

استفاده از زبان های دیگر

بگویید که یک تماس همزمان مشابه در جایی در کد Rust خود دارید که می‌خواهید آن را به یک API غیر همگام در وب نگاشت کنید. معلوم است، شما هم می توانید این کار را انجام دهید!

ابتدا، باید چنین تابعی را به عنوان یک وارد کردن معمولی از طریق بلوک extern (یا نحو زبان انتخابی خود برای توابع خارجی) تعریف کنید.

extern {
    fn get_answer() -> i32;
}

println!("Getting answer...");
let answer = get_answer();
println!("Answer is {}", answer);

و کد خود را در WebAssembly کامپایل کنید:

cargo build --target wasm32-unknown-unknown

اکنون باید فایل WebAssembly را با کدی برای ذخیره/بازیابی پشته ابزار کنید. برای C/C++، Emscripten این کار را برای ما انجام می‌دهد، اما در اینجا از آن استفاده نمی‌شود، بنابراین فرآیند کمی دستی‌تر است.

خوشبختانه، تبدیل Asyncify خود کاملاً مبتنی بر زنجیره ابزار است. این می تواند فایل های WebAssembly دلخواه را تغییر دهد، بدون توجه به اینکه توسط کدام کامپایلر تولید شده است. تبدیل به طور جداگانه به عنوان بخشی از بهینه ساز wasm-opt از زنجیره ابزار Binaryen ارائه می شود و می توان آن را به صورت زیر فراخوانی کرد:

wasm-opt -O2 --asyncify \
      --pass-arg=asyncify-imports@env.get_answer \
      [...]

برای فعال کردن تبدیل، --asyncify عبور دهید، و سپس از --pass-arg=… برای ارائه لیستی از توابع ناهمزمان با کاما جدا شده استفاده کنید، جایی که وضعیت برنامه باید به حالت تعلیق درآید و بعداً از سر گرفته شود.

تنها چیزی که باقی می ماند ارائه کدهای زمان اجرا پشتیبانی است که در واقع این کار را انجام می دهد - کد WebAssembly را تعلیق و از سرگیری می کند. مجدداً، در مورد C / C++ این مورد توسط Emscripten گنجانده شده است، اما اکنون به کد چسب جاوا اسکریپت سفارشی نیاز دارید که فایل‌های WebAssembly دلخواه را مدیریت کند. ما یک کتابخانه فقط برای آن ایجاد کرده ایم.

می‌توانید آن را در GitHub در https://github.com/GoogleChromeLabs/asyncify یا npm با نام asyncify-wasm پیدا کنید.

این یک API نمونه استاندارد WebAssembly را شبیه سازی می کند، اما تحت فضای نام خودش. تنها تفاوت این است که، تحت یک WebAssembly API معمولی، شما فقط می توانید توابع همزمان را به عنوان واردات ارائه کنید، در حالی که تحت پوشش Asyncify، می توانید واردات ناهمزمان را نیز ارائه دهید:

const { instance } = await Asyncify.instantiateStreaming(fetch('app.wasm'), {
    env: {
        async get_answer() {
            let response = await fetch("answer.txt");
            let text = await response.text();
            return Number(text);
        }
    }
});
…
await instance.exports.main();

هنگامی که سعی می کنید یک تابع ناهمزمان - مانند get_answer() در مثال بالا - را از سمت WebAssembly فراخوانی کنید، کتابخانه Promise برگشتی را شناسایی می کند، وضعیت برنامه WebAssembly را به حالت تعلیق در می آورد و ذخیره می کند، برای تکمیل وعده مشترک می شود و بعدا ، پس از حل شدن، پشته تماس و حالت را به طور یکپارچه بازیابی کنید و به اجرا ادامه دهید، گویی هیچ اتفاقی نیفتاده است.

از آنجایی که هر تابعی در ماژول ممکن است یک تماس ناهمزمان ایجاد کند، تمام صادرات نیز به طور بالقوه ناهمزمان می شوند، بنابراین آنها نیز بسته می شوند. ممکن است در مثال بالا متوجه شده باشید که باید await نتیجه instance.exports.main() باشید تا بدانید که اجرای واقعاً چه زمانی به پایان رسیده است.

این همه زیر کاپوت چگونه کار می کند؟

هنگامی که Asyncify تماس با یکی از توابع ASYNCIFY_IMPORTS را تشخیص می‌دهد، یک عملیات ناهمزمان را شروع می‌کند، کل وضعیت برنامه، از جمله پشته تماس و هر محلی موقت را ذخیره می‌کند و بعداً، هنگامی که آن عملیات به پایان رسید، تمام حافظه و تماس را بازیابی می‌کند. پشته و از همان مکان و با همان حالت از سر گرفته می شود که گویی برنامه هرگز متوقف نشده است.

این کاملاً شبیه ویژگی async-await در جاوا اسکریپت است که قبلاً نشان دادم، اما برخلاف جاوا اسکریپت، نیازی به سینتکس یا پشتیبانی زمان اجرا خاصی از زبان ندارد و در عوض با تبدیل توابع همزمان ساده در زمان کامپایل کار می کند.

هنگام کامپایل مثال خواب ناهمزمان نشان داده شده قبلی:

puts("A");
async_sleep(1);
puts("B");

Asyncify این کد را می گیرد و آن را تقریباً به شکل زیر تبدیل می کند (شبه کد، تبدیل واقعی بیشتر از این درگیر است):

if (mode == NORMAL_EXECUTION) {
    puts("A");
    async_sleep(1);
    saveLocals();
    mode = UNWINDING;
    return;
}
if (mode == REWINDING) {
    restoreLocals();
    mode = NORMAL_EXECUTION;
}
puts("B");

mode ابتدا روی NORMAL_EXECUTION تنظیم شده است. به همین ترتیب، اولین باری که چنین کد تبدیل شده ای اجرا می شود، تنها بخشی که به async_sleep() منتهی می شود، ارزیابی می شود. به محض برنامه‌ریزی عملیات ناهمزمان، Asyncify همه محلی‌ها را ذخیره می‌کند و با بازگشت از هر تابع به بالا، پشته را باز می‌کند، به این ترتیب کنترل را به حلقه رویداد مرورگر باز می‌گرداند.

سپس، هنگامی که async_sleep() حل شود، کد پشتیبانی Asyncify mode به REWINDING تغییر می‌دهد و دوباره تابع را فراخوانی می‌کند. این بار، از شاخه "اجرای عادی" صرفنظر می شود - زیرا دفعه قبل کار را انجام داده است و من می خواهم از چاپ دوبار "A" اجتناب کنم - و در عوض مستقیماً به شاخه "پیچیدن" می رسد. پس از رسیدن به آن، تمام محلی‌های ذخیره‌شده را بازیابی می‌کند، حالت را به حالت عادی تغییر می‌دهد و اجرا را طوری ادامه می‌دهد که گویی کد از ابتدا متوقف نشده است.

هزینه های تبدیل

متأسفانه، تبدیل Asyncify کاملاً رایگان نیست، زیرا باید مقدار زیادی کد پشتیبانی را برای ذخیره و بازیابی همه آن محلی‌ها، پیمایش پشته تماس تحت حالت‌های مختلف و غیره تزریق کند. سعی می‌کند فقط عملکردهایی را که در خط فرمان به‌عنوان ناهمزمان علامت‌گذاری شده‌اند، و همچنین هر یک از تماس‌گیرندگان بالقوه آن‌ها را تغییر دهد، اما مقدار سربار اندازه کد ممکن است تا قبل از فشرده‌سازی تا حدود ۵۰ درصد اضافه شود.

نموداری که سربار اندازه کد را برای معیارهای مختلف نشان می‌دهد، از نزدیک به 0٪ در شرایط دقیق تنظیم شده تا بیش از 100٪ در بدترین موارد

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

مطمئن شوید که همیشه بهینه‌سازی‌ها را برای ساخت‌های نهایی فعال کنید تا از بالاتر رفتن آن جلوگیری کنید. همچنین می‌توانید گزینه‌های بهینه‌سازی خاص Asyncify را بررسی کنید تا با محدود کردن تبدیل‌ها به توابع مشخص و/یا فقط فراخوانی مستقیم تابع، هزینه‌های سربار را کاهش دهید. همچنین هزینه کمی برای عملکرد زمان اجرا وجود دارد، اما به خود تماس های async محدود می شود. با این حال، در مقایسه با هزینه کار واقعی، معمولاً ناچیز است.

دموهای دنیای واقعی

اکنون که به نمونه های ساده نگاه کردید، به سراغ سناریوهای پیچیده تر می روم.

همانطور که در ابتدای مقاله ذکر شد، یکی از گزینه های ذخیره سازی در وب، API دسترسی به فایل سیستم ناهمزمان است. این امکان دسترسی به یک فایل سیستم میزبان واقعی را از یک برنامه وب فراهم می کند.

از سوی دیگر، یک استاندارد واقعی به نام WASI برای WebAssembly I/O در کنسول و سمت سرور وجود دارد. این به عنوان یک هدف تلفیقی برای زبان های سیستم طراحی شده است و انواع سیستم فایل و سایر عملیات را به شکل سنتی همزمان در معرض نمایش می گذارد.

چه می شد اگر بتوانید یکی را به دیگری نقشه برداری کنید؟ سپس می‌توانید هر برنامه‌ای را به هر زبان مبدأ با هر زنجیره ابزاری که هدف WASI را پشتیبانی می‌کند، کامپایل کنید، و آن را در یک جعبه شنی روی وب اجرا کنید، در حالی که همچنان به آن اجازه می‌دهید روی فایل‌های کاربر واقعی کار کند! با Asyncify می توانید این کار را انجام دهید.

در این نسخه ی نمایشی، من Rust coreutils crate را با چند وصله کوچک برای WASI کامپایل کرده ام، از طریق Asyncify transform عبور کرده و اتصالات ناهمزمان را از WASI به File System Access API در سمت جاوا اسکریپت پیاده سازی کرده ام. هنگامی که با کامپوننت ترمینال Xterm.js ترکیب می شود، پوسته ای واقع گرایانه را ارائه می دهد که در تب مرورگر اجرا می شود و بر روی فایل های کاربر واقعی کار می کند - درست مانند یک ترمینال واقعی.

آن را به صورت زنده در https://wasi.rreverser.com/ بررسی کنید.

موارد استفاده Asyncify فقط به تایمرها و فایل سیستم ها محدود نمی شود. می‌توانید جلوتر بروید و از APIهای خاص بیشتری در وب استفاده کنید.

برای مثال، همچنین با کمک Asyncify، می‌توان libusb - احتمالاً محبوب‌ترین کتابخانه بومی برای کار با دستگاه‌های USB - را به یک WebUSB API نگاشت کرد، که دسترسی ناهمزمان را به چنین دستگاه‌هایی در وب می‌دهد. پس از نقشه‌برداری و کامپایل، آزمایش‌ها و نمونه‌های استاندارد libusb را برای اجرا در برابر دستگاه‌های انتخابی درست در جعبه شنی یک صفحه وب دریافت کردم.

تصویری از خروجی اشکال زدایی libusb در یک صفحه وب که اطلاعات دوربین Canon متصل را نشان می دهد

اگرچه احتمالاً این داستانی برای پست وبلاگ دیگری است.

این مثال‌ها نشان می‌دهند که Asyncify چقدر می‌تواند برای پر کردن شکاف و انتقال انواع برنامه‌ها به وب باشد، به شما این امکان را می‌دهد که دسترسی بین پلتفرمی، sandboxing و امنیت بهتر را بدون از دست دادن عملکرد به دست آورید.