במאמר מה זה WebAssembly ומאיפה זה הגיע?, הסברתי איך הגענו ל-WebAssembly של היום. במאמר הזה אראה לכם את הגישה שלי להידור של תוכנית C קיימת, mkbitmap
, ל-WebAssembly. הוא מורכב יותר מהדוגמה hello world, כי הוא כולל עבודה עם קבצים, תקשורת בין WebAssembly לבין JavaScript וציור על בד ציור, אבל הוא עדיין פשוט מספיק כדי לא להעמיס עליכם.
המאמר מיועד למפתחי אתרים שרוצים ללמוד על WebAssembly, ומוסבר בו שלב אחר שלב איך אפשר להמשיך אם רוצים לקמפל משהו כמו mkbitmap
ל-WebAssembly. חשוב לי לציין מראש שזה נורמלי לחלוטין שאפליקציה או ספרייה לא יתקמפלו בהרצה הראשונה. לכן, חלק מהשלבים שמתוארים בהמשך לא עבדו, ונאלצתי לחזור אחורה ולנסות שוב בדרך אחרת. במאמר לא מוצגת פקודת הקומפילציה הסופית כאילו היא נחתה מהשמיים, אלא מתואר בו התהליך שעברתי בפועל, כולל כמה תסכולים.
מידע על mkbitmap
תוכנית ה-C mkbitmap
קוראת תמונה ומבצעת עליה אחת או יותר מהפעולות הבאות, לפי הסדר הזה: היפוך, סינון highpass, שינוי קנה מידה וקביעת סף. אפשר לשלוט בכל פעולה בנפרד ולהפעיל או להשבית אותה. השימוש העיקרי ב-mkbitmap
הוא המרת תמונות צבעוניות או תמונות בגווני אפור לפורמט שמתאים כקלט לתוכנות אחרות, במיוחד לתוכנת המעקב potrace
שמהווה את הבסיס ל-SVGcode. ככלי לעיבוד מקדים, mkbitmap
שימושי במיוחד להמרת יצירות אמנות קוויות שנסרקו, כמו קריקטורות או טקסט בכתב יד, לתמונות דו-רמות ברזולוציה גבוהה.
משתמשים ב-mkbitmap
על ידי העברת מספר אפשרויות ושם קובץ אחד או יותר. לפרטים נוספים, אפשר לעיין בדף ההוראות של הכלי:
$ mkbitmap [options] [filename...]


mkbitmap -f 2 -s 2 -t 0.48
(Source).קבל את הקוד
השלב הראשון הוא להשיג את קוד המקור של mkbitmap
. אפשר למצוא אותו באתר של הפרויקט. בזמן כתיבת המאמר הזה, potrace-1.16.tar.gz היא הגרסה האחרונה.
הידור והתקנה באופן מקומי
השלב הבא הוא לקמפל ולהתקין את הכלי באופן מקומי כדי להבין איך הוא מתנהג. קובץ INSTALL
מכיל את ההוראות הבאות:
cd
לתיקייה שמכילה את קוד המקור של החבילה ומקלידים./configure
כדי להגדיר את החבילה למערכת.יכול להיות שההרצה של
configure
תימשך זמן מה. במהלך ההרצה, התוכנה מדפיסה הודעות שמציינות אילו תכונות היא בודקת.מקלידים
make
כדי לקמפל את החבילה.אפשר גם להקליד
make check
כדי להריץ בדיקות עצמיות שמגיעות עם החבילה, בדרך כלל באמצעות קבצים בינאריים לא מותקנים שנבנו זה עתה.מקלידים
make install
כדי להתקין את התוכניות, את קובצי הנתונים ואת המסמכים. כשמתקינים בקידומת שבבעלות root, מומלץ להגדיר את החבילה ולבנות אותה כמשתמש רגיל, ולהריץ רק את השלבmake install
עם הרשאות root.
אחרי שתפעלו לפי השלבים האלה, אמורים להיות לכם שני קובצי הפעלה, potrace
ו-mkbitmap
. המאמר הזה מתמקד בקובץ 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. במסמך Building Projects של Emscripten מצוין:
קל מאוד לבנות פרויקטים גדולים באמצעות 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
ויוצרים קובץ HTML boilerplate 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
ופותחים אותה בדפדפן. תופיע הנחיה שבה תתבקשו להזין קלט. זה צפוי, כי לפי דף ה-man של הכלי, "אם לא מציינים ארגומנטים של שם קובץ, הפקודה mkbitmap פועלת כמסנן, וקוראת מקלט סטנדרטי", שהוא prompt()
כברירת מחדל ב-Emscripten.
מניעת הרצה אוטומטית
כדי להפסיק את ההפעלה המיידית של 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,
};
יצירת גרסה מודולרית עם עוד דגלי בנייה
כדי לספק קלט לאפליקציה, אפשר להשתמש בתמיכה של מערכת הקבצים של Emscripten ב-Module.FS
. בקטע Including 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
לא נקראת יותר בהתחלה.
הפעלה ידנית של הפונקציה הראשית
השלב הבא הוא להפעיל ידנית את הפונקציה mkbitmap
של main()
על ידי הרצת Module.callMain()
. הפונקציה 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 למפות סיביות ו-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.pbm
שנוצר לצד קובץ הקלט example.bmp
.
// 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 ולגרום לו לפעול בדפדפן! היו כמה דרכים ללא מוצא והיה צריך להדר את הכלי יותר מפעם אחת עד שהוא עבד, אבל כמו שכתבתי למעלה, זה חלק מהחוויה. אם נתקעתם, תוכלו להיעזר גם בתג StackOverflow's webassembly
. קומפילציה נעימה!
תודות
המאמר הזה נבדק על ידי Sam Clegg ו-Rachel Andrew.