كل ما تريد معرفته عن WebAssembly

الوقت المقدر للقراءة: 16 دقائق

إذا لم تسمع بعد عن WebAssembly، فستسمع عنه قريبًا على رغم من وجوده في كل مكان، فهو مدعوم من أغلب المتصفحات (وسيدعم الخادم قريبًا) وهو سريع ويُستخدم في الألعاب، وهو معيار مفتوح من اتحاد شبكة الويب العالميّة (W3C)، والتي هي المنظمة الدولية للمعايير الرئيسيّة لشبكة الإنترنت.

ستندهش على الأغلب من ذلك، وسترغب في تعلم البرمجة به، لكن هذا ليس صحيح تمامًا، فأنت لا تكتب شيفرات باستخدام WebAssembly، لنأخذ بعض الوقت ونتعلم حول هذه التكنولوجيات التي تختصر في العادة إلى “Wasm”.

من أين أتت؟

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

ومع ذلك، على الرغم من أن المهندسين المسؤولين عن متصفحات الويب الشهيرة كانوا متفقين بشكل عام على مشكلة الأداء، فلم يتفقوا حول حل لها. فلقد ظهر معسكران، بدأت جوجل مشروع عميل Native وبعد ذلك نسخة محمولة منه للتركيز على السماح للألعاب وبقيّة البرامج المكتوبة بلغة C و ‎C++‎ للعمل في مكان آمن في كروم Chrome. وفي نفس الوقت، حصلت Mozilla على دعم Microsoft لـ asm.js، وهو أسلوب لتحديث المتصفح للعمل على مجموعة فرعية من تعليمات جافا سكربت منخفضة المستوى بسرعة كبيرة (ظهر مشروع آخر مكّن من تحويل شيفرات C و C++‎ إلى هذه التعليمات).

لم يحصل أي معسكر على تبني واسع، لذلك اتفقوا جميعًا على التعاون في 2015 حول معيار جديد يدعى WebAssembly مبني على نهج بسيط مأخوذ من asm.js، وكما كتب Stephen Shankland في CNET:

“في الويب اليوم، سيحوّل جافا سكربت المتصفح هذه التعليمات إلى شيفرات الآلة. لكن مع WebAssembly، سيقوم المبرمج بالكثير من العمل في وقت سابق لإنشاء برنامج بين هاتين الحالتين، وسيحرر هذا المتصفح من الكثير من العمل لإنشاء شيفرة الآلة، لكنه في نفس الوقت سيفي بوعد الويب، والذي هو أن البرنامج سيعمل على أي جهاز مع متصفح مهما كانت تفاصيل المعدات.”

أعلنت موزيلا في 2017 عن منتج الحد الأدنى (Minimum viable product) للتجربة، وجميع المتصفحات تبنته في نهاية ذلك العام، وفي ديسمبر 2019، أطلقت مجموعة عمل WebAssembly مواصفات WebAssembly الثلاثة كتوصيات W3C.

يعرّف WebAssembly صيغة شيفرة بصيغة ثنائية (binary code) محمولة للبرامج التنفيذيّة، و مقابلها بلغة التجميع، وواجهات لتسهيل التفاعل بين هذه البرامج والبيئة المستضيفة. تعمل شيفرة البرمجية لـ WebAssembly في آلات افتراضية ذات مستوى منخفض الذي يحاكي وظائف العديد من المعالجات الدقيقة التي يمكن تشغيلها عن طريق ترجمة فوريّة – Just-In-Time‏ (JIT) – أو تفسير – interpretation -، يمكن لمحرّك WebAssembly العمل بنفس السرعة تقريبًا للترجمة في منصة Native.

لماذا الاهتمام الآن؟

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

ومع ذلك، فإن WebAssembly ليس للمتصفحات فقط، ففي عام 2019، أعلنت موزيلا عن مشروع يدعى WASI اختصارًا للعبارة WebAssembly System Interface أي واجهة نظام WebAssembly لوضع معيار لكيفية تعامل شيفرة WebAssembly مع نظام التشغيل خارج سياق المتصفح، وبالجمع ما بين دعم المتصفح لـ WebAssembly و WASI ستتمكن من تشغيل ملفات الثنائيّة المترجمة في داخل وخارج المتصفحات، عن طريق مختلف الأجهزة وأنظمة التشغيل، بسرعة الآلة (Native) تقريبًا.

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

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

تنبع ميّزة WebAssembly أخرى، من حقيقة أن الشيفرة البرمجية تعمل في آلة افتراضيّة، ونتيجة لذلك، كل وحدة WebAssembly تنفذ في بيئة منعزلة عن وقت تنفيذ المضيف باستخدام تقنية عزل الخطأ (fault isolation). ويتضمن هذا من الأشياء الأخرى، أن تنفّذ التطبيقات في منعزل عن بقيّة بيئة المضيف ولا يمكنها الهروب من البيئة المنعزلة دون المرور عن طريق API.

أمثلة عمليّة WebAssembly

مثال على WebAssembly هو Enarx.

يوفّر مشروع Enarx استقلاليّة الأجهزة لتأمين التطبيقات باستخدام بيئات التنفيذ الموثوق (Trusted Execution Environments)، فيسمح لك هذا الأخير بتسليم تطبيق مترجم إلى WebAssembly بشكل آمن إلى مزود السحابي (cloud) وتشغيله عن بعد، وكما يقول مهندس الأمن في Red Hat المهندس Nathaniel McCallum:

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

ومثال آخر لذلك هو OPA أي عميل السياسة المفتوحة (Open Policy Agent)، والذي أعلن في نوفمبر 2019 أنه يمكنك ترجمة لغة تعريف السياسة الخاصة بهم Rego إلى WebAssembly، يسمح لك Rego بكتابة المنطق للبحث والجمع بين بيانات JSON/YAML من مختلف المصادر لسؤال عن أسئلة مثل “هل API هذا متاح؟”.

أستخدمت OPA لتطبيقات تفعيل السياسة (policy-enable software) والتي من بينها Kubernetes، ويُعتبر تبسيط السياسة باستخدام أدوات مثل OPA خطوة مهمة لتأمين نشر Kubernetes بشكل جيّد بين البيئات المختلفة، فمحمولية وميزات الأمن المدمجة في WebAssembly تعتبر أسلحة ممتازة لهذه الأدوات.

آخر مثال على ذلك هو Unity، لقد ذكرنا في بداية المقال عن أنه WebAssembly يُستخدم في الألعاب، فيعتبر يونتي Unity والذي هو محرّك ألعاب متعدّد المنصات من أوائل المتبنين للـ WebAssembly، حيث وفروا أول نسخة تجريبيّة لـ Wasm في المتصفحات، ومنذ الشهر الثامن من سنة 2018، تُستخدم WebAssembly كهدف للمخرجات لهدف بناء Unity WebGL.

نظرة أعمق على WebAssembly

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

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

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

لغات تدعم WebAssembly

يمكن استخدام العديد من اللغات للترجمة إلى WebAssembly، بما في ذلك C#‎ وGo، فلماذا لا نستخدمهم بدلًا من Rust؟ على الرغم من أن استخدام لغات البرمجة هو أمر شخصي يختلف حسب تفضيلات الشخص، إلا أن هنالك العديد من الأسباب التي تجعل من Rust أفضل أداة لهذا العمل، فهذه اللغات تملك وقت تشغيل كبير يجب وضعه في ملف الثنائي لـ WebAssembly، لذلك فهي مناسبة فقط للمشاريع الجديدة غير المبنيّة على مشاريع أخرى (على سبيل المثال، فهي مناسبة كبديل لجافا سكربت)، هذا المقال ستجد في قسم Wasm في ويكي لغة GO يقول أن أصغر حجم ملف ثنائي غير مضغوط يمكن عمله هو 2 ميغابايت، بالنسبة للغة Rust، والتي تملك أجزاء وقت التشغيل صغيرة للغاية (مجرّد allocator)، فمثال “أهلا بالعالم” يُترجم إلى ملف بحجم 1.6 كيلوبايت على حاسوبي بدون تحسينات لخفض الحجم (والتي يمكنها تصغيره أكثر).

أنا لا أقول أن للغات Go وC#‎ مستقبل سيء بالنسبة لتطبيقات الويب، فأنا متشوّق حول جديدها، لكنني أعتقد أنها ستكون مناسبة للمشاريع الجديدة غير مبنيّة على مشاريع أخرى.

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

لنسمي لغات C و C++‎ و Rust بلغات النظام، لأن هدفهم برمجة الأنظمة وتطبيقات عاليّة الأداء. تتشارك هذه اللغات بمميزات تجعلهم منسابين للترجمة إلى WebAssembly، سنتعرف في القسم القادم على المزيد من التفاصيل وسنعد أمثلة لشيفرات برمجية كاملة بلغات C و TypeScript مع عينات من لغة صيغة نص WebAssembly.

أنواع البيانات الصريحة وجامع القمامة

تتطلّب لغات الأنظمة أنواع بيانات صريحة مثل int وdouble للتصريح عن متغيرات القيم المرجعة من الدوال، فعلى سبيل المثال، توضح هذه الشيفرة البرمجية عملية جمع 64 بت في لغة السي:

long n1 = random();
long n2 = random();
long sum = n1 + n2;

دوال المكتبة random تعرّف نوع long للإرجاع:

long random(); /* returns a long */

في أثناء الترجمة، تتحول الشيفرة المكتوبة بلغة C إلى لغة التجميع (أسمبلي) ومن ثم إلى لغة الآلة، تتحول الشيفرة البرمجيّة السابقة في لغة التجميع لـ Intel (نوع AT&T)، إلى ما يشبه هذا (حيث أن ## هي التعليقات):

addq %rax, %rdx ## %rax = %rax + %rdx (64-bit addition)

إن ‎%‎rax و ‎%‎rdx هي سجلات 64 بت، وتعليمة addq تعني أجمع quadwords، حيث أن quadwords هي 64 بت، والذي هو المعيار لنوع long في لغة سي، تترجم لغة التجميع التعليمات السابقة إلى لغة الآلة بما في ذلك الأنواع، حيث أن في العادة يقدم النوع من خلال خليط بين التعليمات والمعاملات، وفي هذه الحالة، تعليمة add هي addq (عملية جمع 64 بت)، وهنالك أيضًا addl والتي تجمع قيم 32 بت، أي نوع int في لغة السي. السجلات هي من نوع 64 بت (حرف r في ‎%rax و ‎%rdx) بدلًا من قطع 32 بت منه (على سبيل المثال ‎%eax هو النسخة المنخفضة من 32بت من ‎%rax و ‎%edx هي النسخة 32بت المنخفضة من ‎%rdx).

تنفّذ عملية الجمع في لغة التجميع بأداء عالي لأن المعاملات مخزنة في سجلات CPU، ومترجم لغة C العادي (حتى لو استخدم تحسينات الوضع الافتراضي) سينتج شيفرة برمجيّة بلغة أسمبلي مشابهة للسابق.

تعتبر لغات الأنظمة الثلاثة هي خيارات جيّدة للترجمة إلى WebAssembly بسبب تركيزها على أنواع صريحة كما في WebAssembly: i32 لقيمة عدد صحيح 32 بت و f64 لقيمة الفاصلة العائمة، وهكذا…

تشجع أنواع البيانات الصريحة تحسين استدعاءات الدالة، فتملك الدالة مع نوع بيانات صريح توقيع يحدد أنواع البيانات للمعاملات والقيمة التي ستعود من الدالة (إذا كانت موجودة)، وفي الأسفل توقيع لدالة WebAssembly تسمى ‎$add، وهي مكتوبة بشكل نص لغة WebAssembly كما سنتحدث عليها لاحقًا، تأخذ الدالة عددين صحيحين 32 بت كمعاملات وترجع عدد صحيح 64 بت:

(func $add (param $lhs i32) (param $rhs i32) (result i64))

يجب أن يملك مترجم JIT للمتصفح على معاملات أعداد صحيحة 32 بت ويرجع قيمة 64 بت مخزنة في سجلات بالحجم المناسب.

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

int f(int n) {       /** C **/
  return n + 1;
}

فكل من معامل n و نوع الإرجاع من نوع int بصراحة، والدالة المقابلة لذلك في asm.js ستكون هذه :

function f(n) {      /** asm.js **/
  n = n | 0;
  return (n + 1) | 0;
}

فلغة جافا سكربت لا تعلن صراحة عن أنواع البيانات، لكن عملية OR بتيّة في جافا سكربت ترجع عدد صحيح، هذا مثال لعملية OR البتيّة عديمة الجدوى:

n = n | 0;  /* bitwise-OR of n and zero */

عمليّة OR البتيّة بين n و صفر تساوي n، لكن هدفنا هنا أن n يحتوي على عدد صحيح، فتعليمة return تخدعك عن طريق التحسين.

يتميّز TypeScript بين أنواع جافا سكربت بتبني أنواع البيانات الصريحة، والتي ستجعل من هذه اللغة جذابة للترجمة إلى لغة WebAssembly (ستجد شيفرة برمجية تفسر هذا في الأسفل).

الميّزة المشتركة الثانية بين أنظمة اللغات الثلاثة أنها تنفّذ دون جامع القمامة garbage collector (GC)‎، فمترجم لغة Rust لتخصيص الذاكرة بشكل حيوي يكتب شيفرات البرمجيّة الخاصة بالتخصيص وإلغاء التخصيص (allocation/deallocation) وبالنسبة لبقية اللغات، يقع هذا العمل على المبرمج حيث أن من مهامه تخصيص وإلغاء التخصيص بشكل صريح في هذه الذاكرة، لذلك فلغات الأنظمة تتجنّب تعقيدات جامع القمامة التلقائي.

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

فصل المخاوف بين جافا سكربت و WebAssembly

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

من المهم معرفة كيف جافا سكربت وWebAssembly يعملان معًا، فجافاسكربت مصمم لقراءة وكتابة نموذج كائن المستند (Document Object Model – DOM)، الشجرة التي تمثل صفحة الويب، وعلى نقيض ذلك، لا يأتي WebAssembly على أي وظائف مدمجة لـ DOM، لكن يمكن لـ WebAssembly استيراد الدوال من جافا سكربت واستدعاءها حسب الحاجة، يفسر هذا كيفية عمل ذلك:

DOM<----->JS<----->WebAssembly

سيبقى جافا سكربت في أي نوع من أنواعه يدير DOM، لكن يمكنه استخدام وظائف الأغراض العامة من وحدات WebAssembly، كما في الشيفرة البرمجية السابقة.

تسلسل Hailstone وتخمين Collatz

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

هذه دالة hstone (اختصار لـ Hailstone)، تأخذ عدد صحيح كمعامل. تعريف هذه الدالة كالتالي:

             3N + 1 إذا كان N عدد فردي
hstone(N) =
             N/2 إذا كان N عدد زوجي

على سبيل المثال، hstone(12)‎ ترجع 6 في حين hstone(11)‎ ترجع 34، إذا كان N عدد فردي، فإن 3N+1 هو عدد زوجي، وإذا كان N عدد زوجي، فإن N/2 إما عدد زوجي (مثال 4/2=2) أو فردي (مثال 6/2=3).

يمكن استخدام دالة hstone بطريقة عودية عن طريق تمرير قيمة العودة كالمعامل التالي، وستكون النتيجة هي تسلسل hailstone مثل هذه، والتي تبدأ برقم 24 كمعامل أول، وترجع القيمة 12 كمعامل التالي وهكذا:

24,12,6,3,10,5,16,8,4,2,1,4,2,1,...

يتطلب الأمر 10 استدعاءات للدالة حتى تستقر على الرقم واحد، وسيبقى التسلسل 4,2,1 يتكرر إلى ما لا نهاية لأن (3×1)+1 هو 4، لذلك يقسم على 2 ليعود 2، والذي يقسم على 2 ليكون 1 وهكذا دواليك، ولمزيد من المعلومات يمكنك الإطلاع على تفسير مجلة Plus حول لماذا اسم hailstone هو الاسم المناسب لهذا التسلسل.

لاحظ أن أس 2 تتلاقى بسرعة كبيرة، حيث أنها تتطلّب N تقسيمات على 2 للوصول إلى 1، فعلى سبيل المثال 32 = 25 لديها طول تقارب 5 و 64 = 26 لديها طول تقارب 6 ، نهتم هنا بطول التسلسل من المعامل الأول إلى أول ظهور لرقم 1، فشيفرتي البرمجية بلغة C و TypeScript تحسب طول تسلسل hailstone.

تخمين Collatz هو تسلسل hailstone يتقارب إلى 1 مهما كان المعامل N>0، فلم يستطع أحد تفنيد تخمين Collatz ولم يتمكن أي أحد الحصول على إثبات لجعل التخمين نظرية، فالتخمين، على الرغم من سهولة تجربته ببرنامج، يبقى مشكلة صعبة في الرياضيات.

من C إلى WebAssembly في خطوة واحدة

برنامج hstoneCL أسفله هو تطبيق ليس ويب يمكن ترجمته باستخدام مترجم Cعادي (على سبيل المثال GNU أو Clang)، يولّد هذا البرنامج عدد صحيح عشوائي قيمته N>0 ثمانية مرات ويحسب طول تسلسل hailstone بداية من N، دالتين مهمتين وهما main وhstone عند ترجمة التطبيق إلى WebAssembly.

المثال 1 – دالة hstone في لغة السي

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int hstone(int n) {
  int len = 0;
  while (1) {
    if (1 == n) break;          /* halt on 1 */
    if (0 == (n & 1)) n = n / 2; /* if n is even */
    else n = (3 * n) + 1;       /* if n is odd  */
    len++;                      /* increment counter */
  }
  return len;
}

#define HowMany 8

int main() {
  srand(time(NULL));  /* seed random number generator */
  int i;
  puts("  Num  Steps to 1");
  for (i = 0; i < HowMany; i++) {
    int num = rand() % 100 + 1; /* + 1 to avoid zero */
    printf("%4i %7i\n", num, hstone(num));
  }
  return 0;
}

يمكن ترجمة الشيفرة البرمجية وتشغيلها من سطر الأوامر (حيث أن % موجه لسطر الأوامر) على أي نظام مشابه ليونكس:

% gcc -o hstoneCL hstoneCL.c  ## compile into executable hstoneCL
% ./hstoneCL                  ## execute

هذا مثال لمخرجات عند التشغيل:

  Num  Steps to 1
  88      17
   1       0
  20       7
  41     109
  80       9
  84       9
  94     105
  34      13

تتطلّب لغات الأنظمة (بما في ذلك لغة السي) أداة مخصصّة لترجمة الشيفرة البرمجية إلى وحدة WebAssembly، بالنسبة للغات C وC++‎، يعتبر Emscripten هو الخيار الأكثر الانتشارًا مبني على بنية مترجم LLVM (Low-Level Virtual Machine)‎، استخدمت في امثلتي بلغة C على EmScripten، والذي يمكنك تثبيته بمساعدة هذا الدليل.

يمكن جعل برنامج hstoneCL يعمل على الويب باستخدام Emscription لترجمة الشيفرة البرمجيّة – دون تغيير – إلى وحدة WebAssembly، تُنشئ أداة Emscription صفحة HTML مع جافا سكربت (في asm.js) الذي يتوسط بين DOM ووحدة WebAssembly التي تحسب دالة hstone، وهذه هي الخطوات:

1- ترجمة برنامج hstoneCL الذي لا يعمل على الويب إلى WebAssembly:

% emcc hstoneCL.c -o hstone.html  ## generates hstone.js and hstone.wasm as well

يحتوي ملف hstoneCL.c على الشيفرة البرمجيّة الموجودة في الأعلى، و معامل ‎-o لراية المخرجات والتي ستحدد اسم ملف HTML وسيحمل كل من شيفرة جافا سكربت المولّدة وملف ثنائي WebAssembly نفس الاسم (في هذه الحالة، hstone.js و hstone.wasm على التوالي)، النسخ الأقدم من Emscription (قبل الاصدار 13)، قد يتطلّب تضمين راية -s WASM=1 في أمر الترجمة.

2- استخدم خادم ويب Emscription (أو ما يعادله) لاستضافة تطبيق الويب:

% emrun --no_browser --port 9876 .   ## . is current working directory, any port number you like

لتجاوز رسائل التحذيريّة، يمكنك تضمين راية ‎--no_emrun_detect. يشغّل هذا الأمر خادم الويب، والذي يستضيف جميع الموارد في مجلد العمل الحالي، على وجه الخصوص hstone.html و hstone.js و hstone.wasm.

3- افتح المتصفح مع تفعيل WebAssembly (على سبيل المثال Chrome أو Firefox) وانتقل إلى العنوان : http://localhost:9876/hstone.html.

تظهر هذه الصورة المخرجات من حاسوبي باستخدام متصفح Firefox:

webified-1.png

النتيجة رائعة خاصة أنك كتبت سطر واحد فقط دون أي تغييرات على برنامج C الأصلي.

تحسين برنامج hstone للويب

تترجم أداة Emscription برنامج C إلى وحدة WebAssembly وتولّد جافا سكربت المطلوب، لكن هذه الأداة مناسبة لشيفرة برمجيّة ولّدتها اللآلة، على سبيل المثال، ينتج asm.js ملف بحجم 100 كيلوبايت تقريبًا، تتعامل شيفرة البرمجيّة للجافا سكربت عدة سيناريوهات ولا تستخدم API الأخير الخاص بـ WebAssembly. نسخة مصغّرة من برنامج hstone للويب ستجعل من السهل التركيز على كيف تتعامل وحدة WebAssembly (الموجودة في ملف hstone.wasm) مع جافا سكربت (الموجود في ملف hstone.js).

هنالك مشكلة أخرى: الشيفرة البرمجيّة لـ WebAssembly لا تملك حدود دالية في مصدر البرنامج كما في لغة السي، فعلى سبيل المثال، يملك برنامج ChstoneCL دالتين معرفتين من قبل المستخدم وهما main و hstone، ستكون النتيجة تصدير وحدة WebAssembly دالة تسمى ‎_main لكنها لا تصدر دالة اسمها ‎_hstone (حيث أن دالة main هي نقطة الدخول في برنامج سي). جسم دالة Chstone سيكون في دالة غير مصدّرة أي ملفوفة بـ ‎_main، دوال WebAssembly المصدّرة هي نفسها التي يمكن لجافا سكربت استدعاؤها باسمها، وعلى الرغم من ذلك، من الأفضل تحديد أي دوال لغة المصدر يجب تصديرها بالاسم في شيفرة البرمجيّة لـ WebAssembly.

المثال الثاني – برنامج hstone المنقح

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <emscripten/emscripten.h>

int EMSCRIPTEN_KEEPALIVE hstone(int n) {
  int len = 0;
  while (1) {
    if (1 == n) break;          /* halt on 1 */
    if (0 == (n & 1)) n = n / 2; /* if n is even */
    else n = (3 * n) + 1;       /* if n is odd  */
    len++;                      /* increment counter */
  }
  return len;
}

برنامج hstoneWA المنقح المعروض أعلاه، لا يملك دالة main، فلا حاجة له، لأن البرنامج ليس مصمم للعمل كبرنامج مستقل بل مخصص لوحدة WebAssembly مع دالة تصدير واحدة. يشير توجيه EMSCRIPTEN_KEEPALIVE (المعرّف في الملف الرأCemscripten.h) إلى المترجم لتصدير دالة ‎_hstone في وحدة WebAssembly، إتفاقية الاسم واضحة للغاية: يبقى اسم دالة Cمثل hstone، لكن مع شرطة السفليّة كحرف أول في WebAssembly (أي ‎_hstone في هذه الحالة)، وتتبع مترجمات WebAssembly أخرى اتفاقيات أسماء مختلفة.

للتأكد من عمل ذلك، يمكنك اختصار خطوة الترجمة إلى إنتاج وحدة WebAssembly و جافا سكربت، لكن بدون HTML:

% emcc hstoneWA.c -o hstone2.js  ## we'll provide our own HTML file

يمكن اختصار الملف HTML إلى هذا:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8"/>
    <script src="hstone2.js"></script>
  </head>
  <body/>
</html>

يحمل ملف HTML ملف جافا سكربت، والذي يقوم بجلب وتحميل ملف WebAssembly الثنائي الذي يسمى hstone2.wasm، وبالمناسبة ملف WASM الجديد بنصف حجم الملف الأصلي تقريبًا.

يمكن ترجمة الشيفرة البرمجية للتطبيق كالسابق ومن ثم تشغيله مع خادم الويب المدمج:

% emrun --no_browser --port 7777 .  ## new port number for emphasis

بعد طلب وزيارة ملف HTML في المتصفح (في هذه الحالة Chrome)، يمكن استخدام كونسول ويب للتأكّد من تصدير دالة hstone على أنها ‎_‎‎hstone. هذا مقتطف من جلستي في كونسول الويب، مع ## للتعليقات:

> _hstone(27)   ## invoke _hstone by name
< 111           ## output
> _hstone(7)    ## again
< 16            ## output

يعتبر استخدام موجه EMSCRIPTEN_KEEPALIVE هو طريقة واضحة لجعل مترجم Emscripten وحدة WebAssembly تصدّر أي دالة إلى جافا سكربت، وهذا ما يقوم به هذا المترجم، وأي مستند HTML مخصّص مع جافا سكربت يدوي مناسب يمكنه استدعاء دوال من وحدة WebAssembly وذلك بفضل Emscripten.

ترجمة TypeScript إلى WebAssembly

مثال شيفرة البرمجيّة التالية بلغة TypeScript، والتي هي جافا سكربت مع أنواع بيانات صريحة، ويتطلب هذا Node.js مع مدير الحزم الخاص به npm، يثبت أمر npm التالي AssemblyScript والذي هو مترجم WebAssembly لشيفرة برمجية TypeScript:

% npm install -g assemblyscript  ## install the AssemblyScript compiler

يتكون برنامج TypeScript hstone.ts من دالة واحدة، والتي تسمى hstone أيضًا، وسيسبق نوع البيانات i32 (عدد صحيح 32 بت) المعاملات وأسماء المتغيرات المحليّة (في هذه الحالة، n و len على التوالي):

export function hstone(n: i32): i32 { // will be exported in WebAssembly
  let len: i32 = 0;
  while (true) {
    if (1 == n) break;          // halt on 1
    if (0 == (n & 1)) n = n / 2;  // if n is even
    else n = (3 * n) + 1;       // if n is odd
    len++;                      // increment counter
  }
  return len;
}

تأخذ دالة hsotne معامل واحد من نوع i32 وترجع قيمة من نفس النوع، ويشبه جسم الدالة ذلك الموجود في مثال لغة سي، ويمكن ترجمة الشيفرة المصدريّة إلى WebAssembly كالتالي:

% asc hstone.ts -o hstone.wasm  ## compile a TypeScript file into WebAssembly

حجم ملف WASM hstone.wasm حوالي 14 كيلوبايت.

لإيضاح كيف تحمّل وحدة WebAssembly، يتضمن ملف HTML في الأسفل (index.html في موقعي) سكربت جلب وتحميل وحدة WebAssembly hstone.wasm ومن ثم إنشاء مثيل لهذه الوحدة حتى يمكن استدعاء دالة hstone في كونسول المتصفح.

المثال 3 – صفحة HTML لشيفرة TypeScript

<!doctype html>
<html>
  <head>
    <meta charset="utf-8"/>
    <script>
      fetch('hstone.wasm').then(response =>           <!-- Line 1 -->
      response.arrayBuffer()                          <!-- Line 2 -->
      ).then(bytes =>                                 <!-- Line 3 -->
      WebAssembly.instantiate(bytes, {imports: {}})   <!-- Line 4 -->
      ).then(results => {                             <!-- Line 5 -->
      window.hstone = results.instance.exports.hstone; <!-- Line 6 -->
      });
    </script>
  </head>
  <body/>
</html>

يمكن تفسير عنصر script في HTML السابق سطرً سطرًا، فيستخدم استدعاء fetch في السطر الأول وحدة fetch للحصول على وحدة WebAssembly من خادم الويب الذي يستضيف صفحة HTML، وعند وصول إجابة HTTP، ستعمل وحدة WebAssembly كتسلسل من البيتات، والتي ستكون مخزّنة في arrayBuffer من السكربت في السطر 2، تتكوّن هذه البايتات من وحدة WebAssembly، والتي هي كامل الشيفرة البرمجيّة مترجمة من ملف TypeScript، هذه الوحدة لا تملك أي استدعاءات، كما هو مبيّن في نهاية السطر 4.

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

يصدّر السطر السادس من السكربت دالة hstone من TypeScript الأصليّة بنفس الاسم، وستكون دالة WebAssembly متاحة لأي شيفرة جافا سكربت، كما ستؤكد جلسة أخرى في كونسول المتصفح.

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

الهدف هو الحصول على صفحة ويب تحمّل وحدة WebAssembly بنفس طريقة وحدة JS ES2015:

<script type='module'>...</script>

سيعمل جافا سكربت على جلب وترجمة ومعالجة وحدة WebAssembly كما لو كانت مجرّد واحدة JS أخرى.

لغة صيغة النص (text format language)

يمكن ترجمة الشيفرة الثنائيّة لـ WebAssembly من وإلى مرادفها بصيغة النص، فالشيفرة الثنائيّة تكون موجودة في ملفات بصيغة WASM، في حين أن نصوص قابلة للقراءة من قبل المبرمجين تكون موجودة بملفات بصيغة WAT. إن WABT هي مجموعة من عشرات الأدوات للتعامل مع WebAssembly، بما في ذلك للترجمة من وإلى صيغ مثل WASM و WARK ومن بين أدوات التحويل wasm2wat و wasm2c و wat2wasm.

تتبنى لغة صيغة النص صياغة ‎S-expression‎ ‎(S تعني رمزي symbolic) الشائعة في Lisp، تمثل S-expression واختصارها sexpr شجرة كقائمة مع العديد من القوائم الفرعية بشكل اعتباطي، فعلى سبيل المثال، يحدث هذا sexpr بالقرب من نهاية ملف WAT لمثال TypeScript:

(export "hstone" (func $hstone)) ## export function $hstone by the name "hstone"

الشجرة التي تمثله هي التاليّة:

        export      ## root
          |
     +----+----+
     |       |
  "hstone"    func    ## left and right children
               |
            $hstone   ## single child

في الصيغة النصيّة، وحدة WebAssembly هي sexpr حيث أن الجزء الأول هي الوحدة module والتي هي جذر الشجرة.

هذا مثال ثاني لوحدة معرّفة ومصدّرة كدالة فرديّة، والتي لا تأخذ أي معامل لكنها ترجع ثابت 9876:

(module
  (func (result i32)
    (i32.const 9876)
  )
  (export "simpleFunc" (func 0)) // 0 is the unnamed function's index
)

تعرّف هذه الدالة دون اسم (كما في lambda) وتصدّر بالإشارة إلى مؤشر 0، والذي هو مؤشر أول sexpr متداخل في الوحدة، وإن اسم دالة التصدير هي السلسلة النصيّة “simpleFunc”.

الدوال في صيغة النص تملك نمط قياسي، والذي يمكن تفسيره على النحو التالي:

(func <signature> <local vars> <body>)

يحدّد التوقيع المعاملات (إذا كانت موجودة) ويرجع قيمة العودة (إذا كانت موجودة)، فعلى سبيل المثال، هذا توقيع لدالة لا تملك اسم تأخذ معاملين من نوع عدد صحيح 32 بت وتعيد قيمة عدد صحيح 64 بت:

(func (param i32) (param i32) (result i64)...)

يمكن إعطاء الأسماء إلى الدوال والمعاملات والمتغيرات المحليّة، حيث يبدأ الأسم بعلامة دولار:

(func $foo (param $a1 i32) (param $a2 f32) (local $n1 f64)...)

يعكس جسد دالة WebAssembly بنية الآلة الأساسية لهذه اللغة، فتخزين المكدس هو الأساس، ومثال ذلك دالة تضاعف معامل عددها الصحيح وتعيد القيمة الجديدة:

(func $doubleit (param $p i32) (result i32)
  get_local $p
  get_local $p
  i32.add)

تدفع كل واحدة من عمليات get_local، والتي تعمل على المتغيرات المحلية والمعاملات المشابهة، معامل 32 بت إلى المكدس، وستخرج عملية i32.add أعلى قيمتين (والتي هي في هذه الحالة، القيم الوحيدة) من المكدس بجمعهم، وستكون النتيجة هي القيمة الوحيدة الموجودة في المكدّس وبالتالي سترجع من دالة ‎$doubleit.

عند ترجمة شيفرة WebAssembly إلى لغة الآلة، يجب استبدال أساس مكدس WebAssembly عند الإمكان بسجلات الأغراض العامة، وهذه مهمة مترجم JIT، والتي تترجم شيفرة آلة المكدس الافتراضية لـ WebAssembly إلى شيفرة آلة حقيقيّة.

لا يفضل مبرمجو الويب كتابة WebAssembly بصيغة النص، حيث أن الترجمة من لغة عالية المستوى يعتبر خيار أفضل لهم على عكس كتّاب المترجم، حيث سيجدون من المفيد العمل عليها.

الخاتمة

على الرغم من إحراز الكثير من هدف WebAssembly للوصول إلى سرعة مقاربة لآلة (Native)، لكن لايزال مترجم JIT لجافا سكربت يتحسن مع ظهور أنواع مناسبة للتحسين (مثل TypeScript) تقترب من سرعة الآلة (native)، فهل هذا يعني أن WebAssembly هو مضيعة للوقت؟ لا أعتقد ذلك.

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

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *