ما هو WebAssembly وأين أتى؟

منذ أن أصبح الويب نظامًا أساسيًا ليس فقط للمستندات ولكن أيضًا للتطبيقات، دفعت بعض التطبيقات الأكثر تقدمًا متصفحات الويب إلى أقصى حد ممكن. هناك أسلوب "الاقتراب أكثر من المعدن" من خلال التفاعل مع لغات ذات مستوى أقل بهدف تحسين الأداء في العديد من اللغات ذات المستوى الأعلى. فعلى سبيل المثال، تتضمن Java الواجهة الأصلية لـ Java. بالنسبة إلى JavaScript، هذه اللغة ذات المستوى الأدنى هي WebAssembly. سوف تكتشف في هذه المقالة ماهية لغة التجميع ولماذا يمكن أن تكون مفيدة على الويب، ثم ستتعرف على كيفية إنشاء WebAssembly من خلال حل asm.js المؤقت.

لغة التجميع

هل سبق لك أن أجريت البرمجة بلغة التجميع؟ في برمجة الكمبيوتر، يُشار غالبًا إلى لغة التجميع باسم "التجميع" ويشيع اختصارها باسم ASM أو asm، وهي أي لغة برمجة منخفضة المستوى لها علاقة قوية جدًا بين التعليمات الواردة في اللغة وتعليمات الرمز البرمجي للبنية الأساسية.

على سبيل المثال، عند الاطّلاع على بنية Intel® 64 وIA-32 (ملف PDF)، تنفّذ التعليمات MUL (من أجل إجراء التلميح) عملية ضرب بدون توقيع للمعامل الأول (معامل الوجهة) والمعامل الثاني (معامل المصدر)، ثم تخزِّن النتيجة في معامل الوجهة. إنّ معامل الوجهة بسيط جدًا، وهو عبارة عن معامل ضمني موجود في السجلّ AX، بينما يوجد معامل المصدر في سجلّ للأغراض العامة، مثل CX. يتم حفظ النتيجة مرة أخرى في السجلّ AX. ضع في الاعتبار المثال التالي لرمز x86:

mov ax, 5  ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx     ; Multiply the value of register AX (5)
           ; and the value of register CX (10), and
           ; store the result in register AX.

للمقارنة، إذا تم تكليفك بهدف ضرب 5 و10، فمن المحتمل أن تكتب تعليمة برمجية مشابهة لما يلي في JavaScript:

const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;

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

كما يوحي الاسم، يعتمد الكود x86 على بنية x86. ماذا لو كانت هناك طريقة لكتابة كود التجميع لا تعتمد على بنية معينة، ولكنها ستكتسب فوائد أداء التجميع؟

asm.js

كانت الخطوة الأولى لكتابة رمز التجميع بدون تبعيات للبنية هي asm.js، وهي مجموعة فرعية صارمة من لغة JavaScript يمكن استخدامها كلغة هدف منخفضة المستوى وفعّالة لبرامج التجميع. قدَّمت هذه اللغة الفرعية وصفًا فعّالاً للآلة الافتراضية المُضمَّنة في وضع الحماية للغات غير الآمنة للذاكرة، مثل C أو C++. أتاحت مجموعة من عمليات التحقّق الثابتة والديناميكية لمحرّكات JavaScript استخدام استراتيجية تجميع تعمل على تحسين الأداء في وقت مبكر من أجل الحصول على رمز asm.js صالح. تمت ترجمة الرمز المكتوب بلغات مكتوبة بشكلٍ ثابت مع إدارة الذاكرة اليدوية (مثل C) باستخدام برنامج تجميع من مصدر إلى مصدر مثل early Emscripten (استنادًا إلى LLVM).

تم تحسين الأداء عن طريق حصر الميزات اللغوية بتلك القابلة للتفعيل AOT. كان Firefox 22 أول متصفّح يتوافق مع asm.js، وقد تم إصداره باسم OdinMonkey. أضاف Chrome دعم asm.js في الإصدار 61. وعلى الرغم من أن ملف asm.js لا يزال يعمل في المتصفحات، إلا أنه حل محله WebAssembly. قد يكون سبب استخدام asm.js في هذه المرحلة كبديل للمتصفحات التي لا تدعم WebAssembly.

WebAssembly

WebAssembly هي لغة منخفضة المستوى تشبه التجميع ذي تنسيق ثنائي مدمج يعمل بأداء شبه أصلي، كما توفر لغات مثل C/C++ وRst والعديد من اللغات الأخرى ذات هدف التجميع بحيث يمكن تشغيلها على الويب. ولا يزال التوافق مع اللغات المُدارة من خلال الذاكرة مثل Java وDart متاحًا، وسيصبح متاحًا قريبًا، أو أصبح متاحًا بالفعل كما في الحال مع Kotlin/Wasm. تم تصميم WebAssembly للعمل جنبًا إلى جنب مع JavaScript، ما يتيح لكليهما العمل معًا.

بخلاف المتصفح، تعمل برامج WebAssembly أيضًا في أوقات تشغيل أخرى بفضل WASI، وهي واجهة نظام WebAssembly، وهي واجهة نظام تركيبية لـ WebAssembly. تم إنشاء WASI ليكون قابلاً للنقل على جميع أنظمة التشغيل، بهدف أن يكون آمنًا والقدرة على العمل في بيئة وضع حماية.

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

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

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

بالعودة إلى المثال السابق، سيكون كود WebAssembly التالي مكافئًا للتعليمة البرمجية x86 من بداية المقالة:

i32.const 5  ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul      ; Pop the two most recent items on the stack,
             ; multiply them, and push the result onto the stack.

بينما يتم تنفيذ asm.js بالكامل في البرامج، أي أنه يمكن تشغيل كوده في أي محرك جافا سكريبت (حتى لو كان غير محسَّن)، تطلب WebAssembly وظائف جديدة اتفق عليها جميع موردي المتصفحات. تم الإعلان عن WebAssembly في عام 2015 وتم إصداره لأول مرة في آذار (مارس) 2017، وأصبح اقتراح W3C في 5 كانون الأول (ديسمبر) 2019. وتحافظ W3C على المعيار نفسه من خلال المساهمات من جميع موردي المتصفحات الرئيسية والجهات الأخرى المهتمة. منذ عام 2017، أصبح التوافق مع المتصفحات في نطاق عالمي.

تتضمن WebAssembly تمثيلان: نصي وثنائي. ما تراه أعلاه هو التمثيل النصي.

التمثيل النصي

ويستند التمثيل النصي إلى تعبيرات S وعادةً ما يستخدم امتداد الملف .wat (في تنسيق WebAفي التجميع). إذا كنت تريد ذلك، فيمكنك كتابتها يدويًا. بأخذ مثال الضرب المذكور أعلاه وجعله أكثر فائدة من خلال عدم ترميز العوامل بشكل ثابت، ربما يمكنك فهم التعليمة البرمجية التالية:

(module
  (func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
    local.get $factor1
    local.get $factor2
    i32.mul)
  (export "mul" (func $mul))
)

التمثيل الثنائي

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

0000000: 0061 736d                             ; WASM_BINARY_MAGIC
0000004: 0100 0000                             ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                    ; section code
0000009: 00                                    ; section size (guess)
000000a: 01                                    ; num types
; func type 0
000000b: 60                                    ; func
000000c: 02                                    ; num params
000000d: 7f                                    ; i32
000000e: 7f                                    ; i32
000000f: 01                                    ; num results
0000010: 7f                                    ; i32
0000009: 07                                    ; FIXUP section size
; section "Function" (3)
0000011: 03                                    ; section code
0000012: 00                                    ; section size (guess)
0000013: 01                                    ; num functions
0000014: 00                                    ; function 0 signature index
0000012: 02                                    ; FIXUP section size
; section "Export" (7)
0000015: 07                                    ; section code
0000016: 00                                    ; section size (guess)
0000017: 01                                    ; num exports
0000018: 03                                    ; string length
0000019: 6d75 6c                          mul  ; export name
000001c: 00                                    ; export kind
000001d: 00                                    ; export func index
0000016: 07                                    ; FIXUP section size
; section "Code" (10)
000001e: 0a                                    ; section code
000001f: 00                                    ; section size (guess)
0000020: 01                                    ; num functions
; function body 0
0000021: 00                                    ; func body size (guess)
0000022: 00                                    ; local decl count
0000023: 20                                    ; local.get
0000024: 00                                    ; local index
0000025: 20                                    ; local.get
0000026: 01                                    ; local index
0000027: 6c                                    ; i32.mul
0000028: 0b                                    ; end
0000021: 07                                    ; FIXUP func body size
000001f: 09                                    ; FIXUP section size
; section "name"
0000029: 00                                    ; section code
000002a: 00                                    ; section size (guess)
000002b: 04                                    ; string length
000002c: 6e61 6d65                       name  ; custom section name
0000030: 01                                    ; name subsection type
0000031: 00                                    ; subsection size (guess)
0000032: 01                                    ; num names
0000033: 00                                    ; elem index
0000034: 03                                    ; string length
0000035: 6d75 6c                          mul  ; elem name 0
0000031: 06                                    ; FIXUP subsection size
0000038: 02                                    ; local name type
0000039: 00                                    ; subsection size (guess)
000003a: 01                                    ; num functions
000003b: 00                                    ; function index
000003c: 02                                    ; num locals
000003d: 00                                    ; local index
000003e: 07                                    ; string length
000003f: 6661 6374 6f72 31            factor1  ; local name 0
0000046: 01                                    ; local index
0000047: 07                                    ; string length
0000048: 6661 6374 6f72 32            factor2  ; local name 1
0000039: 15                                    ; FIXUP subsection size
000002a: 24                                    ; FIXUP section size

التحويل إلى WebAssembly

كما ترون، ليس .wat أو .wasm مناسبين للمستخدمين على وجه التحديد. وهنا يأتي دور أداة التحويل البرمجي مثل Emscripten. وهي تتيح لك التجميع من لغات ذات مستوى أعلى مثل C وC++. وهناك برامج تجميع أخرى للغات أخرى مثل Rust وغيرها الكثير. ضع في اعتبارك التعليمة البرمجية التالية لـ C:

#include <stdio.h>

int main() {
  printf("Hello World\n");
  return 0;
}

عادةً ما تقوم بتجميع البرنامج C هذا باستخدام برنامج التجميع gcc.

$ gcc hello.c -o hello

بعد تثبيت Emscripten، يمكنك تجميعه في WebAssembly باستخدام الأمر emcc والوسيطات نفسها تقريبًا:

$ emcc hello.c -o hello.html

سيؤدي هذا الإجراء إلى إنشاء ملف hello.wasm وملف برنامج تضمين HTML hello.html. عند عرض الملف hello.html من خادم ويب، سيظهر لك "Hello World" مطبوعًا على وحدة التحكّم في أدوات مطوّري البرامج.

هناك أيضًا طريقة للتجميع في WebAssembly بدون برنامج تضمين HTML:

$ emcc hello.c -o hello.js

وعلى النحو السابق، سيؤدي هذا الإجراء إلى إنشاء ملف hello.wasm، ولكن في هذه المرة سيتم إنشاء ملف hello.js بدلاً من برنامج تضمين HTML. للاختبار، شغِّل ملف JavaScript الناتج hello.js باستخدام، على سبيل المثال، Node.js:

$ node hello.js
Hello World

مزيد من المعلومات

هذه المقدمة الموجزة لـ WebAssembly هي مجرد غيض فيض. تعرّف على مزيد من المعلومات حول WebAssembly في مستندات WebAssembly حول MDN وراجِع مستندات Emscripten. في الواقع، إنّ العمل باستخدام WebAssembly يشبه إلى حدّ ما كيفية رسم ميمز بومة، لا سيما أنّ مطوّري برامج الويب الذين لديهم دراية بـ HTML وCSS وJavaScript ليسوا بالضرورة على دراية باللغات التي سيتم تجميعها من خلالها، مثل C. لحسن الحظ، هناك قنوات مثل علامة webassembly من StackOverflow يسهُل فيها الخبراء تقديم المساعدة لك إن وجّهت إليهم طلبًا بذلك.

شكر وتقدير

تمّت مراجعة هذه المقالة من قِبل جاكوب كوميرو وديريك شوف وراشيل أندرو.