واجهات برمجة التطبيقات الخاصة بعمليات الإدخال/الإخراج على الويب غير متزامنة، ولكنها متزامنة في معظم لغات النظام. عند 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، قد تبدو واجهة برمجة التطبيقات أبسط، ولكن تنطبق المبادئ نفسها. ما عليك سوى إجراء مكالمة والانتظار بشكل متزامن إلى أن يتم عرض النتيجة، بينما تُجري جميع العمليات المُكلّفة وتُظهر النتيجة في نهاية المطاف في عملية invocation واحدة:
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 من بدء عملية "قراءة الملف" وحظر كل شيء آخر، أي علامة التبويب بالكامل، أو المتصفح بالكامل في السابق، وذلك لفترة تتراوح بين مللي ثانية وبضعة ثوانٍ إلى أن تنتهي العملية.
بدلاً من ذلك، يُسمح للرمز فقط بجدولة عملية I/O مع طلب استدعاء ليتم تنفيذه بعد الانتهاء. ويتم تنفيذ عمليات الاستدعاء هذه كجزء من حلقة أحداث المتصفّح. لن أتحدث عن تفاصيل هذه العملية هنا، ولكن إذا كنت مهتمًا بمعرفة آلية عمل حلقة الأحداث، يمكنك الاطّلاع على مقالة المهام والمهام الصغيرة والقوائم وعمليات الجدولة التي تشرح هذا الموضوع بالتفصيل.
في الأساس، يُشغِّل المتصفّح كل أجزاء الرمز البرمجي في حلقة لا تنتهي، وذلك من خلال أخذها من "قائمة الانتظار" واحدة تلو الأخرى. عند بدء حدث معيّن، يضع المتصفّح معالِج الحدث المعنيّ في "قائمة الانتظار"، وفي دورة التكرار التالية، يتم إخراجه من قائمة الانتظار وتنفيذه. تسمح هذه الآلية بمحاكاة المعالجة المتزامنة وتنفيذ الكثير من العمليات المتزامنة باستخدام سوى سلسلة محادثات واحدة.
من المهم تذكُّر أنّه أثناء تنفيذ رمز JavaScript (أو WebAssembly) المخصّص، يتم حظر حلقة الأحداث، ولا يمكن التفاعل مع أيّ معالجات خارجية أو أحداث أو عمليات إدخال/إخراج وما إلى ذلك. والطريقة الوحيدة لاسترداد نتائج الإدخال/الإخراج هي تسجيل callback (دالة استدعاء) وتنفيذ الرمز البرمجي وإنهاء عملية التحكّم في المتصفّح كي يواصل processing أيّ مهام في انتظار المراجعة. بعد انتهاء عمليات الإدخال/الإخراج، سيصبح معالِج الأحداث أحد هذه المهام وسيصبح مُنفَّذًا.
على سبيل المثال، إذا أردت إعادة كتابة العيّنات أعلاه بلغة 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.
بفضل الطبيعة غير المتزامنة لهذه الخطوات، يمكن للدالة الأصلية إعادة التحكّم إلى المتصفّح فور جدولة عمليات الإدخال/الإخراج، وترك واجهة المستخدم بأكملها سريعة الاستجابة ومتاحة للقيام ب tasks أخرى، بما في ذلك العرض والتنقّل وما إلى ذلك، بينما يتم تنفيذ عمليات الإدخال/الإخراج في الخلفية.
في ما يلي مثال آخر: حتى واجهات برمجة التطبيقات البسيطة، مثل "الاستراحة"، التي تجعل التطبيق ينتظر مدّة محدّدة بالثواني، هي أيضًا شكل من أشكال عمليات الإدخال/الإخراج:
#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 وتسمح بتوقيف البرنامج بأكمله مؤقتًا واستئنافه بشكل غير متزامن لاحقًا.
الاستخدام في 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 الذي تريد ربطه بواجهة برمجة تطبيقات غير متزامنة على الويب. تبيّن أنّه يمكنك إجراء ذلك أيضًا.
أولاً، عليك تعريف هذه الدالة على أنّها استيراد عادي من خلال رمز 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% تقريبًا قبل الضغط.
هذا الإجراء ليس مثاليًا، ولكنه مقبول في كثير من الحالات عندما يكون البديل هو عدم توفّر الوظيفة تمامًا أو إجراء عمليات إعادة كتابة كبيرة للرمز البرمجي الأصلي.
احرص دائمًا على تفعيل التحسينات في النُسخ النهائية لتجنُّب ارتفاع هذا الرقم. يمكنك أيضًا التحقّق من خيارات التحسين الخاصة بميزة 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، التي يُحتمل أن تكون المكتبة الأصلية الأكثر رواجًا للعمل مع devices USB، بواجهة برمجة التطبيقات WebUSB API، التي توفّر إمكانية الوصول غير المتزامن إلى هذه الأجهزة على الويب. بعد الربط والتجميع، حصلت على اختبارات libusb ومثالاتها العادية لتشغيلها على الأجهزة التي تم اختيارها مباشرةً في الوضع المحمي لأحد صفحات الويب.
من المحتمل أن تكون هذه قصة لمقالة أخرى في المدونة.
توضّح هذه الأمثلة مدى فعالية Asyncify في سد الفجوة ونقل كل أنواع التطبيقات إلى الويب، ما يتيح لك الوصول إلى جميع الأنظمة الأساسية ووضع الحماية وأمانًا أفضل، بدون فقدان الوظائف.