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

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

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

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

يمكنك استخدام mkbitmap من خلال ضبط عدد من الخيارات واسم ملف واحد أو عدة أسماء. للاطّلاع على جميع التفاصيل، يُرجى الاطّلاع على صفحة man الخاصة بالتطبيق:

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

الحصول على الشفرة‏

تتمثل الخطوة الأولى في الحصول على رمز المصدر الخاص بـ mkbitmap. ويمكنك العثور عليه على الموقع الإلكتروني للمشروع. في وقت كتابة هذه المقالة، كان potrace-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 نصّين برمجيَّين بسيطَين لضبط ملفات الإنشاء لاستخدام 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 وأنشئ ملفًا 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 وافتحه في المتصفّح. من المفترض أن تظهر لك رسالة تطلب منك إدخال معلومات. هذا ما كان متوقّعًا، لأنّه وفقًا لصفحة man الخاصة بالأدوات، "[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 object وواجهات برمجة التطبيقات الأخرى لنظام الملفات في الإخراج. من ناحية أخرى، إذا كان رمز 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 مسجّلاً في وحدة تحكّم DevTools، وتختفي المطالبة، لأنّه لم يعُد يتم استدعاء الدالة main() في mkbitmap عند البدء.

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

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

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

تطبيق mkbitmap مع شاشة بيضاء يعرض رقم إصدار mkbitmap المسجَّل في وحدة تحكّم DevTools

إعادة توجيه الإخراج العادي

يكون الإخراج العادي (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();

ملف &quot;الباحث&quot; (Finder) في نظام التشغيل macOS مع معاينة ملف 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 إذا واجهت مشكلة. مع أطيب التحيّات،

الشكر والتقدير

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