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

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

لغة التجميع

هل سبق لك البرمجة بلغة التجميع؟ في برمجة الكمبيوتر، تُعدّ لغة التجميع، التي يُشار إليها غالبًا باسم Assembly ويختصرها البعض باسم 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) بواسطة مترجم من المصدر إلى المصدر مثل الإصدار المبكر من 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 (لتنسيق نصAssembly t). يمكنك كتابته يدويًا إذا أردت. بالاستناد إلى مثال الضرب أعلاه وجعله أكثر فائدة من خلال عدم ترميز المُضاعِفات بشكلٍ ثابت، يمكنك على الأرجح فهم الرمز التالي:

(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 حيث يسعد الخبراء غالبًا بتقديم المساعدة إذا طلبت منهم ذلك بشكل مهذَّب.

الشكر والتقدير

راجع هذه المقالة كلّ من Jakob Kummerow وDerek Schuff وRachel Andrew.