במאמר מהו WebAssembly ומאיפה הוא הגיע? הסברתי איך הגענו ל-WebAssembly של היום. במאמר הזה אציג את הגישה שלי ל-compilation של תוכנית C קיימת, mkbitmap
, ל-WebAssembly. הדוגמה הזו מורכבת יותר מהדוגמה של hello world, כי היא כוללת עבודה עם קבצים, תקשורת בין עולמות WebAssembly ו-JavaScript ורישום על לוח, אבל היא עדיין ניתנת לניהול ולא תהיה מרתיעה.
המאמר נכתב למפתחי אתרים שרוצים ללמוד על WebAssembly ולראות איך אפשר להתקדם אם רוצים להדר קובץ כמו mkbitmap
ל-WebAssembly. חשוב לדעת: לא תמיד אפשר לקמפל אפליקציה או ספרייה בפעם הראשונה. לכן, חלק מהשלבים שמפורטים בהמשך לא עבדו, ולכן נאלצתי לחזור אחורה ולנסות שוב בצורה שונה. בכתבה לא מופיעה פקודה קסומה סופית של הידור, כאילו היא נפלה מהשמיים, אלא מתוארת ההתקדמות בפועל שלי, כולל כמה תסכולים.
מידע על mkbitmap
תוכנית ה-C mkbitmap
קוראת תמונה ומחילה עליה אחת או יותר מהפעולות הבאות, לפי הסדר הזה: היפוך, סינון מסנן מסנן גבוה, שינוי קנה מידה וערך סף. אפשר לשלוט בכל פעולה בנפרד ולהפעיל או להשבית אותה. השימוש העיקרי ב-mkbitmap
הוא להמיר תמונות צבעוניות או בגווני אפור לפורמט שמתאים כקלט לתוכנות אחרות, במיוחד לתוכנת המעקב potrace
שמהווה את הבסיס של SVGcode. ככלי לעיבוד מראש, mkbitmap
שימושי במיוחד להמרת אמנות קו סרוקה, כמו סרטים מצוירים או טקסט בכתב יד, לתמונות דו-שלביות ברזולוציה גבוהה.
כדי להשתמש בקובץ mkbitmap
, מעבירים אליו כמה אפשרויות ושם קובץ אחד או יותר. כל הפרטים מפורטים בדף העזרה של הכלי:
$ mkbitmap [options] [filename...]
קבל את הקוד
השלב הראשון הוא לקבל את קוד המקור של mkbitmap
. אפשר למצוא אותו באתר של הפרויקט. נכון לעכשיו, הגרסה העדכנית ביותר היא otrace-1.16.tar.gz.
הידור והתקנה באופן מקומי
השלב הבא הוא להדר ולהתקין את הכלי באופן מקומי כדי לקבל מושג איך הוא פועל. הקובץ INSTALL
מכיל את ההוראות הבאות:
cd
לתיקייה שמכילה את קוד המקור של החבילה, ומקלידים./configure
כדי להגדיר את החבילה למערכת.הפעלת
configure
עשויה להימשך זמן מה. במהלך ההרצה, התוכנית מדפיסה כמה הודעות שמציינות אילו תכונות היא בודקת.מקלידים
make
כדי לקמפל את החבילה.אפשר גם להקליד
make check
כדי להריץ את כל הבדיקות העצמיות שמגיעות עם החבילה, בדרך כלל באמצעות קובצי ה-binary שזה עתה נוצרו ולא הותקנו.מקלידים
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. במסמך Building Projects של Emscripten כתוב:
קל מאוד לבנות פרויקטים גדולים באמצעות Emscripten. ב-Emscripten יש שני סקריפטים פשוטים שמגדירים את קובצי ה-makefiles כך שישתמשו ב-
emcc
כתחליף ל-gcc
. ברוב המקרים, שאר מערכת ה-build הנוכחית של הפרויקט לא משתנה.
לאחר מכן התיעוד ממשיך (מעט נערך כדי לקצר):
נניח שבדרך כלל אתם מבצעים build באמצעות הפקודות הבאות:
./configure
make
כדי לבצע build באמצעות 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 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
. בקטע הכללת תמיכה במערכת קבצים במסמכי התיעוד כתוב:
המערכת של 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 למיפוי סיביות ב-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.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.