كيف يتم دمج WebAssembly في هذا الإعداد؟ في هذه المقالة، سنوضّح ذلك باستخدام C/C++ وEmscripten كمثال.
غالبًا ما يتم استخدام WebAssembly (wasm) كطريقة لتحسين الأداء أو لتشغيل قاعدة بيانات رمز C++ الحالية على الويب. من خلال squoosh.app، أردنا توضيح أنّ هناك منظورًا ثالثًا على الأقل لواسم: الاستفادة من الأنظمة المتكاملة الضخمة الخاصة بلغات البرمجة الأخرى. باستخدام Emscripten، يمكنك استخدام رمز C/C++، ويحتوي Rust على دعم wasm مضمّن، ويعمل فريق Go على ذلك أيضًا. ونؤكّد أنّه ستتوفّر قريبًا بلغات أخرى.
في هذه السيناريوهات، لا يشكّل ملف wasm العنصر الرئيسي في تطبيقك، بل هو قطعة من أحجية: وحدة أخرى. يتضمّن تطبيقك حاليًا JavaScript وCSS ومواد عرض الصور و نظام إنشاء يركّز على الويب، وقد يتضمّن أيضًا إطار عمل مثل React. كيف تتم دمج WebAssembly في هذا الإعداد؟ في هذه المقالة، سنوضِّح كيفية تنفيذ ذلك باستخدام C/C++ وEmscripten كمثال.
Docker
لقد تبيّن لي أنّ Docker لا يقدّر بثمن عند العمل مع Emscripten. غالبًا ما يتم كتابة مكتبات C/C++ لتعمل مع نظام التشغيل الذي تم إنشاؤها عليه. من المفيد جدًا أن يكون لديك بيئة متّسقة. باستخدام Docker، يمكنك الحصول على نظام Linux افتراضي تم إعداده مسبقًا للعمل مع Emscripten وتم تثبيت كل الأدوات والتبعيات فيه. إذا كان هناك عنصر غير متوفّر، يمكنك تثبيته بدون القلق بشأن تأثيره في جهازك أو مشاريعك الأخرى. إذا حدث خطأ، تخلص من الحاوية وابدأ مجددًا. إذا نجح ذلك مرة واحدة، يمكنك التأكّد من أنّه سيستمر في العمل ويؤدي إلى نتائج متطابقة.
يتضمّن Docker Registry صورة Emscripten من تطوير trzeci والتي أستخدمها على نطاق واسع.
الدمج مع npm
في معظم الحالات، تكون نقطة الدخول إلى مشروع الويب هي
package.json
في npm. وفقًا للعرف، يمكن إنشاء معظم المشاريع باستخدام npm install &&
npm run build
.
بوجه عام، يجب التعامل مع عناصر التصميم التي ينشئها Emscripten (ملفان .js
و.wasm
) على أنّهما مجرد وحدة JavaScript وملف موارد
آخر. يمكن التعامل مع ملف JavaScript باستخدام أداة تجميع مثل webpack أو rollup،
ويجب التعامل مع ملف wasm مثل أي مادة عرض ثنائية أكبر أخرى، مثل
الصور.
وبالتالي، يجب إنشاء عناصر إنشاء Emscripten قبل بدء عملية الإنشاء "العادية":
{
"name": "my-worldchanging-project",
"scripts": {
"build:emscripten": "docker run --rm -v $(pwd):/src trzeci/emscripten
./build.sh",
"build:app": "<the old build command>",
"build": "npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
يمكن أن تستدعي مهمة build:emscripten
الجديدة Emscripten مباشرةً، ولكن كما ذكرنا سابقًا، ننصح باستخدام Docker للتأكّد من اتساق بيئة الإنشاء.
يطلب الأمر docker run ... trzeci/emscripten ./build.sh
من Docker إنشاء حاوية
جديدة باستخدام صورة trzeci/emscripten
وتنفيذ الأمر ./build.sh
.
build.sh
هو نص برمجي لنظام التشغيل الذي ستكتبه بعد ذلك. يطلب الخيار --rm
من Docker حذف الحاوية بعد انتهاء تشغيلها. بهذه الطريقة، لن تُنشئ
مجموعة من صور الأجهزة القديمة بمرور الوقت. يعني الخيار -v $(pwd):/src
أنّه
عليك أن تطلب من Docker "مطابقة" الدليل الحالي ($(pwd)
) مع /src
داخل
الحاوية. ستتم مزامنة أي تغييرات تجريها على الملفات في الدليل /src
داخل
الحاوية مع مشروعك الفعلي. تُعرف هذه الأدلة المطابقة
باسم "عمليات ربط التثبيت".
لنلقِ نظرة على build.sh
:
#!/bin/bash
set -e
export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CXXFLAGS="${OPTIMIZE}"
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
src/my-module.cpp
# Create output folder
mkdir -p dist
# Move artifacts
mv my-module.{js,wasm} dist
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="
هناك الكثير من النقاط التي يجب تحليلها هنا.
تؤدي set -e
إلى وضع Shell في وضع "توقّف سريع". إذا أدّت أي أوامر في النص البرمجي
إلى ظهور خطأ، يتم إيقاف النص البرمجي بالكامل على الفور. يمكن أن يكون ذلك مفعّلاً بشكلٍ هائل، لأنّ الإخراج الأخير للنص البرمجي سيكون دائمًا رسالة إشارة إلى نجاح الإنشاء أو الخطأ الذي أدّى إلى تعذّر عملية الإنشاء.
باستخدام عبارات export
، يمكنك تحديد قيم متغيرَين من متغيّرات
البيئة. تتيح لك هذه الأدوات تمرير مَعلمات سطر أوامر إضافية إلى مجمع لغة C
(CFLAGS
) ومجمع لغة C++ (CXXFLAGS
) ورابط LDFLAGS
.
وتتلقّى جميعها إعدادات المحسِّن من خلال OPTIMIZE
للتأكّد من أنّه تتم تحسين كل العناصر بالطريقة نفسها. هناك قيمتان محتملتان
للمتغيّر OPTIMIZE
:
-O0
: لا تجرِ أي تحسينات. ولا يتمّ إزالة أيّ رموز برمجية غير مفيدة، ولا تُصغّر Emscripten رمز JavaScript الذي تنشئه أيضًا. مناسبة لتصحيح الأخطاء-O3
: تحسين الأداء بشكلٍ فعّال-Os
: تحسين الأداء والحجم بشكلٍ مكثّف كأحد المعايير الثانوية-Oz
: تحسين الحجم بشكل كبير، مع التضحية بالأداء إذا لزم الأمر
بالنسبة إلى الويب، أنصح باستخدام -Os
في أغلب الأحيان.
يتضمّن الأمر emcc
العديد من الخيارات الخاصة به. يُرجى العِلم أنّه من المفترض أن يكون emcc "بديلاً فوريًا للمنشِئين مثل GCC أو clang". وبالتالي، من المرجّح أن تُنفِّذ emcc أيضًا كل العلامات التي قد تعرفها من GCC. إنّ العلامة -s
خاصة لأنها تسمح لنا بضبط Emscripten
على وجه التحديد. يمكن العثور على جميع الخيارات المتاحة في ملف Emscripten
settings.js
،
ولكن قد يكون هذا الملف مربكًا للغاية. في ما يلي قائمة بعلامات Emscripten
التي أعتقد أنّها الأكثر أهمية لمطوّري الويب:
--bind
تفعِّل embind.-s STRICT=1
لن تتوفّر جميع خيارات الإصدار المتوقّفة نهائيًا. يضمن ذلك إنشاء الرمز بطريقة متوافقة مع الإصدارات المستقبلية.- يسمح
-s ALLOW_MEMORY_GROWTH=1
بزيادة الذاكرة تلقائيًا إذا لزم الأمر. في وقت كتابة هذه المقالة، سيخصّص Emscripten 16 ميغابايت من الذاكرة في البداية. عندما يخصّص الرمز البرمجي أجزاء من الذاكرة، يحدّد هذا الخيار ما إذا كانت هذه العمليات ستؤدي إلى تعذُّر تشغيل وحدة wasm بالكامل عند استنفاد الذاكرة، أو ما إذا كان يُسمح لرمز التجميع بتوسيع إجمالي الذاكرة ليتلاءم مع عملية التخصيص. - يختار
-s MALLOC=...
طريقة تنفيذmalloc()
التي سيتم استخدامها. emmalloc
هو تنفيذ صغير وسريع لـmalloc()
مخصّص لخدمة Emscripten. البديل هوdlmalloc
، وهو تنفيذ كاملmalloc()
. ليس عليك سوى التبديل إلىdlmalloc
إذا كنت تخصص عددًا كبيرًا من العناصر الصغيرة بشكل متكرّر أو إذا كنت تريد استخدام ميزة "المعالجة المتعدّدة". - سيحوّل
-s EXPORT_ES6=1
رمز JavaScript إلى وحدة ES6 مع تصدير تلقائي يعمل مع أي أداة تجميع. يتطلب ذلك أيضًا ضبط-s MODULARIZE=1
.
لا تكون العلامات التالية ضرورية دائمًا أو تكون مفيدة لأغراض تصحيح الأخطاء فقط:
-s FILESYSTEM=0
هو علامة مرتبطة بـ Emscripten وقدرتها على محاكاة نظام ملفات نيابةً عنك عندما يستخدم رمز C/C++ عمليات نظام الملفات. ويعمل على إجراء بعض التحليلات على الرمز البرمجي الذي يُجمِّعه لتحديد ما إذا كان سيتم تضمين emulatior لنظام الملفات في رمز glue أم لا. في بعض الأحيان، يمكن أن يخطئ هذا التحليل، ما يؤدي إلى تحميل 70 كيلوبايت من رمز إضافي لربط لمحاكاة نظام ملفات قد لا تحتاج إليها. باستخدام-s FILESYSTEM=0
، يمكنك إجبار Emscripten على عدم تضمين هذا الرمز.- سيؤدي
-g4
إلى تضمين Emscripten لمعلومات تصحيح الأخطاء في.wasm
و أيضًا إلى إنشاء ملف خرائط مصادر لمكوّن wasm. يمكنك الاطّلاع على مزيد من المعلومات حول debugging باستخدام Emscripten في قسم debugging.
ها أنت ذا. لاختبار هذا الإعداد، لننشئ my-module.cpp
صغيرًا:
#include <emscripten/bind.h>
using namespace emscripten;
int say_hello() {
printf("Hello from your wasm module\n");
return 0;
}
EMSCRIPTEN_BINDINGS(my_module) {
function("sayHello", &say_hello);
}
وindex.html
:
<!doctype html>
<title>Emscripten + npm example</title>
Open the console to see the output from the wasm module.
<script type="module">
import wasmModule from "./my-module.js";
const instance = wasmModule({
onRuntimeInitialized() {
instance.sayHello();
}
});
</script>
(إليك ملخّص يحتوي على جميع الملفات).
لإنشاء كل العناصر، يمكنك تنفيذ
$ npm install
$ npm run build
$ npm run serve
من المفترض أن يؤدي الانتقال إلى localhost:8080 إلى عرض الإخراج التالي فيconsole DevTools:
إضافة رمز C/C++ كعنصر تابع
إذا كنت تريد إنشاء مكتبة C/C++ لتطبيقك على الويب، يجب أن يكون رمزها البرمجي
جزءًا من مشروعك. يمكنك إضافة الرمز إلى مستودع مشروعك يدويًا
أو يمكنك استخدام npm لإدارة هذا النوع من التبعيات أيضًا. لنفترض أنّني
أريد استخدام libvpx في تطبيق الويب الخاص بي. libvpx
هي مكتبة C++ لتشفير الصور باستخدام VP8، وهو برنامج الترميز المستخدَم في ملفات .webm
.
ومع ذلك، لا يتوفّر libvpx في npm ولا يتضمّن package.json
، لذا لا يمكنني
تثبيته باستخدام npm مباشرةً.
لحلّ هذه المشكلة، يمكنك استخدام مكتبة napa التي تتيح لك تثبيت أي عنوان URL لمستودع git
ككود تكميلي في مجلد node_modules
.
ثبِّت napa كتبعية:
$ npm install --save napa
وتأكَّد من تشغيل napa
كنص برمجي للتثبيت:
{
// ...
"scripts": {
"install": "napa",
// ...
},
"napa": {
"libvpx": "git+https://github.com/webmproject/libvpx"
}
// ...
}
عند تشغيل npm install
، يهتمّ napa باستنساخ مستودع libvpx GitHub
في node_modules
الخاص بك باسم libvpx
.
يمكنك الآن توسيع نطاق البرنامج النصي للإنشاء من أجل إنشاء libvpx. يستخدم libvpx configure
وmake
لإنشاءه. لحسن الحظ، يمكن أن يساعد Emscripten في ضمان استخدام configure
و
make
لمجمِّع Emscripten. لهذا الغرض، تتوفّر الأمران
emconfigure
وemmake
:
# ... above is unchanged ...
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
(
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
# ... below is unchanged ...
يتم تقسيم مكتبة C/C++ إلى جزأين: الرؤوس (عادةً ملفات .h
أو
.hpp
) التي تحدِّد بنى البيانات والفئات والثوابت وما إلى ذلك التي تعرضها مكتبة
والمكتبة الفعلية (عادةً ملفات .so
أو .a
). لاستخدام الثابت VPX_CODEC_ABI_VERSION
في المكتبة في الرمز البرمجي، عليك تضمين ملفات الرأس الخاصة بالمكتبة باستخدام عبارة #include
:
#include "vpxenc.h"
#include <emscripten/bind.h>
int say_hello() {
printf("Hello from your wasm module with libvpx %d\n", VPX_CODEC_ABI_VERSION);
return 0;
}
المشكلة هي أنّ المُجمِّع لا يعرف أين يبحث عن vpxenc.h
.
هذا هو الغرض من علامة -I
. ويُخبر المُجمِّع بالأدلة التي يجب
البحث فيها عن ملفات الرأس. بالإضافة إلى ذلك، عليك أيضًا منح المُجمِّع
ملف المكتبة الفعلي:
# ... above is unchanged ...
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
# Compile C/C++ code
emcc \
${OPTIMIZE} \
--bind \
-s STRICT=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s ASSERTIONS=0 \
-s MALLOC=emmalloc \
-s MODULARIZE=1 \
-s EXPORT_ES6=1 \
-o ./my-module.js \
-I ./node_modules/libvpx \
src/my-module.cpp \
build-vpx/libvpx.a
# ... below is unchanged ...
إذا شغّلت npm run build
الآن، ستلاحظ أنّ العملية تُنشئ ملف .js
جديدًا وملف .wasm
جديدًا وأنّ الصفحة التجريبية ستُخرج الثابت:
ستلاحظ أيضًا أنّ عملية الإنشاء تستغرق وقتًا طويلاً. يمكن أن يختلف سبب مدّة الإنشاء الطويلة. في ما يتعلّق بمكتبة libvpx، يستغرق الأمر وقتًا طويلاً لأنّه يتم تجميع برنامج ترميز وفك ترميز لكل من VP8 وVP9 في كل مرة يتم فيها تنفيذ أمر الإنشاء، حتى لو لم تتغيّر الملفات المصدر. سيستغرق إنشاء أي
تغيير صغير في my-module.cpp
وقتًا طويلاً. سيكون من المفيد جدًا
الاحتفاظ بعناصر إنشاء libvpx بعد أن يتم
إنشاؤها للمرة الأولى.
وإحدى طرق تحقيق ذلك هي استخدام متغيّرات البيئة.
# ... above is unchanged ...
eval $@
echo "============================================="
echo "Compiling libvpx"
echo "============================================="
test -n "$SKIP_LIBVPX" || (
rm -rf build-vpx || true
mkdir build-vpx
cd build-vpx
emconfigure ../node_modules/libvpx/configure \
--target=generic-gnu
emmake make
)
echo "============================================="
echo "Compiling libvpx done"
echo "============================================="
# ... below is unchanged ...
(إليك ملخّص يحتوي على جميع الملفات).
يسمح لنا الأمر eval
بضبط متغيّرات البيئة من خلال تمرير المَعلمات
إلى نصّ البناء. سيتخطّى الأمر test
إنشاء libvpx إذا تم ضبط
$SKIP_LIBVPX
(على أي قيمة).
يمكنك الآن تجميع المكوّن الإضافي ولكن تخطّي إعادة إنشاء libvpx:
$ npm run build:emscripten -- SKIP_LIBVPX=1
تخصيص بيئة الإصدار
تعتمد المكتبات أحيانًا على أدوات إضافية لعملية الإنشاء. إذا كانت هذه التبعيات
غير متوفّرة في بيئة الإنشاء التي تقدّمها صورة Docker، عليك
إضافتها بنفسك. على سبيل المثال، لنفترض أنّك تريد أيضًا إنشاء
مستندات libvpx باستخدام doxygen. لا يتوفر Doxygen
داخل حاوية Docker، ولكن يمكنك تثبيته باستخدام apt
.
إذا أردت إجراء ذلك في build.sh
، عليك إعادة تنزيل وإعادة تثبيت
doxygen في كل مرة تريد فيها إنشاء مكتبتك. ولن يؤدي ذلك فقط إلى
إهدار المساحة، بل سيمنعك أيضًا من العمل على مشروعك أثناء عدم الاتصال بالإنترنت.
في هذه الحالة، من المنطقي إنشاء صورة Docker الخاصة بك. يتم إنشاء صور Docker من خلال
كتابة Dockerfile
يصف خطوات الإنشاء. إنّ ملفات Dockerfiles مفيدة جدًا وتحتوي على العديد من الطلبات، ولكن في معظم الأوقات، يمكنك استخدام FROM
وRUN
وADD
فقط. في هذه الحالة:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen
باستخدام FROM
، يمكنك تحديد صورة Docker التي تريد استخدامها كنقطة
بداية. اخترتُ trzeci/emscripten
كأساس، وهي الصورة التي كنت تستخدمها
طوال الوقت. باستخدام RUN
، يمكنك توجيه Docker لتشغيل أوامر shell داخل
الحاوية. إنّ أي تغييرات تجريها هذه الأوامر على الحاوية أصبحت الآن جزءًا من
صورة Docker. للتأكّد من أنّه تم إنشاء صورة Docker وأنّها
متوفّرة قبل تشغيل build.sh
، عليك تعديل package.json
قليلاً:
{
// ...
"scripts": {
"build:dockerimage": "docker image inspect -f '.' mydockerimage || docker build -t mydockerimage .",
"build:emscripten": "docker run --rm -v $(pwd):/src mydockerimage ./build.sh",
"build": "npm run build:dockerimage && npm run build:emscripten && npm run build:app",
// ...
},
// ...
}
(إليك ملخّص يحتوي على جميع الملفات).
سيؤدي ذلك إلى إنشاء صورة Docker، ولكن فقط إذا لم يتم إنشاؤها بعد. بعد ذلك، يتم تنفيذ كل شيء كما كان من قبل، ولكن تتوفّر الآن في بيئة الإنشاء doxygen
الأمر الذي سيؤدي إلى إنشاء مستندات libvpx أيضًا.
الخاتمة
ليس من المستغرب أنّ رمز C/C++ وnpm غير متوافقَين بشكلٍ طبيعي، ولكن يمكنك جعله يعمل بشكلٍ مريح للغاية باستخدام بعض الأدوات الإضافية والعزل الذي يوفّره Docker. قد لا يكون هذا الإعداد مناسبًا لكل مشروع، ولكنه يشكّل نقطة بداية جيدة يمكنك تعديلها لتلبية احتياجاتك. إذا كانت لديك اقتراحات بشأن تحسينات، يُرجى مشاركتها معنا.
الملحق: الاستفادة من طبقات صور Docker
ويمكنك استخدام حل بديل يتمثل في تجميع المزيد من هذه المشاكل باستخدام Docker و النهج الذكي الذي يتّبعه Docker في التخزين المؤقت. تنفِّذ أداة Docker ملفات Dockerfile خطوة بخطوة، ومنحِي نتيجة كل خطوة صورة خاصة بها. غالبًا ما تُعرف هذه الصور الوسيطة باسم "الطبقات". إذا لم يتغيّر أحد الأوامر في ملف Dockerfile، لن يُعيد Docker تشغيل هذه الخطوة عند إعادة إنشاء ملف Dockerfile. بدلاً من ذلك، تتم إعادة استخدام الطبقة من آخر مرة تم فيها إنشاء الصورة.
في السابق، كان عليك بذل بعض الجهد لتجنُّب إعادة إنشاء libvpx في كل مرة
تُنشئ فيها تطبيقك. بدلاً من ذلك، يمكنك نقل تعليمات إنشاء libvpx
من build.sh
إلى Dockerfile
للاستفادة من آلية التخزين المؤقت في Docker:
FROM trzeci/emscripten
RUN apt-get update && \
apt-get install -qqy doxygen git && \
mkdir -p /opt/libvpx/build && \
git clone https://github.com/webmproject/libvpx /opt/libvpx/src
RUN cd /opt/libvpx/build && \
emconfigure ../src/configure --target=generic-gnu && \
emmake make
(إليك ملخّص يحتوي على جميع الملفات).
يُرجى العِلم أنّك بحاجة إلى تثبيت git يدويًا واستنساخ libvpx لأنّه لا يتوفّر لديك
عمليات ربط عمليات التثبيت عند تشغيل docker build
. ونتيجةً لذلك، لن يكون عليك استخدام
napa بعد الآن.