تحويل ملف mkbitmap إلى WebAssembly

في مقالة ما هي WebAssembly ومن أين أتت؟، ثم أوضحت كيف انتهى بنا المطاف إلى استخدام WebAssembly في الوقت الحالي. في هذه المقالة، سأوضح لك نهجي في تجميع برنامج C حالي، mkbitmap، مع WebAssembly. وهو أمر أكثر تعقيدًا من مثال hello world، لأنه يشمل العمل على الملفات والتواصل بين أرضيّ WebAssembly وJavaScript والرسم على لوحة رسم، ولكنه يبقى قابلاً للإدارة بما يكفي لإرباكك.

هذه المقالة مكتوبة للمطوّرين على الويب الذين يريدون تعلُّم استخدام WebAssembly، وهي تعرض بالتفصيل الطريقة التي يمكنك اتّباعها إذا أردت تجميع شيء مثل mkbitmap في WebAssembly. كتحذير عادل، من الطبيعي عدم الحصول على تطبيق أو مكتبة لتجميعها عند التشغيل الأول، لهذا السبب لم تعمل بعض الخطوات الموضحة أدناه، لذلك اضطررتُ إلى التراجع وإعادة المحاولة بشكل مختلف. لا تعرض المقالة الأمر السحري للتجميع النهائي كما لو هبط من السماء، بل تصف التقدم الفعلي الذي أحرزته، وشملت بعض الاستياءات التي واجهها.

معلومات حول mkbitmap

يقرأ برنامج C في mkbitmap الصورة ويطبِّق واحدة أو أكثر من العمليات التالية عليها بالترتيب: العكس وفلترة المرور المرتفع والتحجيم والضبط على الحدّ. يمكن التحكّم في كل عملية وتفعيلها أو إيقافها بشكل فردي. يتمثل الاستخدام الأساسي للرمز mkbitmap في تحويل الصور الملوَّنة أو الصور بتدرج الرمادي إلى تنسيق مناسب كإدخال للبرامج الأخرى، لا سيما برنامج التتبُّع potrace الذي يشكّل أساس SVGcode. mkbitmap مفيد بشكل خاص لتحويل الرسومات الخطية الممسوحة ضوئيًا، مثل الرسوم المتحركة أو النصوص المكتوبة بخط اليد، إلى صور عالية الدقة ثنائية المستوى، وذلك بصفتها أداة معالجة مسبقة.

ويمكنك استخدام mkbitmap من خلال تمرير عدد من الخيارات واسم ملف واحد أو عدة أسماء. للحصول على جميع التفاصيل، راجع صفحة إدارة الأداة:

$ mkbitmap [options] [filename...]
صورة كرتونية ملوّنة
الصورة الأصلية (المصدر).
تم تحويل الصورة الكرتونية إلى تدرج الرمادي بعد المعالجة المسبقة.
تم تعديل المحتوى لأوّل مرة، ثم تم بلوغ الحد الأدنى: mkbitmap -f 2 -s 2 -t 0.48 (المصدر).

الحصول على الرمز‏

الخطوة الأولى هي الحصول على رمز المصدر mkbitmap. يمكنك العثور عليه على الموقع الإلكتروني للمشروع. في وقت كتابة هذا التقرير، كان botrace-1.16.tar.gz هو الإصدار الأحدث.

التجميع والتثبيت على الجهاز

الخطوة التالية هي تجميع الأداة وتثبيتها محليًا للتعرف على سلوكها. يحتوي ملف INSTALL على التعليمات التالية:

  1. cd إلى الدليل الذي يحتوي على رمز مصدر الحزمة والنوع ./configure لإعداد الحزمة لنظامك.

    قد يستغرق تشغيل "configure" بعض الوقت. أثناء التشغيل، تطبع بعض الرسائل لإعلامها بالميزات التي تبحث عنها.

  2. اكتب make لتجميع الحزمة.

  3. يمكنك، إذا أردت، كتابة make check لإجراء أي اختبارات ذاتية مضمّنة في الحزمة، وذلك عادةً باستخدام البرامج الثنائية التي تم إلغاء تثبيتها والتي تم إنشاؤها للتو.

  4. اكتب 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.jsmv 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 يعرض طلبًا يطلب إدخال البيانات

منع التنفيذ التلقائي

لإيقاف تنفيذ 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. قد تكون العلامات التالية مفيدة للمشروعات الأخرى أيضًا.

في هذه الحالة على وجه التحديد، عليك ضبط علامة --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 لم تعُد مطلوبة في البداية.

تطبيق mkbitmap بشاشة بيضاء ويعرض عنصر &quot;الوحدة&quot; الذي تم تسجيله في وحدة التحكّم في &quot;أدوات مطوّري البرامج&quot;

تنفيذ الدالة الرئيسية يدويًا

الخطوة التالية هي استدعاء دالة 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();

تطبيق mkbitmap بشاشة بيضاء ويعرض رقم إصدار الخريطة المصغّرة الذي تم تسجيله في وحدة التحكّم في أدوات مطوّري البرامج

إعادة توجيه الناتج العادي

يكون الناتج العادي (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 يعرض رقم إصدار mkbitmap.

إدخال ملف الإدخال في نظام ملفات الذاكرة

لإدخال ملف الإدخال في نظام ملفات الذاكرة، ستحتاج إلى ما يعادل 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 يعرض مصفوفة من الملفات في نظام ملفات الذاكرة، بما في ذلك example.bmp.

أول عملية تنفيذ فعلية

بعد تنفيذ كل شيء في مكانه الصحيح، يمكنك تنفيذ 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();

تطبيق mkbitmap يعرض مصفوفة من الملفات في نظام ملفات الذاكرة، بما في ذلك example.bmp وexample.pbm.

إخراج ملف الإخراج من نظام ملفات الذاكرة

تتيح الوظيفة 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();

تطبيق macOS Finder مع معاينة لملف الإدخال bmp وملف المخرجات pbm.

إضافة واجهة مستخدم تفاعلية

إلى هذه النقطة، يكون ملف الإدخال ترميزًا ثابتًا ويعمل 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 إذا واجهتك مشكلة. استمتع بالتجميع!

شكر وتقدير

تمت مراجعة هذه المقالة من قِبل سام كليج وراشيل أندرو.