منذ أن أصبح الويب منصةً لا تستضيف المستندات فحسب بل التطبيقات أيضًا، دفعت بعض التطبيقات الأكثر تقدّمًا متصفّحات الويب إلى أقصى حدودها. يتم استخدام أسلوب "الاقتراب من المعدن" من خلال التفاعل مع لغات منخفضة المستوى لتحسين الأداء في العديد من اللغات عالية المستوى. على سبيل المثال، تتضمّن 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 باستخدام استراتيجية تجميع محسّنة مسبقًا (AOT) لرمز asm.js صالح. كانت الرموز المكتوبة بلغات ذات أنواع ثابتة مع إدارة يدوية للذاكرة (مثل C) تتم ترجمتها بواسطة برنامج ترجمة من المصدر إلى المصدر، مثل الإصدارات الأولى من Emscripten (استنادًا إلى LLVM).
تم تحسين الأداء من خلال حصر ميزات اللغة في تلك التي يمكن ترجمتها مسبقًا. كان 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
(لتنسيق نص WebAssembly). وإذا أردت ذلك، يمكنك كتابته يدويًا. باستخدام مثال الضرب من الأعلى وجعله أكثر فائدة من خلال عدم الترميز الثابت للعوامل، يمكنك على الأرجح فهم الرمز التالي:
(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 حيث يسعد الخبراء غالبًا بتقديم المساعدة إذا طلبتها بلطف.
الإقرارات
راجع هذه المقالة جاكوب كومرو وديريك شوف وراشيل أندرو.