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

في أغلب الحالات، تكون نقطة الدخول إلى مشروع الويب هي نقطة npm package.json وفقًا للمؤتمر، يمكن إنشاء معظم المشاريع باستخدام "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 دمج.
  • سيُلغي -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++ عمليات نظام الملفات. ويقوم ببعض التحليل للتعليمة البرمجية التي يقوم بتجميعها لتحديد ما إذا كان ينبغي تضمين نظام الملفات في الرمز الملتصق أم لا. ومع ذلك، في بعض الأحيان، فقد يخطئ التحليل في حساب قيمة كبيرة، وبالتالي تدفع 70 كيلوبايت لمحاكاة نظام الملفات، والتي قد لا تحتاج إليها. باستخدام -s FILESYSTEM=0، يمكنك فرض عدم تضمين هذا الرمز في Emscripten.
  • سيجعل -g4 Emscripten يتضمن معلومات تصحيح الأخطاء في .wasm إرسال ملف خرائط مصدر لوحدة Wasm. يمكنك قراءة المزيد على تصحيح الأخطاء باستخدام Emscripten في عملية تصحيح الأخطاء .

وهذا كل ما في الأمر! لاختبار هذا الإعداد، لنحضّر جهاز 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 الإخراج التالي في وحدة تحكّم أدوات مطوّري البرامج:

أدوات مطوّري البرامج تعرض رسالة مطبوعة باستخدام لغة C++ وEmscripten

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

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

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

DevTools
تعرض نسخة 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. دوكسجين المتاحة في حاوية Docker، ولكن يمكنك تثبيتها باستخدام apt.

إذا نفّذت هذا الإجراء في build.sh، عليك إعادة تنزيله ثم إعادة تثبيته. دوكسجين في كل مرة تريد فيها بناء مكتبتك. لن يكون ذلك فحسب مهدر، ولكنه سيمنعك أيضًا من العمل في مشروعك أثناء عدم الاتصال بالإنترنت.

من المنطقي هنا إنشاء صورة 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 لتنفيذ أوامر واجهة الأوامر ضمن . مهما كانت التغييرات التي تجريها هذه الأوامر على الحاوية هي الآن جزء من صورة 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 ملفات Dockerfiles خطوة بخطوة وتعين نتيجة كل خطوة صورة خاصة به. هذه الصور الوسيطة تسمى "الطبقات". إذا لم يتغير أمر في ملف Dockerfile، فإن Docker لن يعيد تشغيل هذه الخطوة في الواقع عند إعادة إنشاء الملف الشامل. بدلاً من ذلك يعيد استخدام الطبقة التي تم إنشاؤها من آخر مرة تم إنشاء الصورة فيها.

في السابق، كان عليك بذل بعض الجهود لعدم إعادة إنشاء 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 بعد الآن.