في المقالة ما هو WebAssembly وما هو مصدره؟، شرحتُ كيف وصلنا إلى WebAssembly اليوم. في هذه المقالة، سأعرض لك طريقتي في تجميع برنامج C حالي، mkbitmap
، إلى WebAssembly. وهو أكثر تعقيدًا من مثال hello world، إذ يتضمّن العمل مع الملفات والتواصل بين WebAssembly وJavaScript والرسم على لوحة الرسم، ولكنّه لا يزال بسيطًا بما يكفي لعدم إرباكك.
هذه المقالة موجّهة إلى مطوّري الويب الذين يريدون التعرّف على WebAssembly، وتوضّح بالتفصيل الخطوات التي يمكنك اتّخاذها إذا أردت تجميع رمز مثل mkbitmap
إلى WebAssembly. أودّ أن أنبّهك إلى أنّ عدم إمكانية تجميع تطبيق أو مكتبة في المرة الأولى أمر طبيعي تمامًا، ولهذا السبب لم تنجح بعض الخطوات الموضّحة أدناه، لذا كان عليّ التراجع وإعادة المحاولة بطريقة مختلفة. لا تعرض المقالة الأمر النهائي المجمّع السحري كما لو أنّه سقط من السماء، بل تصف تقدّمي الفعلي، بما في ذلك بعض الإحباطات.
معلومات حول mkbitmap
يقرأ برنامج mkbitmap
C صورة ويطبّق عليها عملية واحدة أو أكثر من العمليات التالية، بهذا الترتيب: العكس، والترشيح العالي التمرير، والتحجيم، والتحديد. يمكن التحكّم في كل عملية على حدة وتفعيلها أو إيقافها. ويُستخدم mkbitmap
بشكل أساسي لتحويل الصور الملوّنة أو الصور بتدرّج الرمادي إلى تنسيق مناسب كمدخل لبرامج أخرى، لا سيما برنامج التتبّع potrace
الذي يشكّل أساس SVGcode. باعتبارها أداة معالجة مسبقة، تكون mkbitmap
مفيدة بشكل خاص لتحويل الرسومات الخطية الممسوحة ضوئيًا، مثل الرسوم المتحركة أو النصوص المكتوبة بخط اليد، إلى صور ثنائية المستوى عالية الدقة.
يمكنك استخدام mkbitmap
من خلال تمرير عدد من الخيارات واسم ملف واحد أو أكثر. للاطّلاع على جميع التفاصيل، يُرجى الرجوع إلى صفحة الدليل الخاصة بالأداة:
$ mkbitmap [options] [filename...]


mkbitmap -f 2 -s 2 -t 0.48
(المصدر).الحصول على الشفرة
تتمثل الخطوة الأولى في الحصول على الرمز المصدري لـ mkbitmap
. يمكنك العثور عليه على الموقع الإلكتروني للمشروع. في وقت كتابة هذه المقالة، كان potrace-1.16.tar.gz هو أحدث إصدار.
تجميع التطبيق وتثبيته على الجهاز
الخطوة التالية هي تجميع الأداة وتثبيتها على جهازك للتعرّف على طريقة عملها. يحتوي ملف INSTALL
على التعليمات التالية:
cd
إلى الدليل الذي يحتوي على رمز المصدر للحزمة واكتب./configure
لضبط الحزمة لنظامك.قد يستغرق تشغيل
configure
بعض الوقت. أثناء التشغيل، تطبع بعض الرسائل التي توضّح الميزات التي تتحقّق منها.اكتب
make
لتجميع الحزمة.اختياريًا، اكتب
make check
لتشغيل أي اختبارات ذاتية مضمّنة في الحزمة، ويتم ذلك عادةً باستخدام الملفات الثنائية التي تم إنشاؤها للتو وغير المثبَّتة.اكتب
make install
لتثبيت البرامج وأي ملفات بيانات ومستندات. عند التثبيت في بادئة يملكها الجذر، يُنصح بتكوين الحزمة وإنشائها كمستخدم عادي، وتنفيذ مرحلة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 نصَين برمجيَين بسيطَين يضبطان ملفات makefile لاستخدام
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
يبدو أنّ آخر ملفين واعدان، لذا انتقِل إلى الدليل src/
باستخدام الأمر cd
. يتوفّر الآن أيضًا ملفان جديدان متطابقان، 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
وافتحه في المتصفّح. من المفترض أن تظهر لك رسالة تطلب منك إدخال معلومات. وهذا متوقّع، لأنّه وفقًا لصفحة الدليل الخاصة بالأداة، "إذا لم يتم تقديم وسيطات اسم الملف، ستعمل 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,
};
إنشاء بنية نموذجية باستخدام المزيد من علامات الإنشاء
لتوفير مدخلات للتطبيق، يمكنك استخدام ميزة نظام الملفات في Emscripten على Module.FS
. يذكر قسم تضمين توافق نظام الملفات في المستندات ما يلي:
يحدّد Emscripten ما إذا كان سيتم تضمين إمكانية استخدام نظام الملفات تلقائيًا. لا تحتاج العديد من البرامج إلى ملفات، كما أنّ دعم نظام الملفات ليس صغير الحجم، لذا يتجنّب Emscripten تضمينه عندما لا يجد سببًا لذلك. وهذا يعني أنّه إذا كان رمز C/C++ لا يصل إلى الملفات، فلن يتم تضمين العنصر
FS
وواجهات برمجة التطبيقات الأخرى لنظام الملفات في الناتج. من ناحية أخرى، إذا كان رمز 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
في وحدة تحكّم DevTools.
// 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 نظام ملفات افتراضيًا يحاكي نظام الملفات المحلي، ما يتيح تجميع الرمز البرمجي الأصلي الذي يستخدم واجهات برمجة تطبيقات الملفات المتزامنة وتشغيله بدون أي تغيير أو بتغييرات بسيطة.
لكي يقرأ 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 إذا واجهتك أي مشاكل. نتمنى لك تجربة ممتعة في تجميع الأغاني.
الإقرارات
راجع هذه المقالة سام كليغ وراشيل أندرو.