Emscripten وnpm

كيف يتم دمج WebAssembly في هذا الإعداد؟ في هذه المقالة، سنتطرق إلى ذلك باستخدام C/C++ وEmscripten كمثال.

غالبًا ما يتم تأطير WebAssembly (wasm) على أنه أساسي للأداء أو طريقة لتشغيل قاعدة أكواد C++ الحالية على الويب. من خلال تطبيق squoosh.app، أردنا توضيح أنّ هناك منظورًا ثالثًا على الأقل لـ Wasm، وهو الاستفادة من الأنظمة المتكاملة الضخمة للغات البرمجة الأخرى. من خلال 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 أو ملف مجمّعة، ويجب التعامل مع ملف 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 على تفعيل وضع "الإخفاق بسرعة". إذا أظهرت أي أوامر في النص البرمجي خطأً، سيتم إلغاء البرنامج النصي بأكمله على الفور. يمكن أن يكون هذا مفيدًا للغاية حيث سيكون الإخراج الأخير من النص البرمجي دائمًا رسالة نجاح أو الخطأ الذي تسبب في إخفاق عملية الإنشاء.

باستخدام عبارات export، يمكنك تحديد قيم اثنين من متغيرات البيئة. وتسمح لك هذه الإعدادات بتمرير معلَمات سطر أوامر إضافية إلى محوّل C++ (CFLAGS) والمحول البرمجي لـ C++ (CXXFLAGS) وأداة الربط (LDFLAGS). وتتلقّى جميعها إعدادات أداة تحسين الأداء من خلال OPTIMIZE للتأكّد من تحسين جميع العناصر بالطريقة نفسها. هناك قيمتان محتملتان للمتغيّر OPTIMIZE:

  • -O0: لا تجرِ أي تحسين. ولا يتمّ إزالة أيّ رموز برمجية غير مفيدة، ولا تُصغّر Emscripten رمز JavaScript الذي تنشئه أيضًا. جيد لتصحيح الأخطاء.
  • -O3: تُجري تحسينًا كبيرًا لتحقيق الأداء.
  • -Os: يمكنك تحسين الأداء والحجم بشكل كبير كمعيار ثانوي.
  • -Oz: تحسين الحجم بشكل كبير، مع التضحية بالأداء إذا لزم الأمر

على الويب، أنصحك في الغالب باستخدام -Os.

يتضمّن الأمر emcc العديد من الخيارات الخاصة به. يُرجى العِلم أنّ emcc هو من المفترض أن يكون "بديلاً فوريًا للمنشِئين مثل GCC أو clang". ولذلك، من المرجح أن يتم تنفيذ جميع العلامات التي قد تعرفها من GCC بواسطة emcc أيضًا. تُعدّ العلامة -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++ وEmscripten

إضافة رمز C/C++ كتبعية

إذا كنت ترغب في إنشاء مكتبة C/C++ لتطبيق الويب الخاص بك، فأنت بحاجة إلى أن تكون التعليمات البرمجية الخاصة بها جزءًا من مشروعك. يمكنك إضافة الكود إلى مستودع مشروعك يدويًا أو يمكنك استخدام npm لإدارة هذا النوع من التبعيات أيضًا. لنفترض أنّني أريد استخدام libvpx في تطبيق الويب الخاص بي. libvpx هي مكتبة C++ لتشفير الصور باستخدام VP8، وهو برنامج الترميز المستخدَم في ملفات .webm. ومع ذلك، لا تعمل libvpx على npm ولا تحتوي على package.json، لذا لا يمكنني تثبيتها باستخدام npm مباشرةً.

للتخلّص من هذه اللغز، هناك napa. يسمح لك 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 جديدًا وأنّ الصفحة التجريبية ستُخرج الثابت:

أدوات مطوري البرامج تعرض نسخة ABI من libvpx المطبوع عبر الوصف

ستلاحظ أيضًا أن عملية التصميم تستغرق وقتًا طويلاً. يمكن أن يختلف سبب مدّة الإنشاء الطويلة. في حالة 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 يصف خطوات التصميم. تُعد ملفات Dockerfile قوية جدًا وتتضمن الكثير من الأوامر، ولكن يمكنك في معظم الأحيان الهروب من خلال استخدام 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 وclone libvpx يدويًا لعدم توفّر عمليات تثبيت ربط عند تشغيل docker build. نتيجةً لذلك، لن يكون عليك استخدام napa بعد الآن.