اذهب إلى المحتوى

حلقات المرجع Reference Cycles وتسببها بتسريب الذاكرة Memory Leak في لغة Rust


Naser Dakhel

قد تكون عملية ملء الذاكرة دون تحريرها (العملية المعروفة بتسريب الذاكرة memory leak) صعبة الحدوث بمستوى أمان الذاكرة الذي تقدمه لغة رست Rust، إلا أن حدوث هذا الأمر غير مستحيل، إذ لا تضمن رست منع تسريب الذاكرة بصورةٍ كاملة، والمقصود هنا أن الذاكرة المُسرّبة آمنة في رست. نلاحظ أن رست تسمح بتسريب الذاكرة باستخدام Rc<T>‎ و RefCell<T>‎، إذ أنه من الممكن إنشاء مراجع تشير إلى بعضها ضمن حلقة cycle، وسيسبب هذا تسريب ذاكرة لأن عدد المراجع لكل عنصر لن يصل إلى 0 أبدًا، وبهذا لن تُحرَّر أي قيمة.

إنشاء حلقة مرجع

لنلاحظ كيف من الممكن أن نشكّل حلقة مرجع لتفادي هذا الأمر، بدءًا بتعريف معدّد List وتابع tail في الشيفرة 25:

اسم الملف: src/main.rs

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {}

الشيفرة 25: تعريف قائمة بنية تخزّن RefCell<T>‎ داخلها، بحيث نستطيع تعديل القيمة التي يشير إليها متغاير Cons

نستخدم هنا نوعًا مختلفًا من تعريف List من الشيفرة 5، إذ يصبح العنصر الثاني من المتغاير Cons أي variant مساويًا للنوع RefCell<Rc<List>>‎، مما يعني أننا نحتاج تعديل قيمة List المشار إليها في Cons عوضًا عن حاجتنا لقابلية التعديل على قيمة النوع i32 كما فعلنا في الشيفرة 24 في المقال السابق، نضيف أيضًا التابع tail لتسهيل الوصول إلى العنصر الثاني إذا وُجد متغاير Cons.

أضفنا في الشيفرة 26 الدالة main التي تستخدم التعريف الموجود في الشيفرة 25، إذ تُنشئ الشيفرة قائمةً في a وقائمة في b تشير إلى القائمة a، وثم تعدِّل القائمة في a لتشير إلى b لتنشئ بذلك حلقة مرجع. كتبنا أيضًا تعليمات println!‎ لتظهر قيمة عدد المراجع في نقاط مختلفة ضمن هذه العملية.

اسم الملف: src/main.rs

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // ألغِ تعليق السطر التالي لتلاحظ وجود حلقة المرجع، إذ ستتسبب الشيفرة البرمجية بطفحان المكدس
    // println!("a next item = {:?}", a.tail());
}

الشيفرة 26: إنشاء حلقة مرجع بحيث تشير قيمتي List إلى بعضهما بعضًا

أنشأنا نسخةً من Rc<List>‎ تحتوي على القيمة List في المتغير a مع قائمة مبدئية تحتوي على القيم 5,Nil، ثم أنشأنا نسخة Rc<List>‎ أخرى تحتوي قيمة List أخرى في المتغير b تحوي القيمة 10 وتشير إلى القائمة في a.

عدّلنا a لتشير إلى b بدلًا من Nil لنحصل بذلك على حلقة، وحقّقنا ذلك باستخدام التابع tail للحصول على مرجع إلى RefCell<Rc<List>>‎ في a، والذي وضعناه بعد ذلك في المتغير link، ثم استخدمنا التابع borrow_mut على القيمة RefCell<Rc<List>>‎ لتغيير القيمة داخل Rc<List>‎ التي تخزّن القيمة Nil إلى القيمة Rc<List>‎ الموجودة في b.

نحصل على الخرج التالي عندما ننفذ هذه الشيفرة البرمجية مع الإبقاء على تعليمة println!‎ الأخيرة معلّقة:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.53s
     Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

يبلغ عدد مراجع نُسخ Rc<List>‎ في كلٍّ من a و b القيمة 2 وذلك بعد تغيير القائمة في a لتشير إلى b. تُسقِط أو تحذف لغة رست في نهاية الدالة main المتغير b مما يغيّر من عدد مراجع نسخة b من Rc<List>‎ من 2 إلى 1، ولن تُحرّر الذاكرة التي تشغلها Rc<List>‎ على الكومة heap في هذه اللحظة لأن عدد المراجع هو 1 وليس 0، ومن ثم تُحرّر رست a، مما يُنقص عداد المرجع لنسخة a من Rc<List>‎ من 2 إلى 1 أيضًا. لا يُمكن تحرير ذاكرة النسخة هذه لأن نسخة Rc<List>‎ الأخرى لا تزال تشير إليها، وستبقى الذاكرة المحجوزة للقائمة شاغرة للأبد، ولتخيّل حلقة المرجع هذه بصريًا أنشأنا المخطط التالي في الشكل 4.

reference_cycle_of_lists_01.png

الشكل 4: حلقة مرجع خاصة بقائمتين a و b، تشيران إلى بعضهما بعضًا

إذا أزلت التعليق عن آخر تعليمة println!‎ ونفّذت البرنامج، فستحاول رست طباعة الحلقة مع إشارة القائمة a إلى القائمة b وإشارتها بدورها إلى القائمة a وهكذا دواليك حتى يطفح المكدّس.

ليست عواقب إنشاء حلقة مرجعية هنا خطيرة مقارنةً بالبرامج الواقعية، إذ أن برنامجنا ينتهي بعد إنشائنا حلقة مرجع reference cycle، إلا أن البرنامج سيستخدم ذاكرة أكثر مما يحتاج إذا كان البرنامج أكثر تعقيدًا وشَغَل ذاكرة أكثر في الحلقة واحتفظ بها لفترة أطول، وربما سيتسبّب ذلك بتحميل النظام عبئًا كبيرًا مسببًا نفاد الذاكرة المتوفرة.

إنشاء حلقات مرجع ليس بالأمر السهل ولكنه ليس مستحيلًا، فإذا كانت لديك قيم RefCell<T>‎ تحتوي على قيم Rc<T>‎ أو تشكيلات متداخلة مشابهة لأنواع مع قابلية التغيير الداخلي وعدّ المراجع، فيجب عليك التأكد أنك لم تنشئ حلقات، إذ لا يجب عليك الاعتماد على رست لإيجادها. إنشاء حلقة مرجع يحصل نتيجة خطأ منطقي في برنامجك ولذلك يجب عليك استخدام الاختبارات التلقائية ومراجعة الشيفرة البرمجية ووسائل تطوير البرامج الأخرى لتقليل احتمالية حدوثها.

يمكن إعادة تنظيم هيكلية البيانات كحلٍ آخر لتفادي حلقات المرجع، بحيث تعبّر بعض المراجع عن الملكية بينما لا يعبّر بعضها الآخر، ونتيجةً لذلك سيكون لديك حلقات مكونة من بعض علاقات الملكية ownership وبعضها من علاقات لا ملكية non-ownership، بحيث تؤثر علاقات الملكية فقط إذا كانت القيمة ستُحرَّر.

نريد من المتغاير Cons في الشيفرة 25 أن يملك قائمةً خاصةً به دائمًا، لذا فإن عملية إعادة تنظيم هيكلية البيانات ليست ممكنة. لنتابع مثالًا آخرًا باستخدام البيانات التخطيطية graphs بحيث تتكون من عُقد أب وعُقد أبناء لنرى كيف أن العلاقات اللا ملكية هي طريقة مناسبة لمنع حصول حلقات المرجع.

منع حلقات المرجع: بتحويل Rc‎ إلى Weak

وضّحنا بحلول هذه النقطة أن استدعاء Rc::clone يزيد من قيمة strong_count الخاصة بالنسخة Rc<T>‎ وأن نسخة Rc<T>‎ تُحرَّر فقط عندما تكون قيمة strong_count هي 0. بإمكاننا أيضًا إنشاء مرجع ضعيف weak reference للقيمة داخل نسخة Rc<T>‎ وذلك باستدعاء Rc::downgrade وتمرير مرجع إلى Rc<T>‎. المراجع القوية هي الطريقة التي يمكنك بها مشاركة الملكية لنسخة Rc<T>‎، بينما لا تعبّر المراجع الضعيفة عن علاقة ملكية، ولا يتأثر عددها عندما تُحرَّر نسخة من Rc<T>‎، ولا يسبب حلقة مرجعية، إذ ستُكسر الحلقات المتضمنة لمراجع ضعيفة عندما تصبح قيمة عدد المراجع القوية مساويةً إلى 0.

نحصل على مؤشر ذكي من النوع Weak<T>‎ عندما نستدعي Rc::downgrade، وبدلًا من زيادة strong_count في نسخة Rc<T>‎ بقيمة 1، سيزيد استدعاء Rc::downgrade من قيمة weak_count بمقدار 1. يستخدم النوع Rc<T>‎ القيمة weak_count لمتابعة عدد مراجع Weak<T>‎ الموجودة بصورةٍ مشابهة للقيمة strong_count، إلا أن الفرق هنا هو أن weak_count لا يحتاج أن يكون 0 لكي تُنظف نسخة <Rc<T.

يجب علينا التأكد أن القيمة موجودة فعلًا إذا أردت إجراء أي عمليات على القيمة التي يشير إليها Weak<T>‎، وذلك لأن القيمة قد تُحرَّر، ويمكننا التحقق من ذلك باستدعاء تابع upgrade على نسخة Weak<T>‎ التي تعيد قيمةً من النوع Option<Rc<T>>‎، وسنحصل على نتيجة Some إذا لم تُحرَّر القيمة Rc<T>‎ ونتيجة None إذا حُرّرت القيمة، تضمن رست أن حالتي Some و None ستُعامل على النحو الصحيح ولن تصبح مؤشرًا غير صالح، وذلك لأن upgrade تعيد <Option<Rc<T>‎.

لنأخذ مثالًا، فبدلًا من استخدام قائمة تعرف عناصرها العنصر الذي يليها فقط، سننشئ شجرةً تعرف عناصرها كلٍ من أبنائها وآبائها.

إنشاء هيكل بيانات الشجرة يحتوي على عقدة مع عقد أبناء

أولًا، ننشئ شجرة مع عقد nodes تعرف عقد أبنائها، ولتحقيق ذلك ننشئ بنيةً ندعوها Node تحتوي على قيم من النوع i32 إضافةً إلى مراجع تشير لقيم أبنائها Node:

اسم الملف: src/main.rs

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

نريد من البنية Node أن تمتلك أبنائها children، كما نريد كذلك مشاركة الملكية مع المتغيرات لكي نتمكن من الوصول إلى كل Node في الشجرة مباشرةً، ومن أجل تحقيق ذلك سوف نعرّف عناصر Vec<T>‎ بحيث تكون قيمًا من النوع Rc<Node>‎؛ كما نريد أيضًا تعديل العقد الأبناء لعقدة أخرى، لذلك سيكون لدينا RefCell<T>‎ في الأبناء children وحول Vec<Rc<Node>>‎.

سنستخدم تعريف الهيكلية لإنشاء نسخة Node واحدة بالاسم leaf وقيمتها 3 دون أن تحتوي على أبناء، ونسخةً أخرى اسمها branch قيمتها 5 تحتوي على leaf بمثابة واحد من أبنائها كما هو موضح في الشيفرة 27:

اسم الملف: src/main.rs

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

الشيفرة 27: إنشاء عقدة leaf دون أبناء وعقدة branch مع leaf بمثابة واحد من أبنائها

ننسخ القيمة Rc<Node>‎ الموجودة في leaf ونخزّنها في branch وبذلك تصبح Node في leaf مُمتلكةً من قبل مالكين، هما leaf و branch. يمكننا الانتقال من branch إلى leaf عبر branch.children ولكن لا يوجد طريقة للوصول الى leaf من branch، وسبب ذلك هو أن leaf لا تمتلك مرجع إلى branch، وبالتالي لا تعرف بأنها مرتبطة مع branch، إذًا نحن بحاجة لأن تعرف أن branch هي العقدة الأب وهذا ما سنفعله.

إضافة مرجع يشير إلى عقدة ابن داخل عقدة أب

نحتاج لإضافة حقل parent لجعل عقدة ابن تعرف بوجود آبائها وذلك ضمن تعريف الهيكل Node. تكمن صعوبة الأمر في اختيار النوع الذي يجب استخدامه لتخزين القيمة parent، إلا أننا نعلم أنه لا يمكن للقيمة أن تحتوي النوع Rc<T>‎ لأننا نحصل بذلك على حلقة مرجع تحتوي على القيمة leaf.parent مشيرةً إلى branch و branch.children مشيرةً إلى leaf مما يجعل قيم strong_count غير مساوية إلى القيمة 0 في أي من الحالات.

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

لذا نجعل النوع parent يستخدم Weak<T>‎ بدلًا من Rc<T>‎ وتحديدًا النوع RefCell<Weak<Node>>‎، وسيصبح تعريف الهيكل Node على النحو التالي:

اسم الملف: src/main.rs

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

يمكن للعقدة أن تُشير إلى العقدة الأب ولكن لا يمكن أن تمتلكها. عدّلنا من الدالة main في الشيفرة 28 بحيث تستخدم التعريف الجديد لكي تكون للعقدة leaf طريقة للإشارة إلى العقدة الأب branch:

اسم الملف: src/main.rs

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

الشيفرة 28: عقدة leaf مع مرجع ضعيف للعقدة الأب branch

يبدو إنشاء العقدة leaf شبيهًا للشيفرة 27، باختلاف الحقل parent، إذ تبدأ العقدة leaf بدون أب وهذا يمكّننا من إنشاء نسخة لمرجع Weak<Node>‎ فارغ.

عندما نحاول الحصول على مرجع للعقدة الأب الخاصة بالعقدة leaf وذلك باستخدام التابع upgrade، نحصل على قيمة None، ويمكن رؤية الخرج الناتج من أول تعليمة println!‎:

leaf parent = None

نحصل على مرجع Weak<Node>‎ جديد عندما نُنشئ العقدة branch وذلك في الحقل parent لأن branch لا تحتوي على عقدة أب. لا يزال لدينا leaf وهي عقدة ابن للعقدة branch، وحالما يوجد لدينا نسخة من Node في branch سيكون بإمكاننا تعديل leaf بمنحها مرجعًا إلى العقدة الأب الخاصة بها من النوع Weak<Node>‎. نستخدم التابع borrow_mut على النوع RefCell<Weak<Node>>‎ في حقل parent الخاص بالعقدة leaf، ومن ثم نستخدم الدالة Rc::downgrade لإنشاء مرجع من النوع Weak<Node>‎ يشير إلى العقدة branch من النوع Rc<Node>‎ في branch.

نحصل على متغاير Some يحتوي على branch عندما نطبع أب العقدة leaf مجددًا، إذ يمكن للعقدة leaf الآن الوصول إلى العقدة الأب. نستطيع أيضًا تفادي إنشاء الحلقة التي ستتسبب أخيرًا في طفحان المكدّس عند طباعة leaf كما هو الحال في الشيفرة 26؛ إذ تُطبع مراجع Weak<Node>‎ مع الكلمة (Weak) جانبها:

leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })

يشير عدم وجود خرج لانهائي إلى أن الشيفرة لم تُنشئ حلقة مرجع، ويمكنك التأكد من ذلك عن طريق ملاحظة القيم التي نحصل عليها باستدعاء كل من Rc::strong_count و Rc::weak_count.

مشاهدة التغييرات التي تحصل على strong_count و weak_count

دعنا نلاحظ كيف تتغير قيم نُسخ Rc<Node>‎ لكل من النسخة strong_count و weak_count، وذلك عن طريق إنشاء نطاق داخلي جديد ونقل عملية إنشاء branch إلى هذا النطاق، إذ نستطيع بفعل ذلك مشاهدة ما الذي يحصل عند إنشاء العقدة branch وتحريرها بعد أن تخرج من النطاق. التعديلات موضحة في الشيفرة 29:

اسم الملف: src/main.rs

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );

    {
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        );

        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        );
    }

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );
}

الشيفرة 29: إنشاء branch في نطاق داخلي وفحص عدد المراجع القوية والضعيفة

يبلغ عدد المراجع القوية للنوع Rc<Node>‎ القيمة 1 بعد إنشاء leaf، وعدد المراجع الضعيفة يساوي 0. نُنشئ في النطاق الداخلي العقدة branch ونربطها مع leaf وهذه هي النقطة التي نبدأ فيها بطباعة عدد المراجع، يبلغ عدد المراجع القوية الخاصة بالنوع Rc<Node>‎ في branch القيمة 1 وعدد المراجع الضعيفة 1 (لأن leaf.parent تشير إلى branch باستخدام قيمة من النوع <Weak<Node). نلاحظ تغيّر عداد المراجع القوية إلى 2 عندما نطبع عدد المراجع القوية والضعيفة في leaf وذلك لأن branch هي نسخةٌ من النوع Rc<Node>‎ من القيمة leaf ومخزّنة في branch.children، إلا أننا ما زلنا نحصل على عدد مراجع ضعيفة يساوي 0.

تخرج branch عن النطاق عندما ينتهي النطاق الداخلي وينقص عدد المراجع القوي الخاص بالنوع Rc<Node>‎ إلى 0، لذا تُحرّر قيمة Node الخاصة به. لا يوجد تأثير لعدد المراجع الضعيفة البالغ 1 ضمن leaf.parent على تحرير القيمة Node أو عدم تحريرها ولذلك لا نحصل على تسريب للذاكرة.

إذا أردنا الوصول إلى العقدة الأب الخاصة بالعقدة leaf في نهاية النطاق، فسنحصل على None مجددًا. عدد المراجع القوية الخاصة بالنوع Rc<Node>‎ في leaf هو 1، وعدد المراجع الضعيفة هو 0 في نهاية البرنامج، وذلك لأن المتغير leaf هو المرجع الوحيد للنوع Rc<Node>‎ مجددًا.

المنطق الذي يُدير عدّ وتحرير القيم مُطبَّق في كلٍّ من Rc<T>‎ و Weak<T>‎، إضافةً إلى تنفيذ السمة Drop. سيجعل تحديد أن علاقة العقدة الابن بالعقدة الأب يجب أن تكون مرجعًا ضعيفًا من النوع Weak<T>‎ في تعريف Node، وجود عقدة أب تشير إلى عُقد ابن وبالعكس ممكنًا دون إنشاء حلقة مرجع والتسبُّب بتسريب ذاكرة.

ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...