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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

سد الفجوة باستخدام 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

يمكنك عرض قيم من دوال 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);

في الواقع، بالنسبة إلى واجهات برمجة التطبيقات المستندة إلى Promise مثل fetch()، يمكنك أيضًا دمج Asyncify مع ميزة انتظار المزامنة في JavaScript بدلاً من استخدام واجهة برمجة التطبيقات المستندة إلى معاودة الاتصال. لإجراء ذلك، بدلاً من الاتصال بـ Asyncify.handleSleep()، يمكنك الاتصال بـ Asyncify.handleAsync(). وبعد ذلك، بدلاً من تحديد موعد لاستدعاء wakeUp()، يمكنك تمرير دالة async على JavaScript واستخدام 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 تسمح لك بمعالجة الإحالات الناجحة بين قيم JavaScript وC++. يدعم أيضًا ميزة "عدم المزامنة"، لذلك يمكنك استدعاء 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 تريد ربطه بواجهة برمجة تطبيقات غير متزامنة على الويب. تبين، يمكنك القيام بذلك أيضًا!

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

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

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

تكاليف التحويل

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

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

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

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

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

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

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

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

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

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

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

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

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

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

وعلى الرغم من ذلك، قد تكون المقالة مرتبطة بمشاركة مدونة أخرى.

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