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