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

واجهات برمجة التطبيقات الخاصة بعمليات الإدخال/الإخراج على الويب غير متزامنة، ولكنها متزامنة في معظم لغات النظام. عند compiling code to 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;
}

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

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

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

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

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

ولا يقتصر ذلك على 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) المخصّص، يتم حظر حلقة الأحداث، ولا يمكن التفاعل مع أيّ معالجات خارجية أو أحداث أو عمليات إدخال/إخراج وما إلى ذلك. والطريقة الوحيدة لاسترداد نتائج الإدخال/الإخراج هي تسجيل callback (دالة استدعاء) وتنفيذ الرمز البرمجي وإنهاء عملية التحكّم في المتصفّح كي يواصل معالجة أيّ مهام في انتظار المراجعة. بعد انتهاء عمليات الإدخال/الإخراج، سيصبح معالِج الأحداث أحد هذه المهام وسيصبح تنفيذه ممكنًا.

على سبيل المثال، إذا أردت إعادة كتابة العيّنات أعلاه بلغة JavaScript الحديثة وقرّرت قراءة اسم من عنوان URL عن بُعد، عليك استخدام واجهة برمجة التطبيقات Fetch API وبنية 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 كل المحتوى، يُستخدَم آخر طلب استدعاء لطباعة "مرحبًا، (username)!" فيconsole.

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

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

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

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

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

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

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

في الواقع، بالنسبة إلى واجهات برمجة التطبيقات المستندة إلى الوعد مثل fetch()، يمكنك أيضًا دمج Asyncify مع ميزة async-await في JavaScript بدلاً من استخدام واجهة برمجة التطبيقات المستندة إلى دالة الاستدعاء. لذلك، بدلاً من Asyncify.handleSleep()، يُرجى الاتصال على Asyncify.handleAsync(). بعد ذلك، بدلاً من الحاجة إلى جدولة callback 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 . تتيح هذه الميزة أيضًا استخدام Asyncify، لذلك يمكنك استدعاء await() في Promise الخارجية وسيعمل تمامًا مثل await في رمز JavaScript الذي يستخدم 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 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) ويمكن استدعاؤه على النحو التالي:

wasm-opt -O2 --asyncify \
      --pass-arg=[email protected]_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، يبدأ عملية تتم بشكل غير متزامن، ويحفظ الحالة الكاملة للتطبيق، بما في ذلك تسلسل استدعاء الدوال وأي متغيرات محلية مؤقتة، وبعد ذلك، عند الانتهاء من هذه العملية، يعيد كل الذاكرة وتسلسل استدعاء الدوال ويتابع التنفيذ من المكان نفسه وبالحالة نفسها كما لو أنّ البرنامج لم يتوقف أبدًا.

يشبه ذلك إلى حد كبير ميزة async-await في 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، ويُعيد استدعاء الدالة. هذه المرة، يتم تخطّي فرع "التنفيذ العادي"، لأنّه سبق أن نفَّذ المهمة في المرة السابقة وأريد تجنُّب طباعة "أ" مرّتين، وبدلاً من ذلك، ينتقل مباشرةً إلى فرع "إعادة اللف". بعد الوصول إلى هذه البيانات، تستعيد جميع البيانات المخزَّنة محليًا، وتعيد الوضع إلى "عادي" وتستمرّ في التنفيذ كما لو لم يتم إيقاف الرمز أبدًا في المقام الأول.

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

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

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

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

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

عروض توضيحية في الواقع

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

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

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

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

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

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

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

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

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

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

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