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

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

لغة التجميع

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

على سبيل المثال، عند الاطّلاع على بنية Intel® 64 وIA-32 (بتنسيق PDF)، تؤدي تعليمات MUL (لنصائح 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 استخدام استراتيجية التحويل البرمجي مسبقًا (AOT) لتحسين رمز 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++ وRust وغيرها الكثير مع هدف التجميع كي يتم تشغيله على الويب. لا يزال هناك دعم للّغات التي تتم إدارتها من خلال الذاكرة، مثل Java وDart، ومن المفترض أن يصبح متاحًا قريبًا، أو أصبح متاحًا كما هو الحال مع Kotlin/Wasm. تم تصميم WebAssembly لتعمل إلى جانب JavaScript، ما يسمح لكليهما بالعمل معًا.

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

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

تتم عملية التنفيذ المفاهيمي للتعليمات عن طريق عدّاد البرنامج التقليدي الذي يتقدم من خلال التعليمات. من الناحية العملية، تجمّع معظم محركات 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 في البرنامج بالكامل، يمكن تشغيل الرمز البرمجي الخاص به في أي محرّك JavaScript (حتى إذا لم يتم تحسينه)، إلا أنّ WebAssembly يتطلب وظائف جديدة وافق عليها جميع مورّدي المتصفّحات. تم الإعلان عن WebAssembly في عام 2015 وطرحه لأول مرة في آذار (مارس) 2017، وأصبح اقتراح W3C في 5 كانون الأول (ديسمبر) 2019. ويحافظ W3C على المعيار من خلال المساهمات من جميع موردي المتصفحات الرئيسية والأطراف المعنية الأخرى. منذ عام 2017، صار دعم المتصفحات عالميًا.

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

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

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

(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، يمكنك تحويل الرمز البرمجي أعلاه إلى التمثيل الثنائي التالي. (لا تكون التعليقات عادةً جزءًا من التمثيل الثنائي، ولكن تتم إضافتها بواسطة أداة wat2wasm لفهمها بشكل أفضل).

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، والتي غالبًا ما يرحب الخبراء بمساعدتها إذا سألت عنها بطريقة جيدة.

شكر وتقدير

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