على الرغم من أنّ JavaScript لا تُلقي بالاً كثيرًا لتنظيف ما بعد تنفيذها، فإنّ اللغات الثابتة لا تفعل ذلك بالتأكيد.
Squoosh.app هو تطبيق متوافق مع الأجهزة الجوّالة يعمل على الإنترنت يوضّح مدى تأثير برامج ترميز الصور المختلفة والإعدادات المختلفة في تحسين حجم ملف الصورة بدون التأثير بشكل كبير في الجودة. ومع ذلك، فهو أيضًا عرض تقني يعرض كيفية استخدام المكتبات المكتوبة بلغة C++ أو Rust وعرضها على الويب.
إنّ إمكانية نقل الرموز البرمجية من الأنظمة المتكاملة الحالية أمرٌ قيّم للغاية، ولكن هناك بعض الاختلافات العميقة بين هذه اللغات الثابتة وJavaScript. ويتمثل أحد هذه الاختلافات في اتّباعهما أسلوبَين مختلفَين لإدارة الذاكرة.
على الرغم من أنّ JavaScript لا تُهمل تنظيف المحتوى بعد الانتهاء من استخدامه، فإنّ هذه اللغات الثابتة لا تفعل ذلك. عليك طلب ذاكرة جديدة مخصّصة بشكل صريح، ويجب التأكّد من إعادتها بعد ذلك وعدم استخدامها مرة أخرى. وفي حال عدم حدوث ذلك، ستظهر لك تسريبات… ويحدث ذلك بانتظام. لنلقِ نظرة على كيفية تصحيح أخطاء تسرب الذاكرة، و الأفضل من ذلك، كيفية تصميم الرمز البرمجي لتجنُّب حدوثها في المرة القادمة.
نمط مريب
مؤخرًا، أثناء بدء العمل على Squoosh، لاحظتُ نمطًا مثيرًا للاهتمام في برامج ترميز C++. لنلقِ نظرة على حزمة ImageQuant كمثال (تم تصغيرها لعرض أجزاء إنشاء الكائنات وإلغاء تخصيصها فقط):
liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;
RawImage quantize(std::string rawimage,
int image_width,
int image_height,
int num_colors,
float dithering) {
const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
int size = image_width * image_height;
attr = liq_attr_create();
image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_set_max_colors(attr, num_colors);
liq_image_quantize(image, attr, &res);
liq_set_dithering_level(res, dithering);
uint8_t* image8bit = (uint8_t*)malloc(size);
result = (uint8_t*)malloc(size * 4);
// …
free(image8bit);
liq_result_destroy(res);
liq_image_destroy(image);
liq_attr_destroy(attr);
return {
val(typed_memory_view(image_width * image_height * 4, result)),
image_width,
image_height
};
}
void free_result() {
free(result);
}
JavaScript (أو TypeScript):
export async function process(data: ImageData, opts: QuantizeOptions) {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
}
const module = await emscriptenModule;
const result = module.quantize(/* … */);
module.free_result();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}
هل لاحظت مشكلة؟ تلميح: يشير ذلك إلى استخدام الذاكرة بعد تحريرها، ولكن في JavaScript.
في Emscripten، تُعرِض typed_memory_view
Uint8Array
JavaScript مدعومًا بوحدة تخزين ذاكرة WebAssembly (Wasm)، مع ضبط byteOffset
وbyteLength
على المؤشر والطول المحدَّدَين. النقطة الأساسية
هي أنّ هذا عرض TypedArray في ذاكرة تخزين مؤقتة في WebAssembly، وليس
نسخة مملوكة من JavaScript للبيانات.
عندما نستدعي free_result
من JavaScript، تستدعي بدورها دالة C عادية free
لتمييز
هذه الذاكرة على أنّها متاحة لأي عمليات تخصيص مستقبلية، ما يعني أنّ البيانات التي تشير إليها Uint8Array
يمكن استبدالها ببيانات عشوائية من خلال أيّ استدعاء مستقبلي لواسم.
أو قد تقرّر بعض عمليات تنفيذ free
ملء الذاكرة المُستغَلة حديثًا بقيمة صفرية على الفور. لا ينفّذ الرمز البرمجي
free
الذي يستخدمه Emscripten ذلك، ولكننا نعتمد على تفاصيل تنفيذ
لا يمكن ضمانها.
أو حتى إذا تم الاحتفاظ بالذاكرة التي يشير إليها المؤشر، قد تحتاج عملية التخصيص الجديدة إلى زيادة
ذاكرة WebAssembly. عند زيادة WebAssembly.Memory
إما من خلال JavaScript API أو التعليمات المناسبة
memory.grow
، يتم إلغاء صلاحية ArrayBuffer
الحالية، وبالتالي أيّ مشاهد
تستند إليها.
سأستخدم وحدة تحكّم أدوات المطوّرين (أو Node.js) لشرح هذا السلوك:
> memory = new WebAssembly.Memory({ initial: 1 })
Memory {}
> view = new Uint8Array(memory.buffer, 42, 10)
Uint8Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// ^ all good, we got a 10 bytes long view at address 42
> view.buffer
ArrayBuffer(65536) {}
// ^ its buffer is the same as the one used for WebAssembly memory
// (the size of the buffer is 1 WebAssembly "page" == 64KB)
> memory.grow(1)
1
// ^ let's say we grow Wasm memory by +1 page to fit some new data
> view
Uint8Array []
// ^ our original view is no longer valid and looks empty!
> view.buffer
ArrayBuffer(0) {}
// ^ its buffer got invalidated as well and turned into an empty one
أخيرًا، حتى إذا لم نطلب استخدام Wasm بشكل صريح مرة أخرى بين free_result
وnew
Uint8ClampedArray
، قد نضيف في مرحلة ما ميزة دعم تعدد المواضيع إلى برامج الترميز لدينا. في هذه الحالة،
قد يكون هناك سلسلة محادثات مختلفة تمامًا تستبدل البيانات قبل أن نتمكّن من نسخها.
البحث عن أخطاء الذاكرة
سأجري مزيدًا من التحقّق لمعرفة ما إذا كان هذا الرمز يعرض أي مشاكل في التطبيق. يبدو أنّ هذه فرصة مثالية لتجربة دعم أدوات فحص الأخطاء في Emscripten الجديد نسبيًا الذي تمت إضافته العام الماضي والذي تم تقديمه في محادثة WebAssembly في قمة مطوّري Chrome:
في هذه الحالة، نحن مهتمون بتطبيق
AddressSanitizer الذي
يمكنه رصد مشاكل مختلفة متعلقة بالمؤشرات والذاكرة. لاستخدامه، علينا إعادة تجميع برنامج الترميز
مع -fsanitize=address
:
emcc \
--bind \
${OPTIMIZE} \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="imagequant"' \
-I node_modules/libimagequant \
-o ./imagequant.js \
--std=c++11 \
imagequant.cpp \
-fsanitize=address \
node_modules/libimagequant/libimagequant.a
سيؤدي ذلك تلقائيًا إلى تفعيل عمليات التحقّق من أمان المؤشرات، ولكن نريد أيضًا العثور على أي تسرُّب محتمل للذاكرة. بما أنّنا نستخدم ImageQuant كمكتبة بدلاً من برنامج، لا تتوفّر "نقطة خروج" يمكن فيها لـ Emscripten التحقّق تلقائيًا من تحرير جميع الذاكرة.
بدلاً من ذلك، في مثل هذه الحالات، يقدّم LeakSanitizer (المضمّن في AddressSanitizer) الدالتَين
__lsan_do_leak_check
و
__lsan_do_recoverable_leak_check
،
التي يمكن استدعاؤها يدويًا عندما نتوقّع تحرير كل الذاكرة ونريد التحقّق من صحة
هذا الافتراض. يُقصد استخدام __lsan_do_leak_check
في نهاية تطبيق قيد التشغيل، عندما تريد
إلغاء العملية في حال رصد أي تسرّبات، في حين أنّ __lsan_do_recoverable_leak_check
هو أكثر ملاءمةً لحالات استخدام المكتبة مثل حالتنا، عندما تريد طباعة التسريبات في وحدة التحكّم، مع
مواصلة تشغيل التطبيق بغض النظر عن ذلك.
لنعرِض هذا المساعد الثاني من خلال Embind لنتمكّن من استدعائه من JavaScript في أي وقت:
#include <sanitizer/lsan_interface.h>
// …
void free_result() {
free(result);
}
EMSCRIPTEN_BINDINGS(my_module) {
function("zx_quantize", &zx_quantize);
function("version", &version);
function("free_result", &free_result);
function("doLeakCheck", &__lsan_do_recoverable_leak_check);
}
وسنستخدمها من جانب JavaScript بعد الانتهاء من الصورة. يساعد إجراء ذلك من جانب JavaScript، بدلاً من C++، في ضمان الخروج من جميع النطاقات وتحرير جميع كائنات C++ المؤقتة بحلول وقت إجراء عمليات التحقّق هذه:
// …
const result = opts.zx
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
: module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);
module.free_result();
module.doLeakCheck();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}
يمنحنا هذا تقريرًا مثل ما يلي في وحدة التحكّم:
هناك بعض التسريبات الصغيرة، ولكنّ تتبع تسلسل استدعاء الدوالّ ليس مفيدًا جدًا لأنّه تم تقطيع أسماء الدوالّ. لنعيد الترجمة مع تضمين معلومات تصحيح أخطاء أساسية للحفاظ عليها:
emcc \
--bind \
${OPTIMIZE} \
--closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="imagequant"' \
-I node_modules/libimagequant \
-o ./imagequant.js \
--std=c++11 \
imagequant.cpp \
-fsanitize=address \
-g2 \
node_modules/libimagequant/libimagequant.a
يبدو هذا أفضل بكثير:
لا تزال بعض أجزاء تتبع تسلسل استدعاء الدوال البرمجية غير واضحة لأنّها تشير إلى الوظائف الداخلية في Emscripten، ولكن يمكننا معرفة أنّ تسرُّب البيانات ناتج عن عملية تحويل RawImage
إلى "نوع سلك" (إلى قيمة JavaScript) من خلال Embind. في الواقع، عند الاطّلاع على الرمز البرمجي، يمكننا أن نرى أنّنا نعيد RawImage
مثيلات C++ إلى
JavaScript، ولكننا لا نُطلق سراحهم أبدًا من أيّ من الجانبَين.
تذكير: لا يتوفّر حاليًا دمج ميزة جمع المهملات بين JavaScript و
WebAssembly، ولكن يتم تطوير ميزة. بدلاً من ذلك، عليك
تحرير أي ذاكرة يدويًا واستدعاء وظائف التدمير من جانب JavaScript بعد الانتهاء من استخدام
العنصر. بالنسبة إلى Embind على وجه التحديد، تقترح المستندات الرسمية
استدعاء طريقة .delete()
في فئات C++ المعروضة:
يجب أن يحذف رمز JavaScript صراحةً أيّ معالِج عناصر C++ تلقّاه، وإلا ستتزايد ذاكرة التخزين المؤقت لـ Emscripten بشكل غير محدود.
var x = new Module.MyClass;
x.method();
x.delete();
في الواقع، عند إجراء ذلك في JavaScript لفئة الصف:
// …
const result = opts.zx
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
: module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);
module.free_result();
result.delete();
module.doLeakCheck();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
}
اختفاء التسرب على النحو المتوقّع
اكتشاف المزيد من المشاكل المتعلّقة بالمعقّمات
يؤدي إنشاء برامج ترميز Squoosh أخرى باستخدام برامج التطهير إلى الكشف عن مشاكل مشابهة وبعض المشاكل الجديدة. على سبيل المثال، ظهر لي هذا الخطأ في عمليات ربط MozJPEG:
في هذه الحالة، لا يحدث تسرُّب، ولكننا نكتب في ذاكرة خارج الحدود المخصّصة 😱
عند البحث في رمز MozJPEG، نجد أنّ المشكلة هنا هي أنّ jpeg_mem_dest
، وهي
الدالة التي نستخدمها لتخصيص وجهة ذاكرة لملف JPEG، تعيد استخدام القيم الحالية ل
outbuffer
وoutsize
عندما تكون
غير صفرية:
if (*outbuffer == NULL || *outsize == 0) {
/* Allocate initial buffer */
dest->newbuffer = *outbuffer = (unsigned char *) malloc(OUTPUT_BUF_SIZE);
if (dest->newbuffer == NULL)
ERREXIT1(cinfo, JERR_OUT_OF_MEMORY, 10);
*outsize = OUTPUT_BUF_SIZE;
}
ومع ذلك، نُجري هذا الاستدعاء بدون بدء أي من هذين المتغيّرين، ما يعني أنّ MozJPEG يكتب النتيجة في عنوان ذاكرة عشوائي محتمل تم تخزينه في هذين المتغيّرين في وقت الاستدعاء.
uint8_t* output;
unsigned long size;
// …
jpeg_mem_dest(&cinfo, &output, &size);
يؤدي ضبط كلا المتغيّرين على القيمة 0 قبل الاستدعاء إلى حلّ هذه المشكلة، ويصل الرمز البرمجي الآن إلى فحص تسرُّب الذاكرة بدلاً من ذلك. لحسن الحظ، تم اجتياز عملية التحقّق بنجاح، ما يشير إلى عدم توفّر أي تسرّبات في برنامج الترميز هذا.
مشاكل في الحالة المشتركة
…أم لا؟
نعلم أنّ عمليات ربط برامج الترميز تخزِّن بعض البيانات بالإضافة إلى النتائج في متغيّرات static عالمية، ويحتوي MozJPEG على بعض الهياكل المعقّدة بشكل خاص.
uint8_t* last_result;
struct jpeg_compress_struct cinfo;
val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
// …
}
ماذا لو تمّت بدء بعض هذه العناصر بشكلٍ كسول في التشغيل الأول، ثم تمت إعادة استخدامها بشكلٍ غير صحيح في عمليات التشغيل القادمة؟ وبالتالي، لن تؤدي مكالمة واحدة مع أحد خبراء التعقيم إلى تصنيف التطبيق على أنّه يتضمن مشكلة.
لنحاول معالجة الصورة مرتين من خلال النقر عشوائيًا على مستويات جودة مختلفة في واجهة المستخدم. في الواقع، نحصل الآن على التقرير التالي:
262,144 بايت: يبدو أنّه تم تسريب عيّنة الصورة بالكامل من jpeg_finish_compress
.
بعد الاطّلاع على المستندات والأمثلة الرسمية، تبيّن أنّ jpeg_finish_compress
لا تُفرِّغ الذاكرة التي تم تخصيصها من خلال طلب jpeg_mem_dest
السابق، بل تُفرِّغ فقط
بنية الضغط، على الرغم من أنّ بنية الضغط هذه تعرف مسبقًا عن وجهة
الذاكرة…
يمكننا حلّ هذه المشكلة عن طريق تحرير البيانات يدويًا في دالة free_result
:
void free_result() {
/* This is an important step since it will release a good deal of memory. */
free(last_result);
jpeg_destroy_compress(&cinfo);
}
يمكنني مواصلة البحث عن أخطاء الذاكرة هذه واحدًا تلو الآخر، ولكن أعتقد أنّه من الواضح الآن أنّ النهج الحالي لإدارة الذاكرة يؤدي إلى بعض المشاكل المنهجية المزعجة.
ويمكن أن يقضي المطهّر على بعض هذه الجراثيم على الفور. ويتطلب البعض الآخر استخدام حيل معقدة للقبض عليه. أخيرًا، هناك مشاكل، مثل تلك المذكورة في بداية المشاركة، والتي تبيّن لنا من خلال السجلات أنّه لم يتم رصدها من خلال أداة التعقيم على الإطلاق. والسبب هو أنّ إساءة الاستخدام الفعلية تحدث من جانب JavaScript، ولا يمكن لأداة التطهير الاطّلاع عليها. ولن تظهر هذه المشاكل إلا في مرحلة الإنتاج أو بعد إجراء تغييرات غير ذات صلة على الرمز في المستقبل.
إنشاء حزمة آمنة
لنتراجع خطوة إلى الوراء ونحلّ كل هذه المشاكل من خلال إعادة تنظيم الرمز البرمجي بطريقة أكثر أمانًا. سأستخدم حزمة ImageQuant كمثال مرة أخرى، ولكن تسري قواعد إعادة التنظيم المشابهة على جميع برامج الترميز، بالإضافة إلى قواعد قواعد البيانات الأخرى المشابهة.
أولاً، لنبدأ بإصلاح مشكلة الاستخدام بعد تحرير الذاكرة من بداية المشاركة. لهذا الغرض، نحتاج إلى استنساخ البيانات من طريقة العرض المستندة إلى WebAssembly قبل وضع علامة عليها كمحتوى مجاني من جانب JavaScript:
// …
const result = /* … */;
const imgData = new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
module.free_result();
result.delete();
module.doLeakCheck();
return new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
return imgData;
}
الآن، لنتأكّد من عدم مشاركة أي حالة في المتغيّرات الشاملة بين عمليات الاستدعاء. سيؤدي ذلك إلى حلّ بعض المشاكل التي واجهناها سابقًا، وسيسهّل استخدام أدوات تحويل الترميز في بيئة متعددة المواضيع في المستقبل.
لإجراء ذلك، نعيد صياغة حزمة C++ للتأكّد من أنّ كلّ استدعاء للدالة يدير
بياناته باستخدام المتغيّرات المحلية. بعد ذلك، يمكننا تغيير توقيع دالة free_result
لقبول المؤشر مرة أخرى:
liq_attr* attr;
liq_image* image;
liq_result* res;
uint8_t* result;
RawImage quantize(std::string rawimage,
int image_width,
int image_height,
int num_colors,
float dithering) {
const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
int size = image_width * image_height;
attr = liq_attr_create();
image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_attr* attr = liq_attr_create();
liq_image* image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
liq_set_max_colors(attr, num_colors);
liq_result* res = nullptr;
liq_image_quantize(image, attr, &res);
liq_set_dithering_level(res, dithering);
uint8_t* image8bit = (uint8_t*)malloc(size);
result = (uint8_t*)malloc(size * 4);
uint8_t* result = (uint8_t*)malloc(size * 4);
// …
}
void free_result() {
void free_result(uint8_t *result) {
free(result);
}
ولكن بما أنّنا نستخدم Embind في Emscripten للتفاعل مع JavaScript، يمكننا أيضًا جعل واجهة برمجة التطبيقات أكثر أمانًا من خلال إخفاء تفاصيل إدارة الذاكرة في C++ بالكامل.
لإجراء ذلك، لننقل الجزء new Uint8ClampedArray(…)
من JavaScript إلى جانب C++ باستخدام
Embind. بعد ذلك، يمكننا استخدامها لنسخ البيانات إلى ذاكرة JavaScript حتى قبل الخروج
من الدالة:
class RawImage {
public:
val buffer;
int width;
int height;
RawImage(val b, int w, int h) : buffer(b), width(w), height(h) {}
};
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
RawImage quantize(/* … */) {
val quantize(/* … */) {
// …
return {
val(typed_memory_view(image_width * image_height * 4, result)),
image_width,
image_height
};
val js_result = Uint8ClampedArray.new_(typed_memory_view(
image_width * image_height * 4,
result
));
free(result);
return js_result;
}
يُرجى ملاحظة كيف أنّنا نضمن من خلال تغيير واحد أنّ صفيف البايت الناتج مملوك لواجهة JavaScript
ولا يتم الاحتفاظ بنسخة احتياطية منه في ذاكرة WebAssembly، ونتخلص أيضًا من حزمة RawImage
التي تم تسريبها سابقًا.
لم يعُد على JavaScript الآن القلق بشأن تحرير البيانات على الإطلاق، ويمكنه استخدام النتيجة مثل أي عنصر آخر يتم جمع المهملات منه:
// …
const result = /* … */;
const imgData = new ImageData(
new Uint8ClampedArray(result.view),
result.width,
result.height
);
module.free_result();
result.delete();
// module.doLeakCheck();
return imgData;
return new ImageData(result, result.width, result.height);
}
ويعني ذلك أيضًا أنّنا لم نعد بحاجة إلى ربط free_result
مخصّص على جانب C++:
void free_result(uint8_t* result) {
free(result);
}
EMSCRIPTEN_BINDINGS(my_module) {
class_<RawImage>("RawImage")
.property("buffer", &RawImage::buffer)
.property("width", &RawImage::width)
.property("height", &RawImage::height);
function("quantize", &quantize);
function("zx_quantize", &zx_quantize);
function("version", &version);
function("free_result", &free_result, allow_raw_pointers());
}
بوجهٍ عام، أصبح رمز الغلاف أكثر وضوحًا وأمانًا في الوقت نفسه.
بعد ذلك، أجريت بعض التحسينات البسيطة الأخرى على رمز برنامج ImageQuant wrapper و كرّرت إصلاحات إدارة الذاكرة المشابهة لبرامج الترميز الأخرى. إذا كنت مهتمًا بالاطّلاع على مزيد من التفاصيل، يمكنك الاطّلاع على طلب التغيير الناتج هنا: إصلاحات في الذاكرة لبرامج الترميز باستخدام C++.
الخلاصات
ما هي الدروس التي يمكننا استخلاصها ومشاركتها من عملية إعادة التنظيم هذه والتي يمكن تطبيقها على قواعد بيانات أخرى؟
- لا تستخدِم طرق عرض الذاكرة المستندة إلى WebAssembly، بغض النظر عن اللغة التي تم إنشاؤها منها، بعد دعوة واحدة. ولا يمكنك الاعتماد على بقائها لفترة أطول من ذلك، ولن تتمكّن من رصد هذه الأخطاء بالوسائل التقليدية، لذا إذا كنت بحاجة إلى تخزين البيانات لاستخدامها لاحقًا، انسخها إلى جانب JavaScript واحفظها هناك.
- استخدِم لغة آمنة لإدارة الذاكرة أو على الأقل أدوات لفّ آمنة للأنواع بدلاً من التعامل مع المؤشرات الأوّلية مباشرةً، إن أمكن. لن يُنقذك هذا من الأخطاء في حدود JavaScript ↔ WebAssembly، ولكنه سيقلّل على الأقل من مساحة ظهور الأخطاء التي تتضمّن رمز اللغة الثابت.
- بغض النظر عن اللغة التي تستخدمها، يمكنك تشغيل الرمز البرمجي باستخدام أدوات التطهير أثناء التطوير، إذ يمكن أن تساعد هذه الأدوات في رصد الصعوبات في رمز اللغة الثابت، بالإضافة إلى بعض المشاكل في حدود JavaScript ↔
WebAssembly، مثل عدم تذكُّر استدعاء
.delete()
أو إدخال مؤشرات غير صالحة من جانب JavaScript. - تجنَّب، إن أمكن، عرض البيانات والكائنات غير المُدارة من WebAssembly إلى JavaScript بالكامل. JavaScript هي لغة تجمع المهملات، ولا يُستخدم فيها بشكل شائع إدارة الذاكرة يدويًا. يمكن اعتبار ذلك تسرُّبًا في عملية التجريد لنموذج الذاكرة للغة التي تم إنشاء WebAssembly منها، ومن السهل التغاضي عن الإدارة غير الصحيحة في قاعدة رمز JavaScript.
- قد يكون هذا واضحًا، ولكن كما هو الحال في أي قاعدة بيانات أخرى، تجنَّب تخزين الحالة القابلة للتغيير في المتغيّرات العموميّة. لا تريد تصحيح أخطاء المشاكل المتعلّقة بإعادة استخدامها في عمليات الاستدعاء المختلفة أو حتى في مثيلات المعالجة المتعدّدة، لذا من الأفضل إبقاءها مكتفية ذاتيًا قدر الإمكان.