בקטע מה זה WebAssembly ומאיפה הוא הגיע?, הסברתי איך הגענו ל-WebAssembly של היום. במאמר הזה אראה לכם את הגישה שלי להרכבת תוכנית C קיימת, mkbitmap
, ל-WebAssembly. הוא מורכב יותר מהדוגמה של עולם שלום, כי הוא כולל עבודה עם קבצים, תקשורת בין שטחי WebAssembly ו-JavaScript וציור על קנבס, אבל עדיין אפשר לנהל אותו מספיק כדי לא להציף אתכם.
המאמר הזה מיועד למפתחי אתרים שרוצים ללמוד על WebAssembly, ומראה איך אפשר להמשיך אם רוצים להדרים משהו כמו mkbitmap
ל-WebAssembly. אזהרה הוגנת, לא ניתן לבצע הידור של אפליקציה או ספרייה בהפעלה הראשונה. לכן, חלק מהשלבים שמתוארים בהמשך לא עבדו, ולכן נאלצתי לחזור אחורה ולנסות שוב באופן שונה. המאמר לא מציג את פקודת ההידור הסופית הקסומה כאילו היא נפלה מהשמיים, אלא מתארת את ההתקדמות האמיתית שלי, כולל כמה תסכולים.
מידע על mkbitmap
תוכנת C mkbitmap
קוראת תמונה ומחילה עליה אחת או יותר מהפעולות הבאות, בסדר הזה: היפוך, סינון גובה-רוחב, התאמה לעומס (scaling) וערכי סף. אפשר לשלוט בכל פעולה ולהפעיל או להשבית אותה בנפרד. השימוש העיקרי ב-mkbitmap
הוא להמיר תמונות בצבע או בגווני אפור לפורמט שמתאים כקלט לתוכניות אחרות, ובמיוחד תוכנת המעקב potrace
שמהווה את הבסיס של SVGcode. mkbitmap
הוא כלי לעיבוד מראש. הוא שימושי במיוחד להמרה של גרפיקה סרוקה, כמו סרטים מצוירים או טקסט בכתב יד, לתמונות ברזולוציה גבוהה בשני מפלסים.
כדי להשתמש ב-mkbitmap
, צריך להעביר לו כמה אפשרויות ושם קובץ אחד או יותר. פרטים נוספים זמינים בדף הניהול של הכלי:
$ mkbitmap [options] [filename...]
קבל את הקוד
השלב הראשון הוא להשיג את קוד המקור של mkbitmap
. אפשר למצוא אותו באתר הפרויקט. נכון למועד כתיבת ההודעה הזו, potrace-1.16.tar.gz היא הגרסה האחרונה.
הידור והתקנה באופן מקומי
השלב הבא הוא להדר ולהתקין את הכלי באופן מקומי כדי לקבל מושג איך הוא מתנהג. הקובץ INSTALL
מכיל את ההוראות הבאות:
cd
לספרייה שמכילה את קוד המקור של החבילה ומקלידים./configure
כדי להגדיר את החבילה למערכת.ההפעלה של
configure
עשויה להימשך זמן מה. בזמן שהיא פועלת, השירות מדפיסה כמה הודעות כדי לדעת אילו תכונות הוא בודק.מקלידים
make
כדי להדר את החבילה.אפשר להקליד
make check
כדי להריץ בדיקות עצמיות שמגיעות עם החבילה, בדרך כלל באמצעות הקבצים הבינאריים שהוסרו.מקלידים
make install
כדי להתקין את התוכנות ואת כל קובצי הנתונים והמסמכים. כשמתקינים בקידומת שבבעלות הרמה הבסיסית (root), מומלץ להגדיר ולבנות את החבילה כמשתמש רגיל, ויבוצע רק השלבmake install
עם הרשאות בסיס.
כשפועלים לפי השלבים האלה, מקבלים שני קובצי הפעלה, 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 מספק שני סקריפטים פשוטים שמגדירים את הקבצים לשימוש ב-
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
ויוצרים קובץ סטנדרטי של index.html
ל-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
. בקטע תמיכה במערכת קבצים במסמך מצוין:
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
לא נקראת יותר.
הפעלה ידנית של הפונקציה הראשית
השלב הבא הוא לקרוא באופן ידני לפונקציה main()
של mkbitmap
על ידי הרצת 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 והצלחתם לגרום לו לעבוד בדפדפן! היו כמה דרכי מבוי סתום והיה צורך להדר את הכלי יותר מפעם אחת עד שהכלי עבד, אבל כפי שכתבתי למעלה, זה חלק מהחוויה. אם נתקעת, חשוב גם לזכור את התג webassembly
של StackOverflow. הידור שמח!
אישורים
המאמר הזה נבדק על ידי Sam Clegg ו-Rachel Andrew.