בקטע מה זה WebAssembly ואיפה הוא הגיע?, הסברתי איך סיימנו היום את WebAssembly. במאמר הזה אראה לכם את הגישה שלי להרכבת תוכנית C קיימת, mkbitmap
, ל-WebAssembly. הוא מורכב יותר מהדוגמה של שלום עולם, כי הוא כולל עבודה עם קבצים, תקשורת בין WebAssembly ויבשתי JavaScript וציור אל קנבס, אבל עדיין אפשר לנהל אותה מספיק כדי לא להציף אתכם.
המאמר נכתב למפתחי אתרים שרוצים ללמוד על WebAssembly ולראות איך אפשר להתקדם אם רוצים להדר קובץ כמו mkbitmap
ל-WebAssembly. אזהרה הוגנת: אי ביצוע הידור של אפליקציה או ספרייה בהפעלה הראשונה הוא נורמלי לחלוטין, ולכן חלק מהשלבים שמתוארים בהמשך לא עבדו, ולכן היה עליי לבצע מעקב לאחור ולנסות שוב באופן שונה. המאמר לא מציג את פקודת ההידור הסופית של הקסם כאילו היא נפלה מהשמיים, אלא מתאר את ההתקדמות שלי בפועל, כולל כמה תסכולים.
מידע על mkbitmap
תוכנת C mkbitmap
קוראת תמונה ומחילה עליה אחת או יותר מהפעולות הבאות, לפי הסדר הזה: היפוך, סינון איכות גבוהה (pass), שינוי קנה מידה וקביעת ערך סף. אפשר לשלוט בכל פעולה בנפרד ולהפעיל או להשבית אותה. השימוש העיקרי ב-mkbitmap
הוא להמיר תמונות בצבע או בגווני אפור לפורמט שמתאים כקלט לתוכנות אחרות, במיוחד לתוכנית המעקב potrace
שמהווה את הבסיס של SVGcode. ככלי לעיבוד מראש, mkbitmap
שימושי במיוחד להמרת אמנות קו סרוקה, כמו סרטים מצוירים או טקסט בכתב יד, לתמונות דו-שלביות ברזולוציה גבוהה.
כדי להשתמש בקובץ mkbitmap
, מעבירים אליו כמה אפשרויות ושם קובץ אחד או יותר. לפרטים נוספים, אפשר להיכנס לדף הראשי של הכלי:
$ mkbitmap [options] [filename...]
![תמונה של סרטים מצוירים בצבע.](https://web.dev/static/articles/compiling-mkbitmap-to-webassembly/image/cartoon-image-color.png?authuser=5&hl=he)
![התמונה המצוירת הומרה לגווני אפור אחרי העיבוד מראש.](https://web.dev/static/articles/compiling-mkbitmap-to-webassembly/image/cartoon-image-converted-97142bc66e12e.png?authuser=5&hl=he)
mkbitmap -f 2 -s 2 -t 0.48
(מקור).קבל את הקוד
השלב הראשון הוא לקבל את קוד המקור של mkbitmap
. הוא מופיע באתר הפרויקט. נכון לעכשיו, הגרסה העדכנית ביותר היא otrace-1.16.tar.gz.
הידור והתקנה באופן מקומי
השלב הבא הוא להדר ולהתקין את הכלי באופן מקומי כדי לקבל מושג לגבי אופן הפעולה שלו. הקובץ INSTALL
מכיל את ההוראות הבאות:
cd
לספרייה שמכילה את קוד המקור של החבילה, ומקלידים./configure
כדי להגדיר את החבילה למערכת.ההרצה של
configure
עשויה להימשך זמן מה. בזמן הריצה, הוא מדפיסה הודעות מסוימות שמסבירות אילו תכונות הוא מחפש.כדי להדר את החבילה, מקלידים
make
.אפשר גם להקליד
make check
כדי להריץ בדיקות עצמיות שצורפו לחבילה, בדרך כלל באמצעות הקבצים הבינאריים שהותקנו מראש בחבילה.מקלידים
make install
כדי להתקין את התוכנות ואת קובצי הנתונים והמסמכים. כשמתקינים בתוך קידומת שנמצאת בבעלות הרמה הבסיסית (root), מומלץ להגדיר את החבילה וליצור אותה כמשתמש רגיל, ורק השלבmake install
יופעל עם הרשאות לרמה הבסיסית (root).
לאחר ביצוע השלבים האלה, אמורים להופיע שני קובצי הפעלה, potrace
ו-mkbitmap
, והנושא השני הוא מוקד המאמר הזה. כדי לוודא שהפעולה בוצעה כמו שצריך, אפשר להריץ את הפקודה mkbitmap --version
. הנה הפלט של כל ארבעת השלבים מהמכונה שלי, עם חיתוך נרחב כדי לקצר את הזמן:
שלב 1, ./configure
:
$ ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
checking whether make sets $(MAKE)... yes
[…]
config.status: executing libtool commands
שלב 2, make
:
$ make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
clang -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all-am'.
שלב 3, make check
:
$ make check
Making check in src
make[1]: Nothing to be done for `check'.
Making check in doc
make[1]: Nothing to be done for `check'.
[…]
============================================================================
Testsuite summary for potrace 1.16
============================================================================
# TOTAL: 8
# PASS: 8
# SKIP: 0
# XFAIL: 0
# FAIL: 0
# XPASS: 0
# ERROR: 0
============================================================================
make[1]: Nothing to be done for `check-am'.
שלב 4, sudo make install
:
$ sudo make install
Password:
Making install in src
.././install-sh -c -d '/usr/local/bin'
/bin/sh ../libtool --mode=install /usr/bin/install -c potrace mkbitmap '/usr/local/bin'
[…]
make[2]: Nothing to be done for `install-data-am'.
כדי לבדוק אם זה עובד, מריצים את mkbitmap --version
:
$ mkbitmap --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
אם פרטי הגרסה מגיעים אליך, זה אומר שהרכבת והתקנת האפליקציה mkbitmap
מתבצעות בהצלחה. לאחר מכן, יוצרים במקביל את השלבים האלה לעבוד עם WebAssembly.
הידור של mkbitmap
ל-WebAssembly
Emscripten הוא כלי להרכבת תוכניות C/C++ אל WebAssembly. במסמכי התיעוד של Emscripten בנושא Building Projects מפורטים הפרטים הבאים:
קל מאוד לבנות פרויקטים גדולים באמצעות Emscripten. Emscripten מספק שני סקריפטים פשוטים שמגדירים את קובצי ה-Makefile לשימוש ב-
emcc
כתחליף ל-gcc
. ברוב המקרים, ברוב המקרים שאר מערכת ה-build הנוכחית של הפרויקט נשארת ללא שינוי.
לאחר מכן התיעוד ממשיך (מעט נערך כדי לקצר):
קחו לדוגמה את התרחיש שבו בדרך כלל אתם יוצרים את הפקודות הבאות:
./configure
make
כדי ליצור באמצעות Emscripten, צריך להשתמש בפקודות הבאות:
emconfigure ./configure
emmake make
כלומר, בעצם, ./configure
הופך ל-emconfigure ./configure
ו-make
הופך ל-emmake make
. הסרטון הבא מדגים איך לעשות זאת בעזרת mkbitmap
.
שלב 0, make clean
:
$ make clean
Making clean in src
rm -f potrace mkbitmap
test -z "" || rm -f
rm -rf .libs _libs
[…]
rm -f *.lo
שלב 1, emconfigure ./configure
:
$ emconfigure ./configure
configure: ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... ./install-sh -c -d
checking for gawk... no
checking for mawk... no
checking for nawk... no
checking for awk... awk
[…]
config.status: executing libtool commands
שלב 2, emmake make
:
$ emmake make
make: make
/Applications/Xcode.app/Contents/Developer/usr/bin/make all-recursive
Making all in src
/opt/homebrew/Cellar/emscripten/3.1.36/libexec/emcc -DHAVE_CONFIG_H -I. -I.. -g -O2 -MT main.o -MD -MP -MF .deps/main.Tpo -c -o main.o main.c
mv -f .deps/main.Tpo .deps/main.Po
[…]
make[2]: Nothing to be done for `all'.
אם הכול היה תקין, עכשיו אמורים להיות .wasm
קבצים בספרייה. כדי למצוא אותם, מריצים את find . -name "*.wasm"
:
$ find . -name "*.wasm"
./a.wasm
./src/mkbitmap.wasm
./src/potrace.wasm
שתי האפשרויות האחרונות נראות מבטיחות, לכן cd
נמצאת בספרייה src/
. יש עכשיו גם שני קבצים תואמים חדשים, mkbitmap
ו-potrace
. למאמר הזה, רק mkbitmap
רלוונטי. העובדה שהם לא כוללים את התוסף .js
קצת מבלבלת, אבל הם למעשה קובצי JavaScript, שאפשר לאמת באמצעות קריאה מהירה ל-head
:
$ cd src/
$ head -n 20 mkbitmap
// include: shell.js
// The Module object: Our interface to the outside world. We import
// and export values on it. There are various ways Module can be used:
// 1. Not defined. We create it here
// 2. A function parameter, function(Module) { ..generated code.. }
// 3. pre-run appended it, var Module = {}; ..generated code..
// 4. External script tag defines var Module.
// We need to check if Module already exists (e.g. case 3 above).
// Substitution will be replaced with actual code on later stage of the build,
// this way Closure Compiler will not mangle it (e.g. case 4. above).
// Note that if you want to run closure, and also to use Module
// after the generated code, you will need to define var Module = {};
// before the code. Then that object will be used in the code, and you
// can continue to use Module afterwards as well.
var Module = typeof Module != 'undefined' ? Module : {};
// --pre-jses are emitted after the Module integration code, so that they can
// refer to Module (if they choose; they can also define Module)
משנים את השם של קובץ ה-JavaScript ל-mkbitmap.js
על ידי קריאה ל-mv mkbitmap mkbitmap.js
(ו-mv potrace potrace.js
אם רוצים).
עכשיו הגיע הזמן לבדיקה הראשונה כדי לראות אם הפעלת הקובץ באמצעות Node.js בשורת הפקודה על ידי הרצת node mkbitmap.js --version
:
$ node mkbitmap.js --version
mkbitmap 1.16. Copyright (C) 2001-2019 Peter Selinger.
הרכבת בהצלחה את mkbitmap
ל-WebAssembly. השלב הבא הוא להפעיל אותו בדפדפן.
mkbitmap
באמצעות WebAssembly בדפדפן
מעתיקים את הקבצים mkbitmap.js
ו-mkbitmap.wasm
לספרייה חדשה בשם mkbitmap
ויוצרים קובץ boilerplate של HTML index.html
שטוען את קובץ ה-JavaScript mkbitmap.js
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<script src="mkbitmap.js"></script>
</body>
</html>
מפעילים שרת מקומי שמציג את הספרייה mkbitmap
ופותחים אותה בדפדפן. אמורה להופיע בקשה להזנת קלט. זה כצפוי, כי בהתאם לדף הראשי של הכלי, "[i]f לא ניתנים ארגומנטים לשמות קבצים, אז mkbitmap משמש כמסנן, קריאה מקלט רגיל", שעבור Emscripten כברירת מחדל הוא prompt()
.
מניעת הפעלה אוטומטית
כדי להפסיק את ההפעלה של mkbitmap
באופן מיידי ובמקום זאת להמתין לקלט של משתמשים, צריך להבין את האובייקט Module
של Emscripten. Module
הוא אובייקט JavaScript גלובלי עם מאפיינים שקריאות לקוד שנוצר על ידי Emscripten בנקודות שונות בהפעלה.
אפשר לספק הטמעה של Module
כדי לשלוט בהרצת הקוד.
כשאפליקציית Emscripten מופעלת, היא בודקת את הערכים באובייקט Module
ומחילה אותם.
במקרה של mkbitmap
, צריך להגדיר את Module.noInitialRun
לערך true
כדי למנוע את ההפעלה הראשונית שהובילה להצגת ההנחיה. יוצרים סקריפט בשם script.js
, כוללים אותו לפני <script src="mkbitmap.js"></script>
ב-index.html
ומוסיפים את הקוד הבא אל script.js
. עכשיו כשטוענים מחדש את האפליקציה, ההנחיה אמורה להיעלם.
var Module = {
// Don't run main() at page load
noInitialRun: true,
};
יצירת build מודולרי עם כמה דגלים של build
כדי לספק קלט לאפליקציה, אפשר להשתמש במערכת הקבצים של Emscripten ב-Module.FS
. בקטע Include File System Support (כולל תמיכה במערכת קבצים) של מסמכי התיעוד כתוב:
Emscripten מחליט אם לכלול תמיכה במערכת הקבצים באופן אוטומטי. לתוכנות רבות אין צורך בקבצים, וגודל התמיכה במערכות קבצים אינו זניח, לכן Emscripten נמנע מהכללתה אם אין סיבה לכך. כלומר, אם קוד ה-C/C++ לא ניגש לקבצים, האובייקט
FS
וממשקי API אחרים של מערכת הקבצים לא ייכללו בפלט. לעומת זאת, אם קוד ה-C/C++ שלכם כן משתמש בקבצים, תמיכה במערכות קבצים תיכלל באופן אוטומטי.
לצערנו, mkbitmap
הוא אחד מהמקרים שבהם מערכת Emscripten לא כוללת באופן אוטומטי תמיכה במערכת הקבצים, ולכן צריך לציין זאת במפורש. כלומר, צריך לפעול לפי השלבים של emconfigure
ו-emmake
שתוארו למעלה, ולהגדיר עוד כמה סימונים באמצעות ארגומנט CFLAGS
. הדגלים הבאים יכולים להיות שימושיים גם בפרויקטים אחרים.
- מגדירים את
-sFILESYSTEM=1
כדי שתהיה תמיכה במערכות קבצים. - מגדירים את
-sEXPORTED_RUNTIME_METHODS=FS,callMain
כדי לייצא אתModule.FS
ואתModule.callMain
. - מגדירים את
-sMODULARIZE=1
ואת-sEXPORT_ES6
כדי ליצור מודול ES6 מודרני. - מגדירים
-sINVOKE_RUN=0
כדי למנוע את ההפעלה הראשונית שהובילה להצגת ההצעה.
בנוסף, במקרה הספציפי הזה, צריך להגדיר את הדגל --host
לערך wasm32
כדי לציין לסקריפט configure
שאותה אתם מחברים ל-WebAssembly.
פקודת emconfigure
הסופית נראית כך:
$ emconfigure ./configure --host=wasm32 CFLAGS='-sFILESYSTEM=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sMODULARIZE=1 -sEXPORT_ES6 -sINVOKE_RUN=0'
חשוב לזכור להריץ שוב את emmake make
ולהעתיק את הקבצים החדשים שנוצרו לתיקייה mkbitmap
.
משנים את index.html
כך שיטען רק את מודול ה-ES script.js
, שממנו מייבאים את המודול mkbitmap.js
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>mkbitmap</title>
</head>
<body>
<!-- No longer load `mkbitmap.js` here -->
<script src="script.js" type="module"></script>
</body>
</html>
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
console.log(Module);
};
run();
כשפותחים את האפליקציה בדפדפן, האובייקט Module
אמור להופיע במסוף כלי הפיתוח, וההנחיה נעלמת, כי בהתחלה לא מתבצעת קריאה לפונקציה main()
של mkbitmap
.
הפעלה ידנית של הפונקציה הראשית
בשלב הבא, מפעילים את הפונקציה Module.callMain()
כדי להפעיל ידנית את הפונקציה main()
של mkbitmap
. הפונקציה callMain()
לוקחת מערך של ארגומנטים, שזהים למה שהייתם מעבירים בשורת הפקודה. אם בשורת הפקודה היה מריצים את הפקודה mkbitmap -v
, צריך לקרוא ל-Module.callMain(['-v'])
בדפדפן. הפעולה הזו רושמת את מספר הגרסה של mkbitmap
במסוף כלי הפיתוח.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
Module.callMain(['-v']);
};
run();
הפניה אוטומטית של הפלט הרגיל
הפלט הסטנדרטי (stdout
) כברירת מחדל הוא המסוף. אבל אפשר להפנות אותו למשהו אחר, לדוגמה, פונקציה שמאחסנת את הפלט במשתנה. המשמעות היא שאפשר להוסיף את הפלט ל-HTML על ידי הגדרת המאפיין Module.print
.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
let consoleOutput = 'Powered by ';
const Module = await loadWASM({
print: (text) => (consoleOutput += text),
});
Module.callMain(['-v']);
document.body.textContent = consoleOutput;
};
run();
הכנסת קובץ הקלט למערכת קובצי הזיכרון
כדי להעביר את קובץ הקלט למערכת קובצי הזיכרון, צריך את המקבילה ל-mkbitmap filename
בשורת הפקודה. כדי להבין איך אני ניגש לזה, קודם קצת רקע על האופן שבו mkbitmap
מצפה לקלט שלו ויוצר את הפלט.
הפורמטים הנתמכים של קלט mkbitmap
הם PNM (PBM, PGM, PPM) ו-BMP. פורמטי הפלט הם PBM למיפוי סיביות ב-bit, ו-PGM למיפוי אפור. אם נותנים ארגומנט filename
, mkbitmap
ייצור כברירת מחדל קובץ פלט ששמו התקבל מהשם של קובץ הקלט על ידי שינוי הסיומת שלו ל-.pbm
. לדוגמה, לשם של קובץ הקלט example.bmp
, השם של קובץ הפלט יהיה example.pbm
.
חברת Emscripten מספקת מערכת קבצים וירטואלית שמדמה את מערכת הקבצים המקומית, כך שאפשר להדר ולהפעיל קוד מקורי שמשתמש בממשקי API סינכרוניים של קבצים ללא שינוי או עם שינויים מזעריים.
כדי ש-mkbitmap
יקרא קובץ קלט כאילו הוא הועבר כארגומנט של שורת הפקודה filename
, צריך להשתמש באובייקט FS
ש-Emscripten מספק.
האובייקט FS
מגובה על ידי מערכת קבצים בזיכרון (שנקראת בדרך כלל MEMFS) ויש לו פונקציה writeFile()
שמשמשת לכתיבת קבצים במערכת הקבצים הווירטואלית. אתם משתמשים ב-writeFile()
כמו בדוגמת הקוד הבאה.
כדי לוודא שפעולת כתיבת הקובץ פועלת, מריצים את הפונקציה readdir()
של האובייקט FS
עם הפרמטר '/'
. יופיע הכיתוב example.bmp
ומספר קובצי ברירת מחדל שנוצרים תמיד באופן אוטומטי.
לתשומת ליבך, השיחה הקודמת אל Module.callMain(['-v'])
להדפסת מספר הגרסה הוסרה. הסיבה לכך היא ש-Module.callMain()
היא פונקציה שבדרך כלל מצפה לפעול רק פעם אחת.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
console.log(Module.FS.readdir('/'));
};
run();
ביצוע בפועל
כשהכול מוכן, מפעילים את mkbitmap
על ידי הרצת Module.callMain(['example.bmp'])
. רושמים את התוכן של תיקיית '/'
ב-MEMFS. קובץ הפלט החדש שנוצר אמור להופיע ליד קובץ הקלט example.bmp
.example.pbm
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
console.log(Module.FS.readdir('/'));
};
run();
שליפת קובץ הפלט ממערכת קובצי הזיכרון
הפונקציה readFile()
של האובייקט FS
מאפשרת להוציא את example.pbm
שנוצר בשלב האחרון ממערכת קובצי הזיכרון. הפונקציה מחזירה Uint8Array
שממירים לאובייקט File
ושומרים בדיסק, כי בדרך כלל דפדפנים לא תומכים בקובצי PBM לצפייה ישירה בדפדפן.
(יש דרכים אלגנטיות יותר לשמור קובץ, אבל השימוש ברכיב <a download>
שנוצר באופן דינמי הוא הנפוץ ביותר). אחרי ששומרים את הקובץ, אפשר לפתוח אותו במציג התמונות המועדף עליכם.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
const buffer = await fetch('https://example.com/example.bmp').then((res) => res.arrayBuffer());
Module.FS.writeFile('example.bmp', new Uint8Array(buffer));
Module.callMain(['example.bmp']);
const output = Module.FS.readFile('example.pbm', { encoding: 'binary' });
const file = new File([output], 'example.pbm', {
type: 'image/x-portable-bitmap',
});
const a = document.createElement('a');
a.href = URL.createObjectURL(file);
a.download = file.name;
a.click();
};
run();
הוספת ממשק משתמש אינטראקטיבי
עד השלב הזה, קובץ הקלט הוא בתוך הקוד ו-mkbitmap
פועל עם פרמטרים שמוגדרים כברירת מחדל. בשלב האחרון אתם יכולים לאפשר למשתמש לבחור באופן דינמי קובץ קלט, לשנות את הפרמטרים של mkbitmap
ואז להפעיל את הכלי עם האפשרויות שנבחרו.
// Corresponds to `mkbitmap -o output.pbm input.bmp -s 8 -3 -f 4 -t 0.45`.
Module.callMain(['-o', 'output.pbm', 'input.bmp', '-s', '8', '-3', '-f', '4', '-t', '0.45']);
לא קשה במיוחד לנתח את פורמט התמונה של PBM, לכן אפשר אפילו להציג תצוגה מקדימה של תמונת הפלט בקוד JavaScript מסוים. דרך אחת לעשות זאת אפשר לעיין בקוד המקור של ההדגמה המוטמעת.
סיכום
מזל טוב, קומפלת בהצלחה את mkbitmap
ל-WebAssembly והצלחת להפעיל אותו בדפדפן! התרחשו כמה מבוי סתום ונאלצת להרכיב את הכלי יותר מפעם אחת עד שהוא עבד, אבל כמו שכתבתי למעלה, זה חלק מהחוויה. אם נתקעת, חשוב לזכור גם את התג webassembly
של StackOverflow. הידור שמח!
אישורים
המאמר הזה נבדק על ידי סם קלג ורייצ'ל אנדרו.