توليد أنواع الحروف على الطاير (أو مجنون مع الصدأ)

في هذه المقالة ، سنسخر بلغة برمجة Rust ، ولا سيما كائنات السمات.


عندما تعرفت على Rust ، بدا لي أن أحد تفاصيل تطبيق كائنات الكتابة أمر مثير للاهتمام بالنسبة لي. وهي أن جدول الوظائف الافتراضية ليس في البيانات نفسها ، ولكن في المؤشر "الكثيف" بها. يحتوي كل مؤشر إلى كائن كتابة) على مؤشر للبيانات نفسها ، بالإضافة إلى ارتباط إلى جدول افتراضي حيث توجد عناوين الوظائف التي تقوم بتطبيق كائن النوع لهيكل محدد (ولكن نظرًا لأن هذا يمثل تفاصيل تنفيذ ، فقد يتغير السلوك.


لنبدأ بمثال بسيط يوضح المؤشرات السميكة. سيتم إخراج التعليمات البرمجية التالية على هندسة 64 بت 8 و 16:


fn main () { let v: &String = &"hello".into(); let disp: &std::fmt::Display = v; println!("  : {}", std::mem::size_of_val(&v)); println!("   -: {}", std::mem::size_of_val(&disp)); } 

لماذا هذا مثير للاهتمام؟ عندما كنت مشتركًا في Java Enterprise ، كانت إحدى المهام التي نشأت بانتظام إلى حد ما هي تكييف الكائنات الموجودة مع واجهات معينة. بمعنى ، الكائن موجود بالفعل ، تم إصداره كارتباط ، ولكن يجب تكييفه مع الواجهة المحددة. ولا يمكنك تغيير كائن الإدخال ، هذا ما هو عليه.


كان علي فعل شيء مثل هذا:


 Person adapt(Json value) { // ...- , , ,  "value"  //   Person return new PersonJsonAdapter(value); } 

كانت هناك مشاكل مختلفة مع هذا النهج. على سبيل المثال ، إذا كان الكائن نفسه "يتكيف" مرتين ، فسنحصل على شخصين مختلفين (من وجهة نظر مقارنة الارتباط). وحقيقة أن لديك لإنشاء كائنات جديدة في كل مرة هو قبيح إلى حد ما.


عندما رأيت كائنات الكتابة في Rust ، كان لدي فكرة أنه في Rust يمكن أن يتم ذلك بأناقة أكثر! يمكنك أيضًا أخذ وتعيين جدول افتراضي آخر للبيانات والحصول على كائن سمات جديد! ولا تخصص ذاكرة لكل مثيل. في الوقت نفسه ، يظل منطق "الاقتراض" بأكمله ساري المفعول - تبدو وظيفة التكيف لدينا وكأنها شيء مثل fn adapt<'a>(value: &'a Json) -> &'a Person (أي ، نحن نوع من الاقتراض من مصدر البيانات).


أكثر من ذلك ، يمكنك "فرض" نفس النوع (على سبيل المثال ، String ) لتنفيذ كائن الكتابة لدينا عدة مرات ، مع سلوك مختلف. لماذا؟ لكنك لا تعرف أبدًا ما قد يكون مطلوبًا في المشروع؟


دعنا نحاول تنفيذ هذا.


بيان المشكلة


لقد قمنا بتعيين المهمة بهذه الطريقة: جعل وظيفة التعليق التوضيحي "تعيّن" كائن الكتابة التالي لنوع String المعتاد:


 trait Object { fn type_name(&self) -> &str; fn as_string(&self) -> &String; } 

وظيفة annotate نفسه:


 ///    - `Object`,   , ///   "" -- ,    `type_name`. fn annotate<'a>(input: &'a String, type_name: &str) -> &'a dyn Object { // ... } 

دعنا نكتب اختبار على الفور. أولاً ، تأكد من أن النوع "المعين" يطابق النوع المتوقع. ثانياً ، سوف نتأكد من أنه يمكننا الحصول على الخط الأصلي وسيكون نفس السطر (من وجهة نظر المؤشرات):


 #[test] fn test() { let input: String = "hello".into(); let annotated1 = annotate(&input, "Widget"); let annotated2 = annotate(&input, "Gadget"); // -   ,    assert_eq!("Widget", annotated1.type_name()); assert_eq!("Gadget", annotated2.type_name()); let unwrapped1 = annotated1.as_string(); let unwrapped2 = annotated2.as_string(); //       --   assert_eq!(unwrapped1 as *const String, &input as *const String); assert_eq!(unwrapped2 as *const String, &input as *const String); } 

النهج رقم 1: وبعدنا على الأقل فيضان!


أولاً ، دعونا نحاول تنفيذ تطبيق ساذج تمامًا. فقط قم بلف بياناتنا في "ملف" ، والذي سيحتوي أيضًا على type_name :


 struct Wrapper<'a> { value: &'a String, type_name: String, } impl<'a> Object for Wrapper<'a> { fn type_name(&self) -> &str { &self.type_name } fn as_string(&self) -> &String { self.value } } 

لا شيء خاص بعد. كل شيء يشبه في جافا. ولكن ليس لدينا جامع للقمامة ، حيث سنقوم بتخزين هذا المجمع؟ نحتاج إلى إرجاع الرابط ، بحيث يظل صالحًا بعد استدعاء وظيفة annotate . سنضع شيئًا مخيفًا في Box حتى Wrapper تمييز Wrapper على الكومة. ثم سنعود الرابط إليه. وبغية بقاء الغلاف على قيد الحياة بعد استدعاء وظيفة annotate ، سنقوم "بتسريب" هذا المربع:


 fn annotate<'a>(input: &'a String, type_name: &str) -> &'a dyn Object { let b = Box::new(Wrapper { value: input, type_name: type_name.into(), }); Box::leak(b) } 

... ويمر الاختبار!


ولكن هذا هو بعض القرار المشكوك فيه. لا نقوم فقط بتخصيص الذاكرة مع كل "تعليق توضيحي" ، وبالتالي فإن الذاكرة تتسرب ( Box::leak تُرجع رابطًا إلى البيانات المخزنة على الكومة ، ولكن في نفس الوقت "ينسى" المربع نفسه ، أي أنه لن يكون هناك إصدار تلقائي )


النهج 2: الساحة!


بادئ ذي بدء ، دعونا نحاول حفظ هذه الأغلفة في مكان ما بحيث يتم إطلاقها في وقت ما. ولكن في نفس الوقت الاحتفاظ بالتوقيع annotate كما هو. أي أن إرجاع رابط مع حساب المرجع (على سبيل المثال ، Rc<Wrapper> ) لا يعمل.


أبسط خيار هو إنشاء بنية مساعدة ، "نظام كتابة" ، سيكون مسؤولاً عن تخزين هذه الأغلفة. وعندما ننتهي ، سنصدر هذا الهيكل وجميع الأغلفة به.


شيء من هذا القبيل. تُستخدم مكتبة typed-arena لتخزين الأغلفة ، ولكن يمكنك الحصول عليها Vec<Box<Wrapper>> ، الشيء الرئيسي هو ضمان عدم تحريك Wrapper أي مكان (في الليل يمكنك استخدام واجهة برمجة تطبيقات pin لهذا):


 struct TypeSystem { wrappers: typed_arena::Arena<Wrapper>, } impl TypeSystem { pub fn new() -> Self { Self { wrappers: typed_arena::Arena::new(), } } ///     `input`,      , ///    (  ,    , ///        )! pub fn annotate<'a: 'b, 'b>( &'a self, input: &'b String, type_name: &str ) -> &'b dyn Object { self.wrappers.alloc(Wrapper { value: input, type_name: type_name.into(), }) } } 

ولكن أين ذهبت المعلمة المسؤولة عن عمر الرابط لنوع Wrapper ؟ كان علينا أن نتخلص منه ، لأنه لا يمكننا أن typed_arena::Arena<Wrapper<'?>> بعض العمر الثابت في النوع typed_arena::Arena<Wrapper<'?>> . كل غلاف يحتوي على معلمة فريدة من نوعها ، اعتمادا على input !


بدلاً من ذلك ، نقوم برش القليل من الصدأ غير الآمن للتخلص من معلمة وقت الحياة:


 struct Wrapper { value: *const String, type_name: String, } impl Object for Wrapper { fn type_name(&self) -> &str { &self.type_name } ///   -- ,     (  /// `annotate`),     (    - /// `&Object`)  ,      (`String`). fn as_string(&self) -> &String { unsafe { &*self.value } } } 

وتمر الاختبارات مرة أخرى ، مما يمنحنا الثقة في صحة القرار. بالإضافة إلى الشعور بعدم الارتياح مع عدم unsafe (كما ينبغي أن يكون ، من الأفضل عدم المزاح مع الصدأ غير الآمن!).


ولكن لا يزال ، ماذا عن الخيار الموعود ، والذي لا يتطلب تخصيصات ذاكرة إضافية للأغلفة؟


المنهج رقم 3: دع أبواب الجحيم مفتوحة


فكرة. لكل "نوع" فريد ("القطعة" ، "الأداة") ، سنقوم بإنشاء جدول افتراضي. الأيدي أثناء تنفيذ البرنامج. ونخصصها للرابط الذي أعطانا إياه بالبيانات نفسها (والتي ، كما نذكر ، هي مجرد String ).


أولاً ، وصف موجز لما نحتاج إلى الحصول عليه. لذلك ، إشارة إلى كائن الكتابة ، كيف يتم ترتيب ذلك؟ في الحقيقة ، هذه مجرد مؤشران ، أحدهما للبيانات نفسها والآخر للجدول الافتراضي. لذلك نكتب:


 #[repr(C)] struct TraitObject { pub data: *const (), pub vtable: *const (), } 

( #[repr(C)] نحتاج إلى ضمان الموقع الصحيح في الذاكرة).


يبدو أن كل شيء بسيط ، سنقوم بإنشاء جدول جديد للمعلمات المعينة و "تجميع" رابط لكائن الكتابة! لكن ماذا يتكون هذا الجدول؟


الإجابة الصحيحة على هذا السؤال هي "هذه تفاصيل التنفيذ". لكننا سنفعل ذلك قم بإنشاء ملف سلسلة rust-toolchain في جذر مشروعنا nightly-2018-12-01 هناك: nightly-2018-12-01 . بعد كل شيء ، يمكن اعتبار مجموعة ثابتة مستقرة ، أليس كذلك؟


الآن وقد أصلحنا إصدار Rust (في الواقع ، سنحتاج إلى التجميع الليلي لإحدى المكتبات أدناه).


بعد إجراء بعض عمليات البحث على الإنترنت ، اكتشفنا أن تنسيق الجدول بسيط: أولاً ، يوجد رابط إلى destructor ، ثم حقلان مرتبطان بتخصيص الذاكرة (حجم النوع والمحاذاة) ، ثم تعمل الدالات واحدًا تلو الآخر (يكون الترتيب حسب تقدير المترجم ، ولكن لدينا وظيفتان فقط ، وبالتالي فإن احتمال التخمين مرتفع للغاية ، 50 ٪).


لذلك نكتب:


 #[repr(C)] #[derive(Clone, Copy)] struct VirtualTableHeader { destructor_fn: fn(*mut ()), size: usize, align: usize, } #[repr(C)] struct ObjectVirtualTable { header: VirtualTableHeader, type_name_fn: fn(*const String) -> *const str, as_string_fn: fn(*const String) -> *const String, } 

وبالمثل ، هناك حاجة إلى #[repr(C)] لضمان الموقع الصحيح في الذاكرة. لقد قسمت إلى هيكلين ، وبعد ذلك بقليل سيكون من المفيد لنا.


الآن دعونا نحاول كتابة نظام الكتابة الخاص بنا ، والذي سيوفر وظيفة annotate . سنحتاج إلى تخزين الجداول التي تم إنشاؤها مؤقتًا ، لذلك دعونا نحصل على ذاكرة التخزين المؤقت:


 struct TypeInfo { vtable: ObjectVirtualTable, } #[derive(Default)] struct TypeSystem { infos: RefCell<HashMap<String, TypeInfo>>, } 

نحن نستخدم الحالة الداخلية لـ RefCell حتى TypeSystem::annotate وظيفة TypeSystem::annotate تلقي &self كرابط مشترك. هذا أمر مهم ، لأننا " TypeSystem " من TypeSystem لضمان بقاء الجداول الافتراضية التي TypeSystem أطول من المرجع إلى كائن الكتابة الذي نعود TypeSystem من TypeSystem .


نظرًا لأننا نريد أن نكون قادرين على إضافة تعليقات توضيحية إلى العديد من الحالات ، فإننا لا نستطيع استعارة &mut self قابلة للتغيير.


وسنرسم هذا الرمز:


 impl TypeSystem { pub fn annotate<'a: 'b, 'b>( &'a self, input: &'b String, type_name: &str ) -> &'b dyn Object { let type_name = type_name.to_string(); let mut infos = self.infos.borrow_mut(); let imp = infos.entry(type_name).or_insert_with(|| unsafe { //    ,  ? let vtable = unimplemented!(); TypeInfo { vtable } }); let object_obj = TraitObject { data: input as *const String as *const (), vtable: &imp.vtable as *const ObjectVirtualTable as *const (), }; //       - unsafe { std::mem::transmute::<TraitObject, &dyn Object>(object_obj) } } } 

من أين نحصل على هذا الجدول؟ ستتطابق الإدخالات الثلاثة الأولى فيه مع إدخالات أي جدول افتراضي آخر للنوع المحدد. لذلك ، فقط تأخذ ونسخها. أولاً ، دعنا نحصل على هذا النوع:


 trait Whatever {} impl<T> Whatever for T {} 

من المفيد لنا أن نحصل على هذا "أي طاولة افتراضية أخرى". ثم ننسخ هذه الإدخالات الثلاثة منه:


 let whatever = input as &dyn Whatever; let whatever_obj = std::mem::transmute::<&dyn Whatever, TraitObject>(whatever); let whatever_vtable_header = whatever_obj.vtable as *const VirtualTableHeader; let vtable = ObjectVirtualTable { //  ! header: *whatever_vtable_header, type_name_fn: unimplemented!(), as_string_fn: unimplemented!(), }; TypeInfo { vtable } 

بشكل أساسي ، يمكننا الحصول على الحجم والمحاذاة من خلال std::mem::size_of::<String>() و std::mem::align_of::<String>() . لكن من أي مكان آخر يمكن أن يُسرق منه المدمر ، لا أعرف.


حسنًا ، ولكن من أين نحصل على عناوين هذه الوظائف ، type_name_fn و as_string_fn ؟ قد تلاحظ أن as_string_fn ليس ضروريًا بشكل عام ، حيث يكون مؤشر البيانات دائمًا بمثابة السجل الأول في تمثيل كائن الكتابة. أي أن هذه الوظيفة هي نفسها دائمًا:


 impl Object for String { // ... fn as_string(&self) -> String { self } } 

لكن مع الوظيفة الثانية ، ليس بهذه السهولة! يعتمد أيضًا على اسمنا "النوع" ، type_name .


لا يهم ، يمكننا فقط توليد هذه الوظيفة في وقت التشغيل. دعنا نأخذ مكتبة dynasm لهذا (في الوقت الحالي ، يتطلب إنشاء Rust Nightly). اقرأ عن
اصطلاحات استدعاء وظيفة .


للبساطة ، افترض أننا مهتمون فقط بنظام التشغيل Mac OS و Linux (بعد كل هذه التحولات الممتعة ، لم يعد التوافق يزعجنا حقًا ، أليس كذلك؟). ونعم ، حصرا إلى x86-64 ، بالطبع.


الوظيفة الثانية ، as_string ، سهلة التنفيذ. لقد وعدنا أن تكون المعلمة الأولى في سجل RDI . وإرجاع القيمة إلى RAX . وهذا يعني أن رمز الوظيفة سيكون مثل:


 dynasm!(ops ; mov rax, rdi ; ret ); 

لكن الوظيفة الأولى هي أصعب قليلا. أولاً ، نحن بحاجة إلى العودة &str ، وهو مؤشر سميك. الجزء الأول هو مؤشر لسلسلة ، والجزء الثاني هو طول شريحة السلسلة. لحسن الحظ ، فإن الاتفاقية المذكورة أعلاه تسمح لك بإرجاع نتائج 128 بت باستخدام سجل EDX للجزء الثاني.


يبقى أن تحصل في مكان ما على رابط لشريحة سلسلة تحتوي على type_name string لدينا. لا نريد الاعتماد على type_name (على الرغم من أنه من خلال التعليقات التوضيحية لمدى الحياة ، يمكننا ضمان أن type_name سيعيش أطول من القيمة التي تم إرجاعها).


لكن لدينا نسخة من هذا الخط ، والتي وضعناها في جدول التجزئة. عند عبور أصابعنا ، String::as_str أن موقع شريحة السلسلة التي لن String::as_str لن يتغير من تحريك String (وسيتم نقل String في عملية تغيير حجم HashMap حيث يتم تخزين هذه السلسلة بواسطة المفتاح). لا أعرف ما إذا كانت المكتبة القياسية تضمن هذا السلوك ، لكن هل نلعبها بسهولة؟


نحصل على المكونات اللازمة:


 let type_name_ptr = type_name.as_str().as_ptr(); let type_name_len = type_name.as_str().len(); 

واكتب هذه الوظيفة:


 dynasm!(ops ; mov rax, QWORD type_name_ptr as i64 ; mov rdx, QWORD type_name_len as i64 ; ret ); 

وأخيرًا ، كود annotate النهائي:


 pub fn annotate<'a: 'b, 'b>(&'a self, input: &'b String, type_name: &str) -> &'b Object { let type_name = type_name.to_string(); //       let type_name_ptr = type_name.as_str().as_ptr(); let type_name_len = type_name.as_str().len(); let mut infos = self.infos.borrow_mut(); let imp = infos.entry(type_name).or_insert_with(|| unsafe { let mut ops = dynasmrt::x64::Assembler::new().unwrap(); //     `type_name` let type_name_offset = ops.offset(); dynasm!(ops ; mov rax, QWORD type_name_ptr as i64 ; mov rdx, QWORD type_name_len as i64 ; ret ); //     `as_string` let as_string_offset = ops.offset(); dynasm!(ops ; mov rax, rdi ; ret ); let buffer = ops.finalize().unwrap(); //      let whatever = input as &dyn Whatever; let whatever_obj = std::mem::transmute::<&dyn Whatever, TraitObject>(whatever); let whatever_vtable_header = whatever_obj.vtable as *const VirtualTableHeader; let vtable = ObjectVirtualTable { header: *whatever_vtable_header, type_name_fn: std::mem::transmute(buffer.ptr(type_name_offset)), as_string_fn: std::mem::transmute(buffer.ptr(as_string_offset)), }; TypeInfo { vtable, buffer } }); assert_eq!(imp.vtable.header.size, std::mem::size_of::<String>()); assert_eq!(imp.vtable.header.align, std::mem::align_of::<String>()); let object_obj = TraitObject { data: input as *const String as *const (), vtable: &imp.vtable as *const ObjectVirtualTable as *const (), }; unsafe { std::mem::transmute::<TraitObject, &dyn Object>(object_obj) } } 

لأغراض dynasm نحتاج أيضًا إلى إضافة الحقل buffer إلى بنية TypeInfo بنا. يتحكم هذا الحقل في الذاكرة التي تخزن رمز وظائفنا التي تم إنشاؤها:


 #[allow(unused)] buffer: dynasmrt::ExecutableBuffer, 

وكل الاختبارات تمر!


انتهى يا سيد!


بهذه السهولة والطبيعية ، يمكنك إنشاء تطبيقك الخاص لكائنات الكائنات في كود Rust!


يعتمد الحل الأخير بنشاط على تفاصيل التنفيذ وبالتالي لا يوصى باستخدامه. ولكن في الواقع ، عليك أن تفعل ما عليك. الأوقات اليائسة تتطلب تدابير يائسة!


ومع ذلك ، هناك ميزة واحدة (أكثر) أعتمد عليها هنا. وهي أنه من الآمن تحرير الذاكرة التي يشغلها الجدول تقريبًا بعد عدم وجود إشارات إلى كائن الكتابة الذي يستخدمه. من ناحية ، من المنطقي أنه لا يمكنك استخدام جدول افتراضي إلا من خلال مراجع كائنات الكتابة. من ناحية أخرى ، تتمتع الطاولات التي يوفرها Rust بعمر 'static . من الممكن تمامًا افتراض بعض الكودات التي ستفصل الجدول عن الرابط لبعض أغراضه ( لا تعرف أبدًا ، على سبيل المثال ، بعض الحيل القذرة ).


شفرة المصدر يمكن العثور عليها هنا .

Source: https://habr.com/ru/post/ar432202/


All Articles