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

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

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

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

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

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

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

ومع ذلك، يمكن استخدام منصّتَي برمجة التطبيقات فقط، وهما مساحة التخزين في الذاكرة و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.

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

كمثال أخير، حتى واجهات برمجة التطبيقات البسيطة مثل "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);

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

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

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

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

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

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