Emscripten ו-npm

איך משלבים את WebAssembly בהגדרה הזו? במאמר הזה נבחן את הנושא בעזרת C/C++ ו-Emscripten כדוגמה.

לרוב, הכלי WebAssembly (wasm) שממוסגר בתור רכיב ביצועים או כדרך לנהל את באינטרנט. רצינו להראות ל-squoosh.app שיש לפחות נקודת מבט שלישית עבור Wam: שימוש המערכות האקולוגיות של שפות תכנות אחרות. ב- Emscripten, אפשר להשתמש בקוד C/C++ חלודה כוללת תמיכה מובנית ב-Wasm, וכמו כן Go וגם הצוות עובדים על זה. אני הרבה שפות אחרות יגיעו אחריה.

בתרחישים האלה, Wam הוא לא החלק המרכזי באפליקציה, אלא חידה חלק: עוד מודול. לאפליקציה שלך כבר יש JavaScript, CSS, נכסי תמונות, מערכת build ממוקדת-אינטרנט ואולי אפילו framework כמו React. איך עושים את זה לשלב את WebAssembly בהגדרה הזו? במאמר הזה נלמד בעזרת C/C++ ו-Emscripten.

Docker

ל-Docker יש תפקיד חשוב בעבודה עם Emscripten. C/C++ ספריות נכתבות לעיתים קרובות כדי לעבוד יחד עם מערכת ההפעלה שעליה הן מבוססות. יצירת סביבה עקבית עוזרת מאוד. ב-Docker מקבלים במערכת Linux וירטואלית שכבר מוגדרת לעבודה עם Emscripten את כל הכלים ויחסי התלות שהותקנו. אם משהו חסר, אפשר פשוט להתקין אותה בלי לדאוג לגבי ההשפעה שלה על המחשב שלכם או פרויקטים אחרים. אם משהו משתבש, יש להשליך את המכל ולהתחיל אותו מעל. אם הוא פועל פעם אחת, בטוח שהוא ימשיך לפעול לא תשיגו תוצאות זהות.

ל-Docker Registry יש Emscripten תמונה מאת trzeci שמשמש אותי לעיתים קרובות.

שילוב עם NPM

ברוב המקרים, נקודת הכניסה לפרויקט באינטרנט היא package.json לפי המוסכמה, אפשר ליצור את רוב הפרויקטים באמצעות npm install && npm run build.

באופן כללי, פריטי ה-build שנוצרו על ידי Emscripten (.js ו-.wasm) ) ויש להתייחס אליו כאל מודול JavaScript נוסף נכס. ניתן לטפל בקובץ ה-JavaScript באמצעות Bundler כמו webpack או רול-על, וצריך להתייחס לקובץ ה-Wasm כמו לכל נכס בינארי גדול יותר, כמו תמונות.

לכן, יש לבנות את פריטי המידע שנוצרו בתהליך הפיתוח (Artifact) של Emscripten לפני הפורמט ה"רגיל" תהליך ה-build כולל:

{
    "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 כדי לוודא שסביבת ה-build עקביים.

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 מוסיף את המעטפת ל-"failfast" במצב תצוגה. אם יש פקודות בסקריפט יחזיר שגיאה, הסקריפט כולו יבוטל באופן מיידי. סוג הפריט יכול להיות היא שימושית מאוד, כי הפלט האחרון של התסריט תמיד יהיה מוצלח או השגיאה שגרמה לכשל ב-build.

באמצעות ההצהרות export מגדירים את הערכים של שתי סביבות משתנים. הם מאפשרים להעביר פרמטרים נוספים של שורת הפקודה מהדר (CFLAGS), המהדר של C++ (CXXFLAGS) והמקשר (LDFLAGS). כולם מקבלים את הגדרות האופטימיזציה דרך OPTIMIZE כדי לוודא הכל עובר אופטימיזציה באותו אופן. יש כמה ערכים אפשריים למשתנה OPTIMIZE:

  • -O0: ללא אופטימיזציה. לא מתבטל קוד מת, ו-Emscripten גם לא מקטינה את קוד ה-JavaScript שהוא פולט. מתאים לניפוי באגים.
  • -O3: מבצעים אופטימיזציה אגרסיבית להשגת הביצועים.
  • -Os: מבצעים אופטימיזציה אגרסיבית לביצועים ולגודל כיוצרים משניים קריטריון.
  • -Oz: אופטימיזציה אגרסיבית ביחס לגודל, והירידה בביצועים במקרה הצורך.

באינטרנט, מומלץ בעיקר על -Os.

לפקודה emcc יש הרבה אפשרויות משלה. לתשומת ליבכם: emcc הוא אמור להיות 'החלפה קטנה' למהדרים כמו GCC או clang. כך שכל אם אתם עשויים לדעת מ-GCC, נו. הדגל -s הוא מיוחד בכך שהוא מאפשר לנו להגדיר את Emscripten ספציפית. כל האפשרויות הזמינות נמצאות ב settings.js אבל הקובץ הזה יכול להיות די מפחיד. רשימה של דגלי Emscripten שלדעתי הם החשובים ביותר למפתחי אתרים:

  • --bind מפעילים embind.
  • -s STRICT=1 מבטל את התמיכה בכל אפשרויות ה-build שהוצאו משימוש. כך אפשר להבטיח שהקוד שלכם יוצר באופן שתואם ל-הכוונה.
  • הפונקציה -s ALLOW_MEMORY_GROWTH=1 מאפשרת להגדיל את הזיכרון באופן אוטומטי אם הנחוצים. בזמן הכתיבה, Emscripten יקצה 16MB של זיכרון בהתחלה. מאחר שהקוד שלך מקצה קטעי זיכרון, האפשרות הזו קובעת אם הפעולות האלה יגרמו לכך שכל מודול ה-Wasm ייכשל כשהזיכרון או אם קוד החיבור יכול להרחיב את הזיכרון הכולל להתאים את ההקצאה.
  • -s MALLOC=... בוחר באיזו הטמעה של malloc() להשתמש. emmalloc הוא הטמעה קטנה ומהירה של malloc() במיוחד עבור Emscripten. החלופה היא dlmalloc, הטמעה מלאה של malloc(). רק אתם צריך לעבור אל dlmalloc אם מקצה הרבה אובייקטים קטנים לעיתים קרובות או אם אתם רוצים להשתמש בשרשורים.
  • הקוד -s EXPORT_ES6=1 יהפוך למודול ES6 עם ייצוא ברירת מחדל שעובד עם כל Bundler. נדרש גם -s MODULARIZE=1 כדי צריך להגדיר.

הסימונים הבאים לא תמיד נחוצים או שהם שימושיים רק לניפוי באגים מטרות:

  • -s FILESYSTEM=0 הוא דגל שקשור ל-Emscripten ויש לו יכולת אמולציה של מערכת קבצים כאשר קוד C/C++ משתמש בפעולות של מערכת קבצים. היא מבצעת ניתוח מסוים של הקוד שהיא יוצרת כדי להחליט אם לכלול את באמולציה של מערכת הקבצים בקוד הדבק, או לא. עם זאת, לפעמים אבל הניתוח יכול לטעות, וצריך לשלם סכום של 70kB די גדול בדבק נוסף לאמולציית מערכת קבצים שייתכן שאתם לא צריכים. באמצעות -s FILESYSTEM=0, אפשר לאלץ את Emscripten לא לכלול את הקוד הזה.
  • המדיניות -g4 תגרום ל-Emscripten לכלול מידע על תוצאות ניפוי הבאגים ב-.wasm וב- פולטים גם קובץ מפות מקור עבור מודול Wam. אפשר לקרוא עוד ב- ניפוי באגים באמצעות 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>

(הנה gist שמכיל את כל הקבצים.)

כדי ליצור הכול, מפעילים

$ 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 מטפלת בשכפול ה-GitHub של libvpx למאגר node_modules בשם libvpx.

עכשיו אפשר להרחיב את סקריפט ה-build כדי לבנות libvpx. libvpx משתמש ב-configure ו-make, למרבה המזל, אפליקציית Emscripten יכולה לעזור לוודא ש-configure make משתמש ב'מהדר (compiler)' של Emscripten. למטרה הזו יש את ה-wrapper הפקודות 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 שהודפסה באמצעות emscripten.

ניתן גם לראות שתהליך ה-build נמשך הרבה זמן. הסיבה ל שזמן ה-build הארוך יכול להשתנות. במקרה של libvpx, זה לוקח הרבה זמן כי הוא הידור מהמקודד ומפענח גם ל-VP8 וגם ל-VP9 בכל הרצה של פקודת ה-build, למרות שקובצי המקור לא השתנו. גם קטן הבנייה של השינוי ל-my-module.cpp שלך תימשך זמן רב. זה יהיה מאוד שימושי לשמור את תוצרי ה-build של 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 מאפשרת לנו להגדיר משתני סביבה על ידי העברת פרמטרים לתסריט ה-build. הפקודה test תדלג על בניית libvpx אם $SKIP_LIBVPX מוגדר (לכל ערך).

עכשיו אפשר להדר את המודול אבל לדלג על בנייה מחדש של libvpx:

$ npm run build:emscripten -- SKIP_LIBVPX=1

התאמה אישית של סביבת ה-build

לפעמים הספריות תלויות בכלים נוספים שאפשר ליצור. אם יחסי התלות האלה חסרים בסביבת ה-build שסופקה על ידי קובץ האימג' של Docker, להוסיף אותם בעצמך. לדוגמה, נניח שאתם גם רוצים ליצור תיעוד של libvpx באמצעות doxygen. דוקסינג הוא לא זמין בקונטיינר של Docker, אבל אפשר להתקין אותו באמצעות apt.

אם היה אפשר לעשות את זה ב-build.sh, צריך להוריד ולהתקין מחדש בכל פעם שרוצים לבנות את הספרייה שלכם, לא רק שזה יהיה אבל הוא גם מונע מכם לעבוד על הפרויקט במצב אופליין.

כאן הגיוני ליצור קובץ אימג' של Docker משלכם. קובצי אימג' של Docker פותחו על ידי Dockerfile שמתארת את שלבי ה-build. קובצי Docker הם די טובים ויש להם הרבה פקודות, אבל שאפשר לצאת לדרך דרך 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, אבל רק אם היא עדיין לא נוצרה. לאחר מכן הכול פועל כמו קודם, אבל עכשיו בסביבת ה-build יש doxygen הפקודה הזמינה, שתגרום ליצירת התיעוד של libvpx נו.

סיכום

לא מפתיע שקוד C/C++ ו-npm הם לא התאמה טבעית, אבל אפשר לגרום לו לעבוד בצורה נוחה למדי עם כמה כלים נוספים ובידוד ש-Docker מספק. ההגדרה הזו לא מתאימה לכל הפרויקטים, אבל נקודת התחלה טובה, שתאפשר לך לשנות בהתאם לצרכים שלך. אם יש לכם שיפורים, נא לשתף.

נספח: שימוש בשכבות תמונה של Docker

פתרון חלופי הוא להקיף חלק גדול יותר מהבעיות האלה ב-Docker, הגישה החכמה של Docker לשמירה במטמון. Docker מבצע קובצי Docker שלב אחרי שלב תקצה לתוצאה של כל שלב תמונה משלה. תמונות הביניים האלה נקראות בדרך כלל 'שכבות'. אם פקודה בקובץ Docker לא השתנתה, Docker לא יפעילו מחדש שלב זה כאשר תיצרו מחדש את קובץ ה-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. כתוצאה מכך, אין צורך יותר את "נאפה".