در WebAssembly چیست و از کجا آمده است؟ ، توضیح دادم که چگونه به WebAssembly امروز رسیدیم. در این مقاله، من روش خود را برای کامپایل کردن یک برنامه C موجود، mkbitmap
، به WebAssembly به شما نشان خواهم داد. از مثال hello world پیچیدهتر است، زیرا شامل کار با فایلها، برقراری ارتباط بین WebAssembly و جاوا اسکریپت و کشیدن روی بوم میشود، اما همچنان به اندازهای قابل مدیریت است که شما را تحت تأثیر قرار ندهد.
این مقاله برای توسعه دهندگان وب نوشته شده است که می خواهند WebAssembly را یاد بگیرند و گام به گام نشان می دهد که اگر می خواهید چیزی مانند mkbitmap
در WebAssembly کامپایل کنید چگونه می توانید ادامه دهید. به عنوان یک هشدار منصفانه، دریافت نکردن یک برنامه یا کتابخانه برای کامپایل در اولین اجرا کاملاً طبیعی است، به همین دلیل است که برخی از مراحل شرح داده شده در زیر کار نمیکنند، بنابراین من باید به عقب برگردم و دوباره امتحان کنم. این مقاله فرمان کامپایل نهایی جادویی را به گونهای نشان نمیدهد که انگار از آسمان افتاده است، بلکه پیشرفت واقعی من را توصیف میکند که شامل برخی ناامیدیها نیز میشود.
درباره mkbitmap
برنامه mkbitmap
C یک تصویر را می خواند و یک یا چند مورد از عملیات زیر را به ترتیب بر روی آن اعمال می کند: وارونگی، فیلتر بالاگذر، مقیاس گذاری و آستانه گذاری. هر عملیات را می توان به صورت جداگانه کنترل و روشن یا خاموش کرد. کاربرد اصلی 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 موارد زیر را بیان می کند:
ساخت پروژه های بزرگ با Emscripten بسیار آسان است. Emscripten دو اسکریپت ساده را ارائه میکند که فایلهای ساخت شما را به گونهای پیکربندی میکند که از
emcc
به عنوان جایگزینی برایgcc
استفاده کنند — در بیشتر موارد بقیه سیستم ساخت فعلی پروژه شما بدون تغییر باقی میماند.
سپس مستندات ادامه مییابد (برای اختصار کمی ویرایش شده است):
حالتی را در نظر بگیرید که معمولاً با دستورات زیر میسازید:
./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
ندارند کمی گیج کننده است، اما در واقع فایل های جاوا اسکریپت هستند که با یک 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)
با فراخوانی mv mkbitmap mkbitmap.js
(و در صورت تمایل به ترتیب mv potrace potrace.js
) نام فایل جاوا اسکریپت را به mkbitmap.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
ایجاد کنید که فایل جاوا اسکریپت 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 ابزار ، "[i]اگر هیچ آرگومان نام فایلی داده نشود، mkbitmap به عنوان یک فیلتر عمل می کند و از ورودی استاندارد می خواند" ، که برای Emscripten به طور پیش فرض یک prompt()
است.
جلوگیری از اجرای خودکار
برای توقف فوری اجرای mkbitmap
و در عوض منتظر ماندن آن برای ورودی کاربر، باید شی Module
Emscripten را درک کنید. Module
یک شی جاوا اسکریپت جهانی با ویژگی هایی است که کد تولید شده توسط 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
استفاده کنید. در بخش پشتیبانی از فایل سیستم در اسناد آمده است:
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
را مشاهده کنید که در کنسول DevTools ثبت شده است، و اعلان از بین می رود، زیرا تابع main()
mkbitmap
دیگر در ابتدا فراخوانی نمی شود.
تابع اصلی را به صورت دستی اجرا کنید
مرحله بعدی فراخوانی دستی تابع main()
mkbitmap
با اجرای Module.callMain()
است. تابع callMain()
آرایهای از آرگومانها را میگیرد که با آنچه در خط فرمان ارسال میکنید، یک به یک مطابقت دارند. اگر در خط فرمان mkbitmap -v
اجرا کنید، Module.callMain(['-v'])
در مرورگر فراخوانی کنید. این شماره نسخه mkbitmap
را در کنسول DevTools ثبت می کند.
// This is `script.js`.
import loadWASM from './mkbitmap.js';
const run = async () => {
const Module = await loadWASM();
Module.callMain(['-v']);
};
run();
خروجی استاندارد را تغییر مسیر دهید
خروجی استاندارد ( stdout
) به طور پیش فرض کنسول است. با این حال، می توانید آن را به چیز دیگری هدایت کنید، به عنوان مثال، تابعی که خروجی را در یک متغیر ذخیره می کند. این بدان معنی است که می توانید با تنظیم ویژگی Module.print
خروجی را به HTML اضافه کنید.
// 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 به خصوص سخت نیست، بنابراین با استفاده از کد جاوا اسکریپت ، حتی می توانید پیش نمایش تصویر خروجی را نشان دهید. کد منبع نسخه ی نمایشی تعبیه شده را برای یکی از راه های انجام این کار در زیر ببینید.
نتیجه گیری
تبریک می گوییم، شما با موفقیت mkbitmap
در WebAssembly کامپایل کردید و آن را در مرورگر کار کردید! بن بست هایی وجود داشت و شما مجبور بودید بیش از یک بار ابزار را کامپایل کنید تا زمانی که کار کند، اما همانطور که در بالا نوشتم، این بخشی از تجربه است. همچنین اگر گیر کردید ، تگ webassembly
StackOverflow را به خاطر بسپارید. تدوین مبارک!
قدردانی ها
این مقاله توسط سام کلگ و ریچل اندرو بررسی شده است.