في القسم ما هو WebAssembly ومن أين أتى؟، أوضحت كيف انتهى بنا الأمر إلى WebAssembly اليوم. في هذه المقالة، سأوضح لك أسلوبي في تجميع برنامج C حالي، mkbitmap
، باستخدام WebAssembly. وهو أكثر تعقيدًا من مثال hello world، فهو يتضمّن العمل على الملفات والتواصل بين أرض WebAssembly وJavaScript والرسم على لوحة رسم، ولكن لا يزال من الممكن إدارته بما يكفي لعدم إرباكك.
هذه المقالة مكتوبة لمطوّري البرامج على الويب الذين يريدون تعلُّم WebAssembly، وتعرض خطوة بخطوة كيفية المتابعة إذا أردت تجميع ما مثل mkbitmap
في WebAssembly. إنّ عدم تجميع تطبيق أو مكتبة عند التشغيل لأول مرة أمر طبيعي تمامًا، وهذا هو سبب عدم نجاح بعض الخطوات الموضّحة أدناه، لذلك اضطررتُ إلى التراجع عن العملية وإعادة المحاولة بشكل مختلف. لا تعرض المقالة أمر التجميع النهائي السحري كما لو كان قد سقط من السماء، بل تصف تقدمي الفعلي، بما في ذلك بعض الأمور المحبطة.
معلومات حول mkbitmap
يقرأ برنامج mkbitmap
C الصورة ويطبّق عليها عملية واحدة أو أكثر من العمليات التالية، بهذا الترتيب: العكس، وفلترة التمريرات العالية، والتحجيم، ووضع الحدود. يمكن التحكّم في كل عملية وتفعيلها أو إيقافها بشكل فردي. ويتمثل الاستخدام الرئيسي لـ mkbitmap
في تحويل صور الألوان أو التدرج الرمادي إلى تنسيق مناسب كإدخال لبرامج أخرى، لا سيما برنامج التتبُّع potrace
الذي يشكّل أساس SVGcode. mkbitmap
هو أداة للمعالجة المسبقة، وهو مفيد بشكل خاص في تحويل الرسومات الخطية التي تم مسحها ضوئيًا، مثل الرسوم المتحركة أو النصوص المكتوبة بخط اليد، إلى صور عالية الدقة ثنائية المستوى.
يمكنك استخدام mkbitmap
من خلال تمرير عدد من الخيارات واسم ملف واحد أو عدة ملفات. للحصول على جميع التفاصيل، يُرجى الاطّلاع على صفحة الدليل للأداة:
$ mkbitmap [options] [filename...]
الحصول على الرمز
الخطوة الأولى هي الحصول على رمز المصدر mkbitmap
. يمكنك العثور عليه على الموقع الإلكتروني للمشروع. في وقت كتابة هذا التقرير، كان الإصدار putrace-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 نصَّين برمجيَّين بسيطَين لإعداد ملفات makefiles لاستخدام
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
هي أمر محيّر بعض الشيء، لكنّها في الواقع ملفات 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]إذا لم يتم تقديم أي وسيطات لاسم الملف، تعمل خريطة 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
. ينص قسم تضمين دعم نظام الملفات في المستندات على ما يلي:
تقرّر 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
في وحدة تحكّم "أدوات مطوّري البرامج".
// 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 إذا واجهتك مشكلة. استمتع بتجميع المحتوى الموسيقي!
شكر وتقدير
راجع هذه المقالة سام كليغ وراشيل أندرو.