تطوير WebAssembly: أشعل النار الحقيقي والأمثلة



تم الإعلان عن WebAssembly في عام 2015 - ولكن الآن ، بعد سنوات ، لا يزال هناك عدد قليل ممن يمكنهم التباهي به في الإنتاج. تعتبر المواد المتعلقة بهذه التجربة أكثر قيمة: المعلومات المباشرة عن كيفية التعايش معها في الممارسة العملية ما زالت قليلة.

في مؤتمر HolyJS ، تلقى تقرير عن تجربة استخدام WebAssembly علامات عالية من الجمهور ، والآن تم إعداد نسخة نصية من هذا التقرير خصيصًا لـ Habr (مرفق فيديو أيضًا).



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

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

كيف قمنا بتنفيذ WebAssembly


أنا أعمل في شركة Inetra ، نحن موجودون في نوفوسيبيرسك ونقوم ببعض مشاريعنا الخاصة. واحد منهم هو ByteFog. هذه تقنية نظير إلى نظير لتقديم الفيديو للمستخدمين. عملاؤنا هم الخدمات التي توزع كمية كبيرة من الفيديو. لديهم مشكلة: عندما يحدث حدث شائع ، على سبيل المثال ، مؤتمر صحفي لشخص ما أو حدث رياضي ، وكيفية عدم الاستعداد له ، تأتي مجموعة من العملاء ، ويعتمدون على الخادم ، ويكون الخادم حزينًا. يتلقى العملاء جودة فيديو رديئة للغاية في هذا الوقت.

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



يجب أن نكون مثبتين في كل جهاز يمكنه عرض الفيديو ، وبالتالي فإننا ندعم مجموعة كبيرة جدًا من المنصات: Windows ، Linux ، Android ، iOS ، Web ، Tizen. ما هي اللغة التي يجب أن تختار قاعدة شفرة واحدة على كل هذه المنصات؟ لقد اخترنا C ++ لأنه تبين أنه يمتلك أكبر عدد من المزايا: - D بشكل أكثر جدية ، لدينا خبرة جيدة في C ++ ، إنها بالفعل لغة سريعة ، ومن المحتمل أن تكون في المرتبة الثانية بعد C.

حصلنا على تطبيق كبير جدًا (900 صف) ، لكنه يعمل جيدًا. تحت Windows و Linux ، نترجم إلى كود أصلي. لنظامي Android و iOS ، نقوم ببناء مكتبة نتصل بها بالتطبيق. سنتحدث عن Tizen مرة أخرى ، ولكن على الويب اعتدنا أن نعمل كمكون إضافي للمتصفح.

هذه هي تقنية Netscape Plugin API. كما يوحي الاسم ، إنه قديم جدًا وله عيب أيضًا: إنه يوفر وصولًا واسعًا للغاية للنظام ، بحيث يمكن أن يتسبب رمز المستخدم في حدوث مشكلة أمنية. ربما هذا هو السبب وراء إيقاف تشغيل Chrome لدعم هذه التقنية في عام 2015 ، ثم انضمت جميع المتصفحات إلى هذا الفلاش mob لذا فقد تركنا بدون إصدار ويب لمدة عامين تقريبًا.

في عام 2017 ، جاء أمل جديد. كما قد تتخيل ، هذا WebAssembly. نتيجة لذلك ، وضعنا أنفسنا مهمة نقل تطبيقنا إلى متصفح. نظرًا لأن دعم Firefox و Chrome قد ظهر بالفعل في الربيع ، وبحلول خريف عام 2017 ، قام Edge و Safari بسحب نفسه.

كان من المهم بالنسبة لنا استخدام الشفرة الجاهزة ، نظرًا لأن لدينا الكثير من منطق العمل الذي لم نكن نريد مضاعفته ، حتى لا نضاعف عدد الأخطاء. خذ المترجم Emscripten. يعمل ما نحتاج إليه - يقوم بتجميع التطبيق الإيجابي في المستعرض وإعادة إنشاء البيئة المألوفة للتطبيق الأصلي في المستعرض. يمكننا القول أن Emscripten مثل Browserify لرمز C ++. كما يسمح لك بإعادة توجيه الكائنات من C ++ إلى JavaScript والعكس. كان فكرنا الأول هو: الآن لنأخذ Emscripten ، مجرد ترجمة ، وكل شيء سينجح. بالطبع ، لم يحدث. من هذا بدأت رحلتنا على طول أشعل النار.

أول ما واجهناه هو الإدمان. كان هناك العديد من المكتبات في قاعدة الشفرة الخاصة بنا. الآن لا معنى لإدراجها ، ولكن بالنسبة لأولئك الذين يفهمون ، لدينا Boost. هذه مكتبة كبيرة تتيح لك كتابة تعليمات برمجية مشتركة بين الأنظمة الأساسية ، لكن من الصعب للغاية تكوين الترجمة باستخدامها. كنت أرغب في سحب أكبر قدر ممكن من الشفرة في المتصفح.

Bytefog العمارة


نتيجةً لذلك ، حددنا العنصر الأساسي: يمكننا أن نقول أن هذا هو خادم وكيل يحتوي على منطق العمل الرئيسي. يأخذ هذا الخادم الوكيل البيانات من مصدرين. الأول والأهم هو HTTP ، أي قناة إلى خادم توزيع الفيديو ، والثاني هو شبكة P2P ، أي قناة إلى وكيل آخر من مستخدم آخر. نعطي البيانات للاعب في المقام الأول ، لأن مهمتنا هي إظهار محتوى عالي الجودة للمستخدم. في حالة بقاء الموارد ، نقوم بتوزيع المحتوى على شبكة P2P بحيث يمكن للمستخدمين الآخرين تنزيله. يوجد في الداخل ذاكرة تخزين مؤقت ذكية تقوم بكل السحر.



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



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



بالنسبة لكل مكان ، حددنا أحد المواصفات ، أي حددنا العقد: ما هي الأساليب ، والمعلمات التي ستحصل عليها ، وأنواع البيانات. بمجرد القيام بذلك ، يمكنك العمل بالتوازي ، كل مطور إلى جانبه.

ما هي النتيجة؟ استبدلنا قناة توصيل الفيديو الرئيسية من المزود بـ AJAX المعتاد. نصدر البيانات إلى المشغل من خلال مكتبة HLS.js الشائعة ، ولكن هناك إمكانية أساسية للتكامل مع لاعبين آخرين ، إذا لزم الأمر. لقد استبدلنا طبقة P2P بأكملها بـ WebRTC.



نتيجة للتجميع ، يتم الحصول على العديد من الملفات. الأهم هو الثنائي. يحتوي على الكود البرمجي المترجم الذي سيتم تنفيذه في المتصفح والذي يحتوي على جميع الإرث C ++. ولكن في حد ذاته لا يعمل ، ما يسمى "رمز الغراء" ضروري ، كما يتم إنشاؤه بواسطة المترجم. يقوم رمز الغراء بتنزيل ملف ثنائي ، وتحميل كل من هذه الملفات إلى الإنتاج. لأغراض تصحيح الأخطاء ، يمكنك إنشاء تمثيل نصي للمجمع - ملف .wast و sourcemap. عليك أن تفهم أنها يمكن أن تكون كبيرة جدا. في حالتنا ، وصلوا إلى 100 ميغابايت أو أكثر.

جمع الحزمة


دعونا نلقي نظرة فاحصة على رمز الغراء. هذا هو ES5 القديم الجيد المعتاد ، ويتم تجميعه في ملف واحد. عندما نقوم بتوصيلها بصفحة ويب ، يكون لدينا متغير عمومي يحتوي على كل ما لدينا من وحدة wasmated ، وهي جاهزة لقبول الطلبات إلى API الخاصة بها.

لكن تضمين ملف منفصل يعد تعقيدًا خطيرًا جدًا للمكتبة التي سيستخدمها المستخدمون. نود أن نضع كل شيء في حزمة واحدة. لهذا نستخدم Webpack وخيار تجميع خاص MODULARIZE.

يلتف الرمز اللاصق في نمط "الوحدة النمطية" ، ويمكننا استلامه: الاستيراد أو الاستخدام يتطلب إذا كتبنا على ES5 - Webpack يفهم بهدوء هذا الاعتماد. كانت هناك مشكلة في بابل - لم يعجبه الكود الكبير من الكود ، لكن هذا رمز ES5 ، لا يحتاج إلى نقله ، نحن فقط نضيفه لتجاهله.

سعيا وراء عدد الملفات ، قررت استخدام خيار SINGLE_FILE. إنه يترجم جميع الثنائيات الناتجة عن التجميع إلى نموذج Base64 ويدفعها إلى الشفرة اللاصقة كسلسلة. يبدو وكأنه فكرة رائعة ، ولكن بعد ذلك أصبحت الحزمة 100 ميغابايت في الحجم. لا Webpack ولا بابل ولا المستعرض يعملان على مثل هذا المجلد. على أي حال ، لن نجبر المستخدم على تحميل 100 ميغا بايت؟!

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

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

وقتها


مشكلة أخرى في الوحدة النمطية هي أنه كائن Thenable ، أي أنه يحتوي على طريقة .then (). تتيح لك هذه الوظيفة تعليق الاتصال في وقت بدء تشغيل الوحدة ، وهي مريحة للغاية. ولكن أود أن تتطابق الواجهة مع Promise. حينها ليس الوعد ، لكنه بخير ، فلنختتمه بأنفسنا. دعنا نكتب هذا الرمز البسيط:

return new Promise((resolve, reject) => { Module(config).then((module) => { resolve(module); }); }); 

نقوم بإنشاء وعد ، ونبدأ الوحدة النمطية لدينا ، وكاستدعاء ندعو وظيفة العزم وتمرير الوحدة النمطية التي قمنا بتثبيتها هناك. يبدو أن كل شيء واضح ، كل شيء على ما يرام ، نحن نطلق - شيء خاطئ ، متصفحنا مجمد ، DevTools لدينا معلقة ، والمعالج يسخن على الكمبيوتر. نحن لا نفهم أي شيء - نوع من العودية أو حلقة لا نهائية. يعد تصحيح الأخطاء أمرًا صعبًا للغاية ، وعندما قمنا بمقاطعة JavaScript ، انتهى بنا الأمر في وظيفة Then في وحدة Emscripten.

 Module['then'] = function(func) { if (Module['calledRun']) { func(Module); } else { Module['onRuntimeInitialized'] = function() { func(Module); }; }; return Module; }; 

دعونا ننظر في الأمر بمزيد من التفاصيل. مؤامرة

 Module['onRuntimeInitialized'] = function() { func(Module); }; 

مسؤولة عن تعليق رد الاتصال. كل شيء واضح هنا: وظيفة غير متزامنة تستدعي معاودة الاتصال. كل شيء كما نريد. هناك جزء آخر لهذه الميزة.

 if (Module['calledRun']) { func(Module); 

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

إذا كنت تقرأ الوثائق بعناية ، فقد تبين أن هناك نقطة خفية حول وعد. عندما نحل الوعد باستخدام Thenable ، سيقوم المتصفح بفك القيم من ذلك Thenable ، وللقيام بذلك ، سوف يستدعي الأسلوب .then (). نتيجة لذلك ، نقوم بحل "الوعد" ، وقم بتمرير الوحدة إليه. يسأل المتصفح: إذن هل هذا كائن؟ نعم ، هذا هو ثماني. ثم يتم استدعاء الدالة .then () على الوحدة النمطية ، ويتم تمرير وظيفة الحل نفسها كرد اتصال.

الوحدة يتحقق إذا كان يعمل. إنه بالفعل قيد التشغيل ، لذلك يتم استدعاء رد الاتصال فورًا ، ويتم تمرير نفس الوحدة مرة أخرى. كرد اتصال ، لدينا وظيفة العزم ، ويسأل المتصفح: هل هذا هو كائن Thenable؟ نعم ، هذا هو ثماني. وكل شيء يبدأ من جديد. ونتيجة لذلك ، فإننا نقع في دائرة لا نهاية لها لا يعود المتصفح إليها أبدًا.



لم أجد حلاً أنيقًا لهذه المشكلة. نتيجة لذلك ، أنا ببساطة حذف طريقة .then () قبل حل ، وهذا يعمل.

مخطوطات


لذلك ، قمنا بتجميع الوحدة النمطية ، تجميعها JS ، ولكن هناك شيء مفقود. ربما نحتاج إلى القيام ببعض الأعمال المفيدة. للقيام بذلك ، قم بنقل البيانات وتوصيل العالمين - JS و C ++. كيف نفعل ذلك؟ يوفر Emscripten ثلاثة خيارات:

  • الأول هو وظائف ccall و cwrap. غالبًا ما تقابلهم في بعض البرامج التعليمية على WebAssembly ، لكنها ليست مناسبة للعمل الحقيقي ، لأنها لا تدعم إمكانيات C ++.
  • والثاني هو WebIDL بيندر. وهو يدعم بالفعل وظائف C ++ ، يمكنك العمل بالفعل معها. هذه لغة وصف واجهة خطيرة تستخدمها ، على سبيل المثال ، W3C لتوثيقها. لكننا لم نرغب في نقله إلى مشروعنا واستخدمنا الخيار الثالث
  • Embind. يمكننا القول أن هذه طريقة أصلية لتوصيل الكائنات لـ Emscripten ، فهي تستند إلى قوالب C ++ وتتيح لك القيام بالكثير من الأشياء عن طريق إعادة توجيه كيانات مختلفة من C ++ إلى JS والعكس.


يتيح لك Embind:

  • استدعاء وظائف C ++ من رمز JavaScript
  • إنشاء كائنات JS من فئة C ++
  • من رمز C ++ ، انتقل إلى واجهة برمجة تطبيقات المتصفح (إذا كنت تريد ذلك لسبب ما ، فيمكنك ، على سبيل المثال ، كتابة إطار الواجهة الأمامية بالكامل في C ++).
  • الشيء الرئيسي بالنسبة لنا: تنفيذ واجهة جافا سكريبت الموصوفة في C ++.


تبادل البيانات


النقطة الأخيرة مهمة ، لأن هذا هو بالضبط الإجراء الذي ستقوم به باستمرار عند نقل التطبيق. لذلك ، أود أن أتناولها بمزيد من التفصيل. الآن سيكون هناك رمز C ++ ، ولكن لا تخاف ، فهو يشبه تقريبًا TypeScript :-D

المخطط كما يلي:



على جانب C ++ ، هناك نواة نرغب في منح الوصول إليها ، على سبيل المثال ، لشبكة خارجية - لتحميل الفيديو. كان يستخدم للقيام بذلك مع مآخذ التوصيل الأصلية ، وكان هناك نوع من عميل HTTP قام بذلك ، لكن لا توجد مآخذ توصيل أصلية في WebAssembly. نحتاج إلى الخروج بطريقة أو بأخرى ، لذلك قطعنا عميل HTTP القديم ، وأدخلنا الواجهة في هذا المكان ، ونطبق هذه الواجهة في JavaScript باستخدام AJAX العادي ، بأي طريقة. بعد ذلك ، سنقوم بتمرير الكائن الناتج مرة أخرى إلى C ++ ، حيث سيستخدمه kernel.

لنجعل أبسط عميل HTTP يمكنه فقط الحصول على طلبات:

 class HTTPClient { public: virtual std::string get(std::string url) = 0; }; 

إلى الإدخال ، يتلقى سلسلة مع URL ليتم تحميلها ، وإلى الإخراج
سلسلة مع نتيجة الطلب. في C ++ ، يمكن أن تحتوي السلاسل على بيانات ثنائية ، لذلك هذا مناسب للفيديو. المخطوطات تجعلنا نكتب هنا
مثل هذا المجمع المخيف:



في الأمر ، الشيء الرئيسي هو شيئان - اسم الوظيفة على جانب C ++ (قمت بتمييزها باللون الأخضر) ، والأسماء المقابلة على جانب JavaScript (قمت بتمييزها باللون الأزرق). نتيجة لذلك ، نكتب إعلان الاتصال:



إنه يعمل مثل كتل Lego ، التي نقوم بتجميعها منها. لدينا فئة ، هذه الفئة لديها طريقة ، ونحن نريد أن نرث من هذه الفئة لتنفيذ الواجهة. هذا كل شيء. نذهب إلى JavaScript ونرث. ويمكن القيام بذلك بطريقتين. الأول هو تمديد. هذا يشبه إلى حد كبير تمتد القديم من العمود الفقري.



تحتوي الوحدة على كل ما جمعته Emscripten ، وله خاصية بواجهة تصدير. نحن نسمي طريقة الامتداد ونمر كائن هناك مع تنفيذ هذه الطريقة ، أي أنه سيتم تنفيذ بعض الطريقة في دالة get
الحصول على معلومات باستخدام أجاكس.

عند الإخراج ، يعطينا تمديد منشئ جافا سكريبت منتظم. يمكننا أن نسميها عدة مرات حسب الضرورة وإنشاء كائنات في الكمية التي نحتاجها. ولكن هناك موقف عندما يكون لدينا كائن واحد ، ونريد فقط تمريره إلى الجانب C ++.



للقيام بذلك ، قم بطريقة ما بربط هذا الكائن بنوع يفهمه C ++. هذا ما تقوم به وظيفة التنفيذ. في الإخراج ، لا يعطي مُنشئًا ، بل كائنًا جاهزًا للاستخدام ، عميلنا ، والذي يمكننا إعادته إلى C ++. يمكنك القيام بذلك ، على سبيل المثال ، مثل هذا:

 var app = Module.makeApp(client, …) 

لنفترض أن لدينا مصنعًا يُنشئ تطبيقنا ، ويأخذ تبعياته في معلمات ، على سبيل المثال ، العميل وشيء آخر. عندما تعمل هذه الوظيفة ، نحصل على كائن تطبيقنا ، الذي يحتوي بالفعل على واجهة برمجة التطبيقات التي نحتاجها. يمكنك أن تفعل العكس:

 val client = val::global(″client″); client.call<std::string>(″get″, val(...) ); 

مباشرة من C ++ ، خذ عميلنا من نطاق المتصفح العالمي. علاوة على ذلك ، بدلاً من العميل ، يمكن أن يكون هناك أي واجهة برمجة تطبيقات للمتصفح ، تبدأ من وحدة التحكم ، وتنتهي بـ DOM API ، WebRTC - كل ما تريد. بعد ذلك ، نسمي الأساليب التي يتبعها هذا الكائن ، ونلتف جميع القيم الموجودة في val class السحري ، والتي يوفرها لنا Emscripten.

أخطاء ملزمة


بشكل عام ، هذا كل شيء ، ولكن عند بدء التطوير ، تنتظرك الأخطاء الملزمة. أنها تبدو شيء مثل هذا:



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

  • أسماء
  • أنواع
  • عدد المعلمات

يعد بناء جملة Embind بناءً على ذلك أمرًا غير معتاد ، ليس فقط بالنسبة للبائعين المتقدمين ، ولكن أيضًا للأشخاص الذين يتعاملون مع C ++. هذا هو نوع من DSL حيث من السهل ارتكاب خطأ ، تحتاج إلى اتباع هذا. عند الحديث عن واجهات ، عندما تقوم بتطبيق نوع من الواجهة في JavaScript ، من الضروري أن تتطابق تمامًا مع ما وصفته في عقدك.

كان لدينا قضية مثيرة للاهتمام. استخدم زميلي جورا ، الذي شارك في المشروع على جانب C ++ ، Extend لاختبار وحداته. لقد عملوا بشكل مثالي من أجله ، لذلك ارتكبهم ونقلهم لي. اعتدت تطبيق لدمج هذه الوحدات في مشروع JS. وتوقفوا عن العمل بالنسبة لي. عندما اكتشفنا ذلك ، اتضح أنه عند الربط في أسماء الوظائف ، حصلنا على خطأ مطبعي.

كما نرى من الاسم ، فإن Extend هو امتداد للواجهة ، لذا إذا ارتكبت خطأ في مكان ما ، لن يلقي Extend خطأ ، فسيقرر أنك قد أضفت للتو طريقة جديدة ، وهذا كل شيء على ما يرام.

أي أنه يخفي أخطاء الربط حتى يتم استدعاء الطريقة نفسها. أقترح استخدام تطبيق في جميع الحالات التي يناسبك ، لأنه يتحقق على الفور من صحة واجهة المعاد توجيهها. ولكن إذا كنت بحاجة إلى Extend ، فيجب عليك تغطية الاختبارات الخاصة بكل طريقة حتى لا تخبطها.

تمديد و ES6


مشكلة أخرى مع Extend هي أنها لا تدعم فئات ES6. عندما ترث كائنًا مشتقًا من فئة ES6 ، فإن Extend تتوقع أن تكون جميع الخصائص قابلة للتعداد ، ولكن مع ES6 ، فإنها ليست كذلك. الأساليب موجودة في النموذج الأولي ولديها عدد لا يحصى: false. أستخدم عكازًا مثل هذا ، حيث أذهب إلى النموذج الأولي وأقوم بإعداد عدد لا يحصى: صحيح:

 function enumerateProto(obj) { Object.getOwnPropertyNames(obj.prototype) .forEach(prop => Object.defineProperty(obj.prototype, prop, {enumerable: true}) ) } 

آمل أن أتمكن من التخلص منه يومًا ما ، حيث يوجد حديث في مجتمع Emscripten حول تحسين الدعم لـ ES6.

ذاكرة الوصول العشوائي


الحديث عن C ++ ، لا يسع المرء إلا ذكر الذاكرة. عندما فحصنا كل شيء على فيديو بجودة SD ، كان كل شيء على ما يرام معنا ، لقد كان رائعًا! بمجرد إجراء اختبار FullHD ، كان هناك نقص في الذاكرة. لا يهم ، هناك خيار TOTAL_MEMORY ، الذي يحدد قيمة ذاكرة البدء للوحدة النمطية. لقد صنعنا نصف غيغا بايت ، كل شيء على ما يرام ، ولكن بطريقة ما لا إنسانية للمستخدمين ، لأننا نحتفظ بالذاكرة للجميع ، ولكن ليس لدى الجميع اشتراك في محتوى FullHD.

هناك خيار آخر - ALLOW_MEMORY_GROWTH. انها تسمح لك لتنمو الذاكرة
تدريجيا حسب الحاجة. يعمل مثل هذا: يمنح Emscripten افتراضيًا الوحدة 16 ميجابايت للتشغيل. عندما تستخدمها جميعًا ، يتم تخصيص قطعة ذاكرة جديدة. يتم نسخ جميع البيانات القديمة هناك ، ولا يزال لديك نفس القدر من المساحة للبيانات الجديدة. يحدث هذا حتى تصل إلى 4 جيجابايت.

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

حقن التبعية


يبدو أن هذا كان كل شيء ، ولكن بعد ذلك ذهب أشعل النار قليلاً. هناك مشكلة مع Dependency Injection. نكتب أبسط فئة التي هناك حاجة التبعية.

 class App { constructor(httpClient) { this.httpClient = httpClient } } 

على سبيل المثال ، نقوم بتمرير عميل HTTP الخاص بنا إلى تطبيقنا. نحن ننقذ في الممتلكات الصف. يبدو أن كل شيء سوف يعمل بشكل جيد.

 Module.App.extend( ″App″, new App(client) ) 

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

 class App { _construct(httpClient) { this.httpClient = httpClient this._parent._construct.call(this) } } 

نفعل الشيء نفسه تقريبًا: نقوم بتخزين التبعية في مجال الكائن ، ولكن هذا هو الكائن الذي ظهر بعد الميراث. يجب ألا ننسى إعادة توجيه استدعاء المنشئ إلى الكائن الأصل ، الموجود على جانب C ++. السطر الأخير هو تماثل طريقة super () في ES6. هذه هي الطريقة التي يحدث الميراث في هذه الحالة:

 const appConstr = Module.App.extend( ″App″, new App() ) const app = new appConstr(client) 

أولاً ، نرث ، ثم ننشئ كائنًا جديدًا تم تمرير التبعية إليه بالفعل ، وهذا يعمل.

خدعة المؤشر


مشكلة أخرى هي تمرير الكائنات عن طريق المؤشر من C ++ إلى JavaScript. لقد فعلنا بالفعل عميل HTTP. للبساطة ، لقد فاتنا واحد من التفاصيل الهامة.

 std::string get(std::string url) 

تقوم الطريقة بإرجاع القيمة فورًا ، أي أنه يجب أن يكون الطلب متزامنًا. لكن بعد كل شيء ، تطلب AJAX الحصول على AJAX وأنها غير متزامنة ، لذلك في الواقع الفعلي ، لن تُرجع الطريقة لا شيء ، أو يمكننا إرجاع معرف الطلب. ولكن لكي يكون هناك شخص ما لإرجاع الإجابة ، فإننا نجتاز المستمع كمعلمة ثانية ، حيث سيكون هناك عمليات رد اتصال من C ++.

 void get(std::string url, Listener listener) 

في JS ، يبدو كما يلي:

 function get(url, listener) { fetch(url).then(result) => { listener.onResult(result) }) } 

لدينا وظيفة الحصول على هذا الكائن المستمع. نبدأ تنزيل الملف وتعليق الاتصال. عند تنزيل الملف ، نقوم بسحب الوظيفة المطلوبة من المستمع وننقل النتيجة إليها.

يبدو أن الخطة جيدة ، ولكن عند اكتمال دالة get ، سيتم إتلاف كل المتغيرات المحلية ، ومعها معلمات الدالة ، أي ، سيتم إتلاف المؤشر ، وسوف يدمر emscripten وقت التشغيل الكائن على جانب C ++.

نتيجة لذلك ، عندما يتعلق الأمر باستدعاء الخط listener.onResult (نتيجة) ، لن يكون المستمع موجودًا ، وعند الوصول إليه ، سيحدث خطأ في الوصول إلى الذاكرة يؤدي إلى تعطل التطبيق.

أود تجنب ذلك ، وهناك حل ، لكن الأمر استغرق عدة أسابيع للعثور عليه.

 function get(url, listener) { const listenerCopy = listener.clone() fetch(url).then((result) => { listenerCopy.onResult(result) listenerCopy.delete() }) } 

اتضح أن هناك طريقة لاستنساخ المؤشر. لسبب ما ، لم يتم توثيقه ، لكنه يعمل بشكل جيد ، ويسمح لك بزيادة عدد المرجع في مؤشر Emscripten. يسمح لنا هذا بتعليقه في الختام ، وبعد ذلك ، عندما نطلق رد الاتصال ، سيكون المستمع الخاص بنا متاحًا بواسطة هذا المؤشر ويمكننا العمل حسب حاجتنا.

الشيء الأكثر أهمية هو عدم نسيان حذف هذا المؤشر ، وإلا فسيؤدي ذلك إلى حدوث خطأ في تسرب الذاكرة ، وهو أمر سيء للغاية.

الكتابة بسرعة إلى الذاكرة


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

 var newData = new Uint8Array(…); var size = newData.byteLength; var ptr = Module._malloc(size); var memory = new Uint8Array( Module.buffer, ptr, size ); memory.set(newData); 

newData هي بياناتنا كصفيف مكتوب. يمكننا أن نأخذ طوله ونطلب تخصيص ذاكرة بالحجم الذي نحتاجه من وحدة WebAssembly. تقوم دالة malloc بإرجاع مؤشر إلينا ، وهو مجرد فهرس للصفيف الذي يحتوي على كافة الذاكرة في WebAssembly. من جانب JavaScript ، يبدو وكأنه ArrayBuffer.

من خلال الخطوة التالية ، سنقطع نافذة في ArrayBuffer بالحجم الصحيح من مكان معين ونسخ بياناتنا هناك. على الرغم من حقيقة أن العملية المحددة لها دلالات نسخ ، عندما نظرت إلى هذا القسم في الملف التعريفي ، لم أر عملية طويلة. أعتقد أن المتصفح يعمل على تحسين هذه العملية بمساعدة دلالات النقل ، أي نقل ملكية الذاكرة من كائن إلى آخر.

وفي تطبيقنا ، نعتمد أيضًا على دلالات النقل لحفظ نسخ الذاكرة.

Adblock


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



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

الإنتاج


نتيجة لذلك ، دخلنا في الإنتاج. نعم ، لم يكن الأمر سهلاً ، استغرق الأمر 8 أشهر وأريد أن أسأل نفسي إذا كان الأمر يستحق ذلك. في رأيي - كان يستحق:

لا حاجة لتثبيت


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

قاعدة رمز موحدة وتصحيح الأخطاء على منصات مختلفة


في الوقت نفسه ، تمكنا من الحفاظ على قاعدة الشفرة الفردية الخاصة بنا. يمكننا تحريف نفس الكود على منصات مختلفة ، وقد حدث مرارًا وتكرارًا ظهور الأخطاء التي كانت غير مرئية على أحد الأنظمة الأساسية على الأخرى. وبالتالي ، يمكننا اكتشاف الأخطاء الخفية باستخدام أدوات مختلفة على منصات مختلفة.

الافراج السريع


لقد حصلنا على إصدار سريع ، حيث يمكن إصدارنا كتطبيق ويب بسيط وتحديث رمز C ++ مع كل إصدار جديد. إنه لا يقارن بكيفية إصدار مكونات إضافية جديدة أو تطبيق جوال أو تطبيق SmartTV. يعتمد الإصدار علينا فقط: عندما نريد ، فسيتم إصداره.

ردود فعل سريعة


وهذا يعني ردود الفعل السريعة: إذا حدث خطأ ما ، فيمكننا أن نكتشف خلال اليوم وجود مشكلة والرد عليها.

أعتقد أن كل هذه المشاكل كانت تستحق هذه المزايا. ليس كل شخص لديه تطبيق C ++ ، ولكن إذا كان لديك تطبيق وتريد أن يكون في متصفحك - WebAssembly هو حالة استخدام بنسبة 100 ٪ بالنسبة لك.

مكان التقديم


ليس كل شخص يكتب في C ++. ولكن ليس فقط C ++ متاح لـ WebAssembly. نعم ، هذا هو تاريخيا أول منصة لا تزال متاحة في asm.js ، وهي تكنولوجيا موزيلا في وقت مبكر. بالمناسبة ، لذلك ، لديها أدوات جيدة جدا ، كما هم كبار السن من التكنولوجيا نفسها.

الصدأ


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

Lua ، Perl ، Python ، PHP ، إلخ.


تتوفر جميع اللغات التي يتم ترجمتها تقريبًا في WebAssembly ، نظرًا لأن المترجمين الشفويين مكتوبون في لغة C ++ ، فقد تم تجميعها ببساطة في WebAssembly والآن يمكنك تحريف PHP في مستعرض.

اذهب


في الإصدار 1.11 ، قاموا بإنشاء إصدار تجريبي من التحويل البرمجي في WebAssembly ، وفي الإصدار 2.0 يعدون بدعم الإصدار. ظهر دعمهم لاحقًا ، لأن WebAssembly لا يدعم أداة تجميع مجمعي البيانات المهملة ، و Go هي لغة ذاكرة مُدارة. لذلك كان عليهم سحب أداة تجميع مجمعي البيانات المهملة الخاصة بهم ضمن WebAssembly.

كوتلين / الأم


عن نفس القصة مع Kotlin. المترجم لديه دعم تجريبي ، ولكن سيكون عليهم أيضًا القيام بشيء مع جامع البيانات المهملة. لا أعرف الوضع الحالي.

3D-


? , — 3D-. , , asm.js WebAssembly . , WebAssembly.




, : , , . , .





. , , , , . , , ; — .



, Google Chrome, , WebAssembly-. npm- , Wasm, JS. , ++ - — .

HunSpell — Wasm .


— « ». , - , — OpenSSL. WebAssembly. OpenSSL — , , .


use case wotinspector.com. World of Tanks. , , , , , .

— . , , . , , - ++, WebAssembly, ( , ).

. , , . . , , , , . . .


, , ++. , FFmpeg, . , ffmpeg. . , , , , .



— . OpenCV — , WebAssembly, . PDF. SQLite, SQL. SQLite WebAssembly Emscripten, .

Node.js





WebAssembly, Node.js. , Sass — css. Ruby, ++ ( libsass). , Webpack', Node.js. node-sass , JS- .

, , . . :



, node-sass 100 . , ( ) . WebAssembly : , WebAssembly .

Node. , WebAssembly libsass-asm . , . WebAssembly …


Figma — web-. - Sketch, , . ++ ( ), asm.js. , .



WebAssembly, , 3 . , .

Visual Studio Code, , Electron, , , Node-sass. , Node, . , , , WebAssembly.





— AutoCAD. 30 , ++, . , , - JavaScript, , . WebAssembly AutoCAD - , 5 .

, , , , , , , , . FFMpeg — , — QEMU. , , KVM, .



2011 QEMU . , . , Linux , Linux-, , - .

, . bash, , Linux. — GUI . . , , …



, , - . Windows 2000 , , 18 , . , Chrome ( FireFox).

, WebAssembly , , , , .


, WebAssembly. , — , . — , .



, C++ web-. , , — . — , , , .

, . , C++, JavaScript, . , C++. , JS C++, .

— .



CI Pipeline


? JS- , Webpack. , , ( ), JS. webpack watch, , .




, . , , .

Chrome DevTools, Sources wasm-. ( - ), , , .



, , : «, , , , , !». , embedded-, , - .

: -g4 wast- , .



, 100 ( FAR). — , Chrome. E:/_work/bfg/bytefrog/… — . , ++ . , SourceMap!

SourceMap


, .
  • Firefox.
  • --sourcemap-base=http://localhost , SourceMap -, .
  • HTTP.
  • .
  • Windows «:» . .


. CMake , URL -. : wast- , . , .

, :



++ . ! , , stack trace, . , wasm- stack trace, , , , , .



, — SourceMap . , , . , .



«var0».



, . , SourceMap, , .


. Chrome, Firefox. Firefox — «» , , .



Chrome ( , , Mangled ), , , , .




. , :

  • . runtime, . ++ Rust Go.
  • JS — Wasm. , JS Wasm. -, , . , .
  • . , , , .
  • Wasm . Wasm , JS. WebAssembly , .
  • JS.


: .

  • wasp_cpp_bench
  • Chrome 65.0.3325.181 (64-bit)
  • Core i5-4690
  • 24gb ram
  • 5 ; max min;


. JS — , .



++, , - . Grayscale. C++ , . ( ), , JS. , , , ++, .


Sentry, — wasm. , traceKit, Sentry — Raven, — , , wasm . , , , pull request, npm install JS-.



. production, , . debug-, , :




  • WebAssembly , .
  • — . 8 , C++, , .
  • , , WebAssembly — .
  • — JS. JS- , «» , , .


, :
  • Emscripten Embind. .
  • - Emscripten — . , , 3000 Emscripten.
  • Sentry.
  • Firefox.


شكرا لاهتمامكم! .



HolyJS, : 24-25 HolyJS . (, Node.js Ryan Dahl!), — 1 .

Source: https://habr.com/ru/post/ar441140/


All Articles