استخدام واجهات برمجة تطبيقات الويب غير المتزامنة من WebAssembly

تعد واجهات برمجة تطبيقات I/O على الويب غير متزامنة، إلا أنها متزامنة في معظم لغات النظام. فعندما وتجميع التعليمات البرمجية إلى WebAssembly، تحتاج إلى ربط نوع من واجهات برمجة التطبيقات مع نوع آخر - ويتم عدم المزامنة. في هذه المشاركة، ستتعرّف على كيفية استخدام Asyncify، وكيفية عمله وغير ذلك.

وحدات الإدخال والإخراج بلغات النظام

سأبدأ بمثال بسيط في لغة 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;
}

بينما لا يقدم هذا المثال الكثير، إلا أنه يوضح بالفعل شيئًا ستجده في أحد التطبيقات بأي حجم: يقرأ بعض المدخلات من العالم الخارجي، ويعالجها داخليًا ويكتب ومخرجاتك إلى العالم الخارجي. تحدث كل هذه التفاعلات مع العالم الخارجي عبر بعض الدوال التي تسمى عادةً دوال المدخلات والمخرجات، والتي يتم اختصارها أيضًا إلى وحدات الإدخال والإخراج.

لقراءة الاسم من C، تحتاج على الأقل إلى طلبَي إدخال/إخراج مهمين: fopen، لفتح الملف، fread لقراءة البيانات منه. بعد استرداد البيانات، يمكنك استخدام دالة إدخال/إخراج أخرى printf لطباعة النتيجة إلى وحدة التحكم.

تبدو هذه الدوال بسيطة جدًا للوهلة الأولى، ولن تحتاج إلى التفكير مليًا في الآلات المستخدمة لقراءة أو كتابة البيانات. ومع ذلك، اعتمادًا على البيئة، يمكن أن يكون هناك الكثير يحدث الكثير في الداخل:

  • إذا كان ملف الإدخال موجودًا على محرك أقراص محلي، يحتاج التطبيق إلى تنفيذ سلسلة من إلى الذاكرة والقرص لتحديد موقع الملف، والتحقّق من الأذونات، وفتحه للقراءة، ثم قراءة حظر تلو الآخر إلى أن يتم استرداد عدد وحدات البايت المطلوب يمكن أن يكون هذا بطيئًا جدًا، اعتمادًا على سرعة القرص والحجم المطلوب.
  • أو قد يكون ملف الإدخال موجودًا في موقع مثبَّت على الشبكة، وفي هذه الحالة، الحزمة أيضًا، ما يزيد من مستوى التعقيد ووقت الاستجابة وعدد إعادة المحاولة لكل عملية.
  • أخيرًا، لا يمكن ضمان أنّ printf حتى يطبع المحتوى إلى وحدة التحكّم وقد تتم إعادة توجيهه. إلى ملف أو إلى موقع على الشبكة، وفي هذه الحالة سيتعين عليه الانتقال إلى نفس الخطوات المذكورة أعلاه.

باختصار، يمكن أن يكون مؤتمر I/O بطيئًا ولا يمكنك توقع المدة التي ستستغرقها مكالمة معينة نظرة سريعة على التعليمات البرمجية. أثناء تشغيل هذه العملية، سيظهر تطبيقك بأكمله مجمدًا. وعدم الاستجابة للمستخدم.

وهذا لا يقتصر على C أو C++ أيضًا. تقدم معظم لغات النظام جميع وحدات الإدخال والإخراج في شكل من واجهات برمجة التطبيقات المتزامنة. على سبيل المثال، إذا ترجمت المثال إلى Rust، فقد تبدو واجهة برمجة التطبيقات أبسط، ولكن تنطبق نفس المبادئ. ما عليك سوى إجراء مكالمة والانتظار بشكل متزامن لعرض النتيجة، بينما ينفذ جميع العمليات المكلفة ثم ينتج عنها في الاستدعاء:

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

ولكن ماذا يحدث عندما تحاول تجميع أي من هذه العينات إلى WebAssembly وترجمتها إلى الويب؟ أو، لتقديم مثال محدد، ما الذي يمكن أن "تمت قراءة الملف" له العملية التي يتم الترجمة إليها؟ ستفتح بحاجة إلى قراءة البيانات من بعض مساحة التخزين.

نموذج غير متزامن للويب

تتوفّر على الويب مجموعة متنوّعة من خيارات التخزين المختلفة التي يمكنك الربط بها، مثل التخزين في الذاكرة (JS). الكائنات)، localStorage، IndexedDB، والتخزين من جهة الخادم، وواجهة برمجة تطبيقات File System Access API جديدة.

ومع ذلك، لا يمكن استخدام سوى اثنتين من واجهات برمجة التطبيقات هذه، والتخزين في الذاكرة وlocalStorage. بشكل متزامن، وكلاهما هما الخياران الأكثر تقييدًا لما يمكنك تخزينه ومدة الاحتفاظ به. الكل توفر الخيارات الأخرى واجهات برمجة تطبيقات غير متزامنة فقط.

هذه إحدى الخصائص الأساسية لتنفيذ التعليمات البرمجية على الويب: أي عملية تستغرق وقتًا طويلاً، على أي إدخال/إخراج غير متزامن.

يرجع السبب في ذلك إلى أن شبكة الويب كانت تتضمن سلسلة محادثات واحدة في السابق، وأن أي رمز مستخدم يلامس واجهة المستخدم على سلسلة التعليمات نفسها التي يعمل بها واجهة المستخدم. ينبغي أن تتنافس مع المهام المهمة الأخرى مثل والتنسيق والعرض والتعامل مع الأحداث في وقت وحدة المعالجة المركزية. أنت لا تريد جزءًا من JavaScript أو WebAssembly ليكون قادرًا على بدء "قراءة ملف" التشغيل وحظر أي شيء آخر - علامة التبويب بأكملها، أو، في الماضي، المتصفح بالكامل لفترة تتراوح من مللي ثانية إلى بضع ثوانٍ، إلى أن ينتهي.

بدلاً من ذلك، يُسمح للرمز البرمجي فقط بجدولة عملية إدخال/إخراج مع استدعاء ليتم تنفيذها بمجرد انتهائه. ويتم تنفيذ عمليات الاستدعاء هذه كجزء من حلقة الأحداث في المتصفّح. لن أكون سوف ندخل في التفاصيل هنا، ولكن إذا كنت مهتمًا بمعرفة كيفية عمل حلقة الأحداث خلافيًا، إتمام الدفع المهام والمهام الصغيرة وقوائم الانتظار والجداول الزمنية والذي يفسر هذا الموضوع بالتفصيل.

النسخة المختصرة هي أن المتصفح يشغِّل جميع أجزاء الرموز البرمجية على شكل حلقة لانهائية، عن طريق ونقلهم من قائمة الانتظار واحدًا تلو الآخر. عند تشغيل حدث ما، يضع المتصفح علامة المعالِج المقابل، وفي التكرار الحلقي التالي، يتم إخراجه من قائمة الانتظار وتنفيذه. تسمح هذه الآلية بمحاكاة التزامن وتشغيل العديد من العمليات المتوازية أثناء استخدام سلسلة محادثات واحدة.

شيء مهم يجب تذكره بشأن هذه الآلية هو أنه على الرغم من أن لغة JavaScript المخصصة (أو WebAssembly) تنفيذ الرمز البرمجي، ويتم حظر حلقة الحدث، وما مِن طريقة للتفاعل مع أي معالِجات خارجية أو أحداث أو مؤتمر I/O وما إلى ذلك. إن الطريقة الوحيدة للحصول على نتائج مؤتمر I/O هي تسجيل وإنهاء تنفيذ الرمز، ثم إعادة منح عنصر التحكم إلى المتصفح بحيث يبقى معالجة أي مهام معلقة. بمجرد انتهاء I/O، سيصبح المعالج إحدى تلك المهام سيتم تنفيذه.

على سبيل المثال، إذا أردت إعادة كتابة العينات أعلاه في لغة JavaScript حديثة وقررت قراءة من عنوان URL بعيد، فيمكنك استخدام واجهة برمجة تطبيقات الجلب والبنية غير المتزامنة:

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 لاسترداد جميع المحتويات، فإنها تستدعي آخر معاودة الاتصال، والتي تطبع "مرحبًا، (اسم المستخدم)!" إلى وحدة التحكم.

وبفضل الطبيعة غير المتزامنة لهذه الخطوات، يمكن للدالة الأصلية إعادة التحكم إلى بمجرد جدولة I/O، وترك واجهة المستخدم بأكملها متجاوبة ومتاحة مهام أخرى، بما في ذلك العرض والتمرير وما إلى ذلك، أثناء تنفيذ وحدات الإدخال والإخراج في الخلفية.

وكمثال أخير، حتى واجهات برمجة التطبيقات البسيطة مثل "sleep" التي تجعل التطبيق ينتظر عدد الثواني، هي أيضًا شكل من أشكال عمليات الإدخال والإخراج:

#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 في تنفيذها الافتراضي و"النوم" غير فعّال للغاية، وسيحظر واجهة المستخدم بالكامل ولن يسمح بمعالجة أي أحداث أخرى في هذه الأثناء. وبشكل عام، لا تفعل ذلك في رمز الإنتاج.

بدلاً من ذلك، هناك إصدار أكثر اصطلاحًا من كلمة "sleep" في JavaScript طلب الرقم setTimeout() الاشتراك باستخدام معالج:

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

ما القاسم المشترك بين كل هذه الأمثلة وواجهات برمجة التطبيقات؟ في كل حالة، الكود الاصطلاحي في الأصل تستخدم لغة الأنظمة واجهة برمجة تطبيقات للحظر لـ I/O، بينما يستخدم مثال مكافئ للويب غير متزامن API بدلاً من ذلك. وعند التجميع إلى الويب، تحتاج إلى التحويل بين هذين النوعين نماذج التنفيذ، ولا تمتلك WebAssembly قدرة مدمجة على إجراء ذلك حتى الآن.

سدّ الفجوة باستخدام Asyncify

وهنا يأتي دور Asyncify. تُعد Asyncify ميزة وقت التجميع التي تدعمها Emscripten والتي تسمح بإيقاف البرنامج بأكمله مؤقتًا استئنافه بشكل غير متزامن لاحقًا.

رسم بياني للمكالمات
وصف JavaScript -> WebAssembly -> web API -> لاستدعاء مهمة غير متزامن، حيث تتصل Asyncify
نتيجة المهمة غير المتزامنة في 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 هو ماكرو يسمح بتحديد مقتطفات JavaScript كما لو كانت دوال C. في الداخل، استخدم دالة Asyncify.handleSleep() الذي يطلب من Emscripten تعليق البرنامج ويوفر معالج wakeUp() الذي يجب يتم استدعاؤه عند انتهاء العملية غير المتزامنة. في المثال أعلاه، يتم تمرير المعالج إلى setTimeout()، ولكن يمكن استخدامه في أي سياق آخر يقبل معاودة الاتصال. أخيرًا، يمكنك يمكنك استدعاء async_sleep() في أي مكان تريده مثل sleep() العادي أو أي واجهة برمجة تطبيقات متزامنة أخرى.

عند تجميع مثل هذا الرمز، عليك إعلام Emscripten بتفعيل ميزة Asyncify. إجراء ذلك عن طريق اجتياز -s ASYNCIFY بالإضافة إلى -s ASYNCIFY_IMPORTS=[func1, func2] مع قائمة بالدوال تشبه الصفيفة والتي قد تكون غير متزامنة.

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

ويتيح ذلك لـ Emscripten معرفة أن أي استدعاءات لهذه الدوال قد تتطلب حفظ ملف العمل واستعادته حينها، سيُدخل برنامج التحويل البرمجي تعليمات برمجية داعمة حول هذه الطلبات.

الآن، عند تنفيذ هذه التعليمة البرمجية في المتصفح، سترى سجل إخراج سلسًا مثلما تتوقع، مع ظهور B بعد تأخير قصير بعد A.

A
B

يمكنك إرجاع القيم من عدم مزامنة الدوال أيضًا المزايا عليك القيام به هو إرجاع نتيجة 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);

في الواقع، بالنسبة إلى واجهات برمجة التطبيقات المستندة إلى Promise، مثل fetch()، يمكنك أيضًا دمج Asyncify مع واجهة JavaScript. async-await بدلاً من استخدام واجهة برمجة التطبيقات المستندة إلى معاودة الاتصال. لذلك، بدلاً من "Asyncify.handleSleep()"، اتصل بـ "Asyncify.handleAsync()". بعد ذلك، بدلاً من الاضطرار إلى جدولة استدعاء wakeUp()، يمكنك تمرير دالة JavaScript async واستخدام await وreturn داخلها، مما يجعل الرمز يبدو أكثر طبيعية وتزامنًا، مع عدم فقدان أي من مزايا إدخال/إخراج غير متزامن.

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 خارجية وسيعمل تمامًا مثل await في انتظار أن يكون رمز JavaScript:

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 code وتريد ربطها غير متزامنة على الويب. تبين أنه يمكنك فعل ذلك أيضًا!

أولاً، يجب عليك تعريف هذه الدالة على أنها عملية استيراد منتظمة عبر كتلة 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 toolchain ويمكن استدعاؤها على النحو التالي:

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

يمكنك تمرير --asyncify لتفعيل عملية التحويل، ثم استخدام --pass-arg=… لتقديم فاصلة قائمة بالدوال غير المتزامنة، حيث يجب تعليق حالة البرنامج واستئنافها في وقت لاحق.

كل ما تبقى هو تقديم رمز برمجي داعم لبيئة التشغيل سينفّذ هذا الإجراء، مثل تعليق واستئناف رمز WebAssembly. مرة أخرى، في حالة C / C++ ، سيتم تضمين ذلك بواسطة Emscripten، لكنك الآن بحاجة إلى رمز JavaScript مخصّص سيتعامل مع ملفات WebAssembly العشوائية. لقد أنشأنا مكتبة فقط لهذا الغرض.

يمكنك العثور عليه على GitHub في https://github.com/GoogleChromeLabs/asyncify or npm تحت اسم asyncify-wasm.

ويحاكي هذا الإصدار إنشاء مثيل WebAssembly عادي. API، ولكن ضمن مساحة الاسم الخاصة بها. هو أنه ضمن واجهة برمجة تطبيقات WebAssembly عادية، يمكنك توفير دوال متزامنة فقط عمليات الاستيراد، بينما يمكنك توفير عمليات استيراد غير متزامنة ضمن برنامج تضمين 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، تبدأ في استدعاء غير متزامن. تحفظ حالة التطبيق بالكامل، بما في ذلك حزمة الاستدعاءات وأي بيانات محلي، وفي وقت لاحق، عند انتهاء هذه العملية، تستعيد كل الذاكرة وحزمة الاتصالات من نفس المكان وبنفس الحالة كما لو لم يتوقف البرنامج أبدًا.

يشبه ذلك إلى حد كبير ميزة الانتظار غير المتزامن في JavaScript التي عرضتها سابقًا، ولكن على عكس حيث لا تتطلب لغة JavaScript أي دعم خاص ببناء جملة أو وقت تشغيل من اللغة، عن طريق تحويل الدوال المتزامنة العادية في وقت التجميع.

عند تجميع مثال النوم غير المتزامن المعروض سابقًا:

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 مجانيًا بالكامل، حيث يجب إدخال قدر كبير من كود داعم لتخزين واستعادة كل هذه البيانات المحلية، والتنقل في حزمة الاتصالات ضمن الأوضاع المختلفة وما إلى ذلك. يحاول تعديل الدوال التي تم وضع علامة عليها كغير متزامنة في الأمر فقط. بالإضافة إلى أيٍ من المتصلين المحتملين، ولكن قد تصل النفقات العامة للرمز إلى ما يصل إلى 50% تقريبًا قبل الضغط.

رسم بياني يعرض الرمز
زيادة حجم النفقات العامة لمختلف مقاييس الأداء، من% 0 تقريبًا في ظل ظروف محسّنة إلى أكثر من% 100 في أسوأ الحالات
الحالات

وهذا ليس مثاليًا، ولكنه مقبول في كثير من الحالات عندما لا يكون البديل هو تمامًا أو الاضطرار إلى إجراء تعديلات مهمة على التعليمة البرمجية الأصلية.

احرص دائمًا على تفعيل التحسينات للإصدارات النهائية لتجنُّب الارتقاء إلى مستوى أعلى من ذلك. يمكنك راجع أيضًا التحسين الخاص بـ Asyncify options لتقليل النفقات العامة بنسبة قصر التحويلات فقط على دوال محددة و/أو استدعاءات الدوال المباشرة فقط. تتوفر أيضًا تكلفة بسيطة لأداء بيئة التشغيل، لكنها تقتصر على الطلبات غير المتزامنة نفسها. ومع ذلك، بالمقارنة بتكلفة العمل الفعلي، فعادةً ما يكون ضئيلاً.

عروض توضيحية من العالم الحقيقي

الآن بعد أن نظرت إلى الأمثلة البسيطة، سأنتقل إلى سيناريوهات أكثر تعقيدًا.

كما ذكرنا في بداية المقالة، أحد خيارات التخزين على الويب هو واجهة برمجة تطبيقات الوصول إلى نظام الملفات غير المتزامنة. حيث توفر الوصول إلى نظام ملفات المضيف الحقيقي من تطبيق ويب.

من ناحية أخرى، هناك معيار فعلي يسمى WASI لـ WebAssembly I/O في وحدة التحكم ومن جهة الخادم. تم تصميمه كهدف تجميع نظام الملفات، ويعرض جميع أنواع أنظمة الملفات والعمليات الأخرى في بيئة ونموذج متزامن.

ماذا لو كان بإمكانك تعيين واحد إلى آخر؟ بعد ذلك، يمكنك تجميع أي تطبيق بأي لغة مصدر. بأي سلسلة أدوات تدعم هدف WASI، وتشغيلها في وضع حماية على الويب، مع الاستمرار ما يتيح لها العمل على ملفات المستخدم الحقيقية! باستخدام Asyncify، يمكنك إجراء ذلك.

في هذا العرض التوضيحي، جمعتُ صندوق Rust coreutils باستخدام بعض التصحيحات الصغيرة إلى WASI، تم تمريرها عبر تحويل Asyncify وتنفيذها غير المتزامنة عمليات الربط من WASI إلى File System Access API على جانب JavaScript. بعد دمجه مع المكوِّن الطرفي Xterm.js، وهو يوفر واجهة أوامر واقعية تعمل في علامة تبويب في المتصفح وتعمل على ملفات المستخدمين، تمامًا مثل الوحدة الطرفية الفعلية.

يمكنك الاطّلاع عليها مباشرةً على الرابط https://wasi.rreverser.com/.

لا تقتصر حالات الاستخدام Asyncify على الموقّتات وأنظمة الملفات فقط. يمكنك الذهاب إلى أبعد من ذلك تستخدم واجهات برمجة تطبيقات أكثر تخصصًا على الويب.

على سبيل المثال، بمساعدة Asyncify، من الممكن تعيين libusb: ربما تكون المكتبة الأصلية الأكثر رواجًا للعمل باستخدام أجهزة USB - إلى WebUSB API، ما يتيح الوصول غير المتزامن إلى هذه الأجهزة على الويب. بعد ربط البيانات وجمعها، حصلت على اختبارات libusb العادية وأمثلة لتنفيذها مقابل النتائج المختارة الأجهزة مباشرةً في وضع الحماية لصفحة الويب.

لقطة شاشة من libusb
نتائج تصحيح الأخطاء على صفحة ويب تعرض معلومات حول كاميرا Canon المتصلة

إنها على الأرجح قصة لمنشور مدونة آخر.

توضّح هذه الأمثلة مدى فعالية Asyncify في سد الفجوة ونقل جميع أنواعًا من التطبيقات إلى الويب، مما يسمح لك بالوصول عبر النظام الأساسي ووضع الحماية، من الأمان، وكل ذلك بدون فقدان الوظائف.