في بعض الأحيان، قد تحتاج إلى استخدام مكتبة متوفّرة فقط كرمز C أو C++. في العادة، هذه هي المرحلة التي تستسلم فيها. لم يعُد الأمر كذلك، لأنّنا نملك الآن Emscripten وWebAssembly (أو Wasm).
سلسلة الأدوات
لقد وضعت لنفسي هدفًا يتمثّل في معرفة كيفية تجميع بعض رموز C الحالية إلى Wasm. لقد أثيرت بعض الضجة حول الواجهة الخلفية لـ Wasm في LLVM، لذا بدأت في البحث في ذلك. صحيح أنّه يمكنك تجميع البرامج البسيطة بهذه الطريقة، ولكن ستواجهك مشاكل على الأرجح عند استخدام مكتبة C العادية أو حتى عند تجميع ملفات متعددة. قادني هذا إلى الدرس الرئيسي الذي تعلمته:
على الرغم من أنّ Emscripten كان برنامج تجميع من C إلى asm.js، فقد تطوّر منذ ذلك الحين لاستهداف Wasm وهو في طور التبديل إلى الواجهة الخلفية الرسمية لـ LLVM داخليًا. يوفّر Emscripten أيضًا تنفيذًا متوافقًا مع Wasm لمكتبة C العادية. استخدام Emscripten فهي تتضمّن الكثير من العمل المخفي، وتحاكي نظام الملفات، وتوفّر إدارة الذاكرة، وتغلّف OpenGL باستخدام WebGL، والكثير من الأشياء التي لا تحتاج حقًا إلى تجربتها بنفسك.
قد يبدو أنّ عليك القلق بشأن التضخم، وقد كنت قلقًا بالتأكيد، ولكن برنامج التجميع Emscripten يزيل كل ما هو غير ضروري. في تجاربي، تكون وحدات Wasm الناتجة ذات حجم مناسب للمنطق الذي تحتويه، وتعمل فِرق Emscripten وWebAssembly على جعلها أصغر حجمًا في المستقبل.
يمكنك الحصول على Emscripten باتّباع التعليمات الواردة على موقعه الإلكتروني أو باستخدام Homebrew. إذا كنت من محبي أوامر Docker مثلي ولا تريد تثبيت أي شيء على نظامك لمجرد تجربة WebAssembly، فهناك صورة Docker تتم صيانتها جيدًا ويمكنك استخدامها بدلاً من ذلك:
$ docker pull trzeci/emscripten
$ docker run --rm -v $(pwd):/src trzeci/emscripten emcc <emcc options here>
تجميع شيء بسيط
لنأخذ المثال الكلاسيكي لكتابة دالة بلغة C تحسب رقم فيبوناتشي th:
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
if(n <= 0){
return 0;
}
int i, t, a = 0, b = 1;
for (i = 1; i < n; i++) {
t = a + b;
a = b;
b = t;
}
return b;
}
إذا كنت تعرف لغة C، فلن تكون الدالة نفسها مفاجئة للغاية. حتى لو لم تكن تعرف لغة C ولكنك تعرف JavaScript، فستتمكن من فهم ما يحدث هنا.
emscripten.h هو ملف عنوان يوفّره Emscripten. نحتاج إليها فقط لنتمكّن من الوصول إلى ماكرو EMSCRIPTEN_KEEPALIVE، ولكنّها توفّر وظائف أكثر بكثير.
يطلب هذا الماكرو من برنامج التجميع عدم إزالة دالة حتى إذا كانت تبدو
غير مستخدَمة. إذا حذفنا هذا الماكرو، فسيقوم برنامج التجميع بتحسين الدالة بعيدًا
— لا أحد يستخدمها بعد كل شيء.
لنحفظ كل ذلك في ملف باسم fib.c. لتحويله إلى ملف .wasm،
علينا استخدام أمر برنامج التجميع emcc في Emscripten:
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' fib.c
لنحلّل هذا الأمر. emcc هو برنامج تجميع Emscripten. fib.c هو ملف C. كل شيء على ما يرام حتى الآن. يطلب -s WASM=1 من Emscripten تزويدنا بملف Wasm
بدلاً من ملف asm.js.
-s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' يخبر برنامج التجميع بترك الدالة
cwrap() متاحة في ملف JavaScript، وسنتحدث عن هذه الدالة
لاحقًا. يطلب -O3 من برنامج التجميع إجراء تحسينات بشكل كبير. يمكنك اختيار أرقام أقل لتقليل وقت الإنشاء، ولكن ذلك سيؤدي أيضًا إلى زيادة حجم الحِزم الناتجة لأنّ برنامج التجميع قد لا يزيل الرموز البرمجية غير المستخدَمة.
بعد تشغيل الأمر، من المفترض أن تحصل على ملف JavaScript باسم
a.out.js وملف WebAssembly باسم a.out.wasm. يحتوي ملف Wasm (أو "الوحدة") على رمز C المجمَّع ويجب أن يكون صغيرًا إلى حدّ ما. يتولّى ملف JavaScript تحميل وحدة Wasm وإعدادها وتوفير واجهة برمجة تطبيقات أفضل. وإذا لزم الأمر، سيتولى أيضًا إعداد
المكدس والكومة والوظائف الأخرى التي يُتوقع عادةً أن يوفرها
نظام التشغيل عند كتابة كود C. وبالتالي، يكون حجم ملف JavaScript أكبر قليلاً،
إذ يبلغ 19 كيلوبايت (حوالي 5 كيلوبايت بتنسيق gzip).
تشغيل شيء بسيط
أسهل طريقة لتحميل الوحدة وتشغيلها هي استخدام ملف JavaScript الذي تم إنشاؤه. بعد تحميل هذا الملف، سيكون لديك
Module عمومي
تحت تصرّفك. استخدِم
cwrap
لإنشاء دالة JavaScript أصلية تتولى تحويل المَعلمات
إلى شيء متوافق مع لغة C واستدعاء الدالة المغلّفة. تأخذ cwrap اسم الدالة ونوع القيمة التي تم إرجاعها وأنواع الوسيطات كوسيطات، بهذا الترتيب:
<script src="a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const fib = Module.cwrap('fib', 'number', ['number']);
console.log(fib(12));
};
</script>
إذا شغّلت هذا الرمز، من المفترض أن يظهر لك الرقم "144" في وحدة التحكّم، وهو العدد الثاني عشر في "متتالية فيبوناتشي".
الهدف الأسمى: تجميع مكتبة C
حتى الآن، تمت كتابة رمز C الذي كتبناه مع وضع Wasm في الاعتبار. ومع ذلك، فإنّ حالة الاستخدام الأساسية لـ WebAssembly هي الاستفادة من المنظومة المتكاملة الحالية لمكتبات C والسماح للمطوّرين باستخدامها على الويب. تعتمد هذه المكتبات غالبًا على مكتبة C العادية ونظام التشغيل ونظام الملفات وأشياء أخرى. يوفّر Emscripten معظم هذه الميزات، على الرغم من وجود بعض القيود.
لنرجع إلى هدفي الأصلي: تجميع برنامج ترميز لـ WebP إلى Wasm. تمت كتابة المصدر لبرنامج ترميز WebP بلغة C وهو متاح على GitHub بالإضافة إلى بعض مستندات واجهة برمجة التطبيقات الشاملة. هذه نقطة بداية جيدة.
$ git clone https://github.com/webmproject/libwebp
للبدء ببساطة، لنحاول عرض WebPGetEncoderVersion() من
encode.h إلى JavaScript من خلال كتابة ملف C باسم webp.c:
#include "emscripten.h"
#include "src/webp/encode.h"
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetEncoderVersion();
}
هذا برنامج بسيط وجيد لاختبار ما إذا كان بإمكاننا الحصول على الرمز المصدر لـ libwebp لتجميعه، لأننا لا نحتاج إلى أي معلمات أو هياكل بيانات معقدة لاستدعاء هذه الدالة.
لتجميع هذا البرنامج، علينا إخبار برنامج التجميع بمكان العثور على
ملفات عناوين libwebp باستخدام العلامة -I، وعلينا أيضًا تمرير جميع ملفات C الخاصة بـ
libwebp التي يحتاجها. سأكون صريحًا: لقد أعطيتُه جميع ملفات C التي تمكّنتُ من العثور عليها واعتمدتُ على برنامج التجميع لإزالة كل ما هو غير ضروري. ويبدو أنّها نجحت بشكل رائع.
$ emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
-I libwebp \
webp.c \
libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c
الآن، نحتاج فقط إلى بعض رموز HTML وJavaScript لتحميل الوحدة الجديدة الرائعة:
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = async (_) => {
const api = {
version: Module.cwrap('version', 'number', []),
};
console.log(api.version());
};
</script>
وسيظهر رقم الإصدار الصحيح في الناتج:

الحصول على صورة من JavaScript إلى Wasm
إنّ الحصول على رقم إصدار برنامج الترميز هو أمر رائع، ولكن ترميز صورة فعلية سيكون أكثر إثارة للإعجاب، أليس كذلك؟ لنفعل ذلك إذًا.
السؤال الأول الذي يجب الإجابة عنه هو: كيف يمكننا نقل الصورة إلى Wasm؟
بالنظر إلى واجهة برمجة التطبيقات لترميز libwebp، فإنّها تتوقع مصفوفة من وحدات البايت في RGB أو RGBA أو BGR أو BGRA. لحسن الحظ، تتضمّن واجهة برمجة التطبيقات Canvas API
getImageData()،
التي تمنحنا
Uint8ClampedArray
يحتوي على بيانات الصورة في RGBA:
async function loadImage(src) {
// Load image
const imgBlob = await fetch(src).then((resp) => resp.blob());
const img = await createImageBitmap(imgBlob);
// Make canvas same size as image
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
// Draw image onto canvas
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, img.width, img.height);
}
الآن، ما عليك سوى نسخ البيانات من JavaScript إلى Wasm. لذلك، نحتاج إلى عرض دالتَين إضافيتَين. أحدهما يخصّص الذاكرة للصورة داخل Wasm والآخر يحرّرها مرة أخرى:
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
return malloc(width * height * 4 * sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
free(p);
}
يخصّص create_buffer مخزنًا مؤقتًا لصورة RGBA، أي 4 بايت لكل بكسل.
المؤشر الذي يعرضه malloc() هو عنوان خلية الذاكرة الأولى في
هذا المخزن المؤقت. عندما يعود المؤشر إلى JavaScript، يتم التعامل معه على أنّه مجرد رقم. بعد عرض الدالة في JavaScript باستخدام cwrap، يمكننا
استخدام هذا الرقم للعثور على بداية المخزن المؤقت ونسخ بيانات الصورة.
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
const image = await loadImage('/image.jpg');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
// ... call encoder ...
api.destroy_buffer(p);
النهائي الكبير: ترميز الصورة
أصبحت الصورة متاحة الآن في Wasm. حان الوقت لاستدعاء برنامج ترميز WebP
لأداء مهمته. بالاطّلاع على
مستندات WebP، يبدو أنّ WebPEncodeRGBA
هو الخيار الأنسب. تأخذ الدالة مؤشرًا إلى الصورة المدخلة وأبعادها، بالإضافة إلى خيار جودة بين 0 و100. وتخصّص أيضًا مخزنًا مؤقتًا للإخراج، والذي نحتاج إلى إفراغه باستخدام WebPFree() بعد الانتهاء من صورة WebP.
نتيجة عملية الترميز هي مخزن مؤقت للإخراج وطوله. بما أنّ الدوال في لغة C لا يمكن أن تتضمّن مصفوفات كنوع القيمة التي تم إرجاعها (ما لم نخصّص الذاكرة بشكل ديناميكي)، لجأتُ إلى مصفوفة عامة ثابتة. أعلم أنّ لغة C ليست نظيفة (في الواقع، تعتمد على حقيقة أنّ مؤشرات Wasm بعرض 32 بت)، ولكن لإبقاء الأمور بسيطة، أعتقد أنّ هذا اختصار عادل.
int result[2];
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* img_in, int width, int height, float quality) {
uint8_t* img_out;
size_t size;
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
result[0] = (int)img_out;
result[1] = size;
}
EMSCRIPTEN_KEEPALIVE
void free_result(uint8_t* result) {
WebPFree(result);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
return result[0];
}
EMSCRIPTEN_KEEPALIVE
int get_result_size() {
return result[1];
}
بعد الانتهاء من كل ذلك، يمكننا استدعاء دالة الترميز، والحصول على المؤشر وحجم الصورة، ووضعهما في مخزن JavaScript المؤقت الخاص بنا، وتحرير جميع مخازن Wasm المؤقتة التي خصصناها في هذه العملية.
api.encode(p, image.width, image.height, 100);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result(resultPointer);
استنادًا إلى حجم الصورة، قد يظهر لك خطأ يشير إلى أنّ Wasm لا يمكنه زيادة الذاكرة بما يكفي لاستيعاب كل من الصورة المدخلة والصورة الناتجة:

لحسن الحظ، حل هذه المشكلة موجود في رسالة الخطأ! نحتاج فقط إلى إضافة -s ALLOW_MEMORY_GROWTH=1 إلى أمر التجميع.
وإلى هنا، فقد تحققت رغبتك! لقد جمّعنا برنامج ترميز WebP وحوّلنا ترميز صورة JPEG إلى
WebP. لإثبات نجاح العملية، يمكننا تحويل مخزن النتائج المؤقت إلى كائن ثنائي كبير واستخدامه في عنصر <img>:
const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = blobURL;
document.body.appendChild(img);

الخاتمة
ليس من السهل تشغيل مكتبة C في المتصفّح، ولكن بمجرد فهم العملية بشكل عام وكيفية تدفق البيانات، يصبح الأمر أسهل ويمكن أن تكون النتائج مذهلة.
يفتح WebAssembly العديد من الإمكانيات الجديدة على الويب للمعالجة والأرقام الأزمة والألعاب. يُرجى العِلم أنّ Wasm ليس حلاً سحريًا يجب تطبيقه على كل شيء، ولكن عندما تواجه إحدى هذه المشاكل، يمكن أن يكون Wasm أداة مفيدة للغاية.
محتوى إضافي: تشغيل شيء بسيط بالطريقة الصعبة
إذا أردت تجنُّب ملف JavaScript الذي تم إنشاؤه، قد تتمكّن من ذلك. لنعد إلى مثال فيبوناتشي. لتحميلها وتشغيلها بأنفسنا، يمكننا القيام بما يلي:
<!DOCTYPE html>
<script>
(async function () {
const imports = {
env: {
memory: new WebAssembly.Memory({ initial: 1 }),
STACKTOP: 0,
},
};
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/a.out.wasm'),
imports,
);
console.log(instance.exports._fib(12));
})();
</script>
لا تتوفّر لوحدات WebAssembly التي تم إنشاؤها بواسطة Emscripten ذاكرة للعمل
إلا إذا وفّرت لها ذاكرة. يمكنك تزويد وحدة Wasm
بأي شيء باستخدام الكائن imports، وهو المَعلمة الثانية للدالة
instantiateStreaming. يمكن لوحدة Wasm الوصول إلى كل شيء داخل
كائن الاستيراد، ولكن لا شيء آخر خارجه. وفقًا للعرف، تتوقع الوحدات التي تم تجميعها بواسطة Emscripting بعض الأشياء من بيئة JavaScript التي يتم تحميلها:
- أولاً، هناك
env.memory. لا تدرك وحدة Wasm العالم الخارجي، إذا جاز التعبير، لذا فهي بحاجة إلى الحصول على بعض الذاكرة للعمل معها. أدخِلWebAssembly.Memory. وهي تمثّل جزءًا من الذاكرة الخطية (يمكن توسيعه اختياريًا). تكون مَعلمات تحديد الحجم "بوحدات صفحات WebAssembly"، ما يعني أنّ الرمز أعلاه يخصّص صفحة واحدة من الذاكرة، ويبلغ حجم كل صفحة 64 KiB. بدون توفير خيارmaximum، لن يكون هناك حدّ أقصى نظريًا لنمو الذاكرة (يفرض Chrome حاليًا حدًّا أقصى يبلغ 2 غيغابايت). لا تحتاج معظم وحدات WebAssembly إلى ضبط حد أقصى. - يحدّد
env.STACKTOPالمكان الذي من المفترض أن يبدأ فيه نمو الحزمة. يُعدّ المكدّس ضروريًا لإجراء استدعاءات الدوال وتخصيص الذاكرة للمتغيرات المحلية. بما أنّنا لا نستخدم أي حيل لإدارة الذاكرة الديناميكية في برنامج "فيبوناتشي" الصغير، يمكننا استخدام الذاكرة بأكملها كمكدّس، وبالتاليSTACKTOP = 0.