Rust: مشكلة في تتبع RFC 1892 ، "إيقاف غير مهيأ لصالح نوع ربما Uninit جديد"

تم إنشاؤها على ١٩ أغسطس ٢٠١٨  ·  382تعليقات  ·  مصدر: rust-lang/rust

مشكلة تتبع جديدة = https://github.com/rust-lang/rust/issues/63566

هذه مشكلة تتبع لـ RFC "تجاهل uninitialized لصالح نوع MaybeUninit " (rust-lang / rfcs # 1892).

خطوات:

  • [x] تنفيذ RFC (cc @ rust-lang / libs)
  • [x] ضبط الوثائق (في https://github.com/rust-lang/rust/pull/60445)
  • [x] Stabilization PR (في https://github.com/rust-lang/rust/pull/60445)

أسئلة لم يتم حلها:

  • هل يجب أن يكون لدينا أداة ضبط آمنة تُرجع &mut T ؟
  • هل يجب إعادة تسمية MaybeUninit ؟
  • هل يجب إعادة تسمية into_inner ؟
  • هل يجب أن يكون MaybeUninit<T> Copy مقابل T: Copy ؟
  • هل يجب أن نسمح باستدعاء get_ref و get_mut (لكن لا يتم القراءة من المراجع المرتجعة) قبل تهيئة البيانات؟ (AKA: "هل الإشارات إلى البيانات غير المهيأة insta-UB ، أم فقط UB عند القراءة منها؟") هل يجب إعادة تسميتها على غرار into_inner ؟
  • هل يمكننا جعل into_inner (أو أيًا كان ما يطلق عليه) ذعرًا عندما يكون T غير مأهول ، مثل mem::uninitialized يفعل حاليًا؟ (منجز)
  • يبدو أننا لا نريد
B-RFC-approved C-tracking-issue E-mentor T-lang T-libs

التعليق الأكثر فائدة

mem::zeroed() مفيدًا لبعض حالات FFI حيث يُتوقع منك عدم وجود قيمة بقيمة memset(&x, 0, sizeof(x)) قبل استدعاء دالة C. أعتقد أن هذا سبب كاف لإبقائه غير مهين.

ال 382 كومينتر

ccRalfJung

[] تنفيذ RFC

يمكنني المساعدة في تنفيذ RFC.

رائع ، يمكنني المساعدة في المراجعة :)

أود بعض التوضيحات حول هذا الجزء من طلب التعليقات:

جعل الاتصال غير مهيأ على نوع فارغ يؤدي إلى ذعر وقت التشغيل والذي يطبع أيضًا رسالة الإيقاف.

هل يجب فقط mem::uninitialized::<!>() الذعر؟ أم يجب أن يشمل هذا أيضًا الهياكل (وربما التعدادات؟) التي تحتوي على النوع الفارغ (مثل (!, u8)

AFAIK نقوم فقط بإنشاء رمز ضار حقًا مقابل ! . معظم الاستخدامات الأخرى لـ mem::uninitialized غير صحيحة ، لكن لا يحدث أن يستغلها المترجم.

لذلك سأفعل ذلك مقابل ! فقط ، ولكن أيضًا مقابل mem::zeroed . (لقد نسيت تعديل هذا الجزء عندما أضفت zeroed إلى RFC ، على ما يبدو).

يمكننا البدء بعمل هذا:
https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/intrinsic.rs#L184 -L198

تحقق مما إذا كان fn_ty.ret.layout.abi هو Abi::Uninhabited وعلى الأقل انبعث فخًا ، على سبيل المثال: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/ معامل # L400 -L403

بمجرد أن ترى المصيدة (أي intrinsics::abort ) أثناء عملها ، يمكنك معرفة ما إذا كانت هناك طريقة لطيفة لإثارة الذعر. سيكون الأمر `` صعبًا بسبب الفك ، سنحتاج إلى وضعهم في حالة خاصة هنا: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/block.rs#L445 - L447

للذعر فعليًا ، ستحتاج إلى شيء مثل هذا: https://github.com/rust-lang/rust/blob/8928de74394f320d1109da6731b12638a2167945/src/librustc_codegen_llvm/mir/block.rs#L360 -L407
(يمكنك تجاهل ذراع EvalErrorKind::BoundsCheck )

eddyb شكرا للمؤشرات.


أقوم الآن بإصلاح (العديد) من تحذيرات الإهمال وأشعر بإغراء (جدًا) لتشغيل sed -i s/mem::uninitialized()/mem::MaybeUninit::uninitialized().into_inner()/g ولكن أعتقد أن هذا سيفوت النقطة ... أو هل هذا جيد إذا كنت أعرف أن القيمة ملموسة (نسخ) اكتب؟ على سبيل المثال ، let x: [u8; 1024] = mem::uninitialized(); .

هذا سيفتقد النقطة بالضبط ، نعم. ^^

على الأقل في الوقت الحالي ، أود التفكير في mem::MaybeUninit::uninitialized().into_inner() UB لجميع الأنواع غير النقابية. لاحظ أن Copy بالتأكيد غير كافٍ ؛ كلا من bool و &'static i32 هما Copy والمقتطف الخاص بك مصمم ليكون insta-UB لهما. قد نرغب في استثناء "الأنواع التي تكون فيها جميع أنماط البت على ما يرام" (أنواع الأعداد الصحيحة ، أساسًا) ، لكنني سأعارض إجراء مثل هذا الاستثناء لأن undef ليس نمط بت عادي. لهذا السبب يقول RFC أنك بحاجة إلى التهيئة بالكامل قبل استدعاء into_inner .

تقول أيضًا أنه مقابل get_mut ، لكن مناقشة RFC التي رغب بها بعض الأشخاص لتخفيف القيود هنا. هذا خيار يمكنني العيش معه. ولكن ليس مقابل into_inner .

أخشى أن كل هذه الاستخدامات لـ uninitialized يجب مراجعتها بعناية أكبر ، وفي الواقع كان هذا أحد أهداف RFC. نود أن يكون النظام البيئي الأوسع أكثر حذرًا هنا ، إذا استخدم الجميع فقط into_inner الفور ، فإن RFC كان عديم القيمة.

نود أن يكون النظام البيئي الأوسع أكثر حذرًا هنا ، إذا استخدم الجميع into_inner الفور ، فإن RFC كان عديم القيمة.

هذا يعطيني فكرة ... ربما يجب علينا فحص (المجموعة: "الصواب") لهذا النوع من التعليمات البرمجية؟ سم مكعب @ أولي obk

أقوم الآن بإصلاح (عدة) تحذيرات الإهمال

يجب أن نشحن Nightly بهذه التحذيرات فقط بمجرد أن يكون البديل الموصى به متاحًا على الأقل في Stable. انظر مناقشة مماثلة على https://github.com/rust-lang/rust/pull/52994#issuecomment -411413493

تضمين التغريدة

قد نرغب في استثناء "الأنواع التي تكون فيها جميع أنماط البت على ما يرام" (أنواع الأعداد الصحيحة بشكل أساسي)

لقد شاركت في مناقشة حول هذا من قبل ، لكنني سأرسل هنا للتوزيع على نطاق أوسع: هذا بالفعل شيء لدينا العديد من حالات الاستخدام الحالية في الفوشيه ، ولدينا سمة لهذا ( FromBytes ) واشتقاق ماكرو لهذه الأنواع. كان هناك أيضًا مواد داخلية Pre-RFC لإضافتها إلى المكتبة القياسية (ccgnzlbgjoshlf).

سأعارض إجراء مثل هذا الاستثناء لأن undef ليس نمط بت عادي.

نعم ، هذا جانب يختلف فيه mem::zeroed() بشكل كبير عن mem::uninitialized() .

تضمين التغريدة

لقد شاركت في مناقشة حول هذا من قبل ، لكنني سأرسل هنا للتوزيع على نطاق أوسع: هذا بالفعل شيء لدينا العديد من حالات الاستخدام الحالية في الفوشيه ، ولدينا سمة لهذا (FromBytes) واشتقاق ماكرو لهذه الأنواع. كان هناك أيضًا مواد داخلية Pre-RFC لإضافتها إلى المكتبة القياسية (ccgnzlbgjoshlf).

كانت تلك المناقشات حول طرق السماح بأمان memcpy s عبر الأنواع ، لكنني أعتقد أن هذا متعامد إلى حد كبير فيما إذا كانت الذاكرة التي يتم نسخها قد تمت تهيئتها أم لا - إذا وضعت ذاكرة غير مهيأة في الداخل ، فستخرج ذاكرة غير مهيأة.

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

IIRC ، لم يقترح أحد أو يناقش أي نهج يمكنك من خلاله وضع ذاكرة غير مهيأة والحصول على ذاكرة أولية ، لذلك لا أتابع ما يجب أن تفعله هذه المناقشات مع هذا. بالنسبة لي فهي متعامدة تمامًا.

لتوجيه النقطة إلى المنزل أكثر قليلاً ، تعرف LLVM البيانات غير المهيأة على أنها Poison ، والتي تختلف عن "بعض أنماط البت العشوائية ولكن الصالحة". التفرع بناءً على قيمة السم أو استخدامه لحساب عنوان تم إلغاء الإشارة إليه بعد ذلك هو UB. لذلك ، لسوء الحظ ، "الأنواع التي تكون فيها جميع أنماط البت على ما يرام" لا تزال غير آمنة للبناء لأن استخدامها بدون تهيئتها بشكل منفصل سيكون UB.

صحيح ، آسف ، كان علي توضيح ما قصدته. كنت أحاول أن أقول إن "الأنواع التي تكون جميع أنماط البت فيها على ما يرام" هي بالفعل شيء نهتم بتعريفه لأسباب أخرى. مثل RalfJung قال أعلاه ،

سأعارض إجراء مثل هذا الاستثناء لأن undef ليس نمط بت عادي.

الحمد لله هناك أناس يستطيعون القراءة ، لأنني على ما يبدو لا أستطيع ...

حسنًا ، ما قصدت قوله هو: لدينا بالتأكيد أنواعًا تكون فيها جميع أنماط البت التي تمت تهيئتها على ما يرام - جميع أنواع i* و u* ، المؤشرات الأولية ، أعتقد أن f* وكذلك ثم tuple / Structs تتكون فقط من هذه الأنواع.

ما هو السؤال المفتوح في أي ظروف يسمح لأي من هذه الأنواع أن تكون غير مهيأة ، أي السم. إجابتي المفضلة هي "أبدًا".

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

قراءة المساحة المتروكة للبايت كـ MaybeUninit<u8> يجب أن تكون جيدة.

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

قراءة مساحة البايت على أنها ربما Uninitيجب ان يكون بخير.

كانت المناقشة باختصار حول تقديم سمة ، Compatible<T> ، باستخدام طريقة آمنة fn safe_transmute(self) -> T التي "تعيد ترجمة" / "memcpys" بتات self إلى T . ضمان هذه الطريقة هو أنه إذا تمت تهيئة self بشكل صحيح ، فسيكون الناتج T . تم اقتراح أن يقوم المترجم بملء عمليات التنفيذ المتعدية تلقائيًا ، على سبيل المثال ، إذا كان هناك impl Compatible<V> for U ، و impl Compatible<W> for V فهناك impl Compatible<W> for U (إما لأنه تم توفيره يدويًا ، أو ينشئه المترجم تلقائيًا - كيف يمكن تنفيذ ذلك تم التلويح باليد بالكامل).

تم اقتراح أنه يجب أن يكون unsafe لتنفيذ السمة: إذا قمت بتطبيقها لـ T الذي يحتوي على وحدات بايت متروكة حيث يحتوي Self على حقول ، فسيكون كل شيء على ما يرام على الأقل حتى تحاول استخدام T وينتهي سلوك برنامجك اعتمادًا على محتويات الذاكرة غير المهيأة.

ليس لدي أي فكرة عن علاقة أي من هذا بـ MaybeUninit<u8> ، ربما يمكنك توضيح ذلك؟

الشيء الوحيد الذي يمكنني تخيله هو أنه يمكننا إضافة ضمانة شاملة: unsafe impl<T> Compatible<[MaybeUninit<u8>; size_of::<T>()]> for T { ... } نظرًا لأن نقل أي نوع إلى حجم [MaybeUninit<u8>; N] آمن لجميع الأنواع. لا أعرف مدى فائدة مثل هذا الضمير ، بالنظر إلى أن MaybeUninit هو اتحاد ، وأي شخص يستخدم [MaybeUninit<u8>; N] ليس لديه فكرة عما إذا كان عنصر معين من المصفوفة قد تمت تهيئته أم لا .

gnzlbg حينها كنت تتحدث عن FromBits<T> for [u8] . هذا هو المكان الذي أقول فيه أنه يتعين علينا استخدام [MaybeUninit<u8>] بدلاً من ذلك.

لقد ناقشت هذا الاقتراح مع nikomatsakis في RustConf ، وشجعني على المضي قدمًا في RFC. كنت سأفعل ذلك في غضون أسابيع قليلة ، ولكن إذا كان هناك اهتمام ، يمكنني محاولة إنجازه في نهاية هذا الأسبوع. هل سيكون ذلك مفيدًا لهذه المناقشة؟

@ joshlf ما هو الاقتراح الذي تتحدث عنه؟

تضمين التغريدة

gnzlbg حينها كنت تتحدث عن FromBitsلـ [u8]. هذا هو المكان الذي أقول أنه يتعين علينا استخدام [ربما Uninit] في حين أن.

مسكتك ، أوافق تماما هنا. لقد نسيت تمامًا أننا أردنا أيضًا القيام بذلك 😆

@ joshlf ما هو الاقتراح الذي تتحدث عنه؟

اقتراح FromBits / IntoBits . TLDR: يعني T: FromBits<U> أن أي نمط بت صالح U يتوافق مع T صالح. U: IntoBits<T> يعني نفس الشيء. يستنتج المترجم تلقائيًا كلا الأزواج من الأنواع وفقًا لقواعد معينة ، وهذا يفتح الكثير من الأشياء الممتعة التي تتطلب حاليًا unsafe . هناك مسودة من RFC كتبتها هنا منذ فترة ، لكنني أنوي تغيير أجزاء كبيرة منها ، لذا لا تأخذ هذا النص على أنه أكثر من مجرد دليل تقريبي.

joshlf أعتقد أن مثل هذا الزوج من السمات من شأنه أن يبني على قمة هذا النقاش أكثر من كونه جزءًا منه. AFAIK لدينا سؤالان مفتوحان من حيث الصلاحية:

  • هل يتكرر أدناه المراجع؟ أعتقد أن الأمر لا ينبغي أن يكون أكثر فأكثر ، حيث نرى المزيد من الأمثلة. لذلك من المحتمل أن نتكيف مع مستندات MaybeUninit::get_mut وفقًا لذلك (ليس من UB في الواقع استخدام ذلك قبل إتمام التهيئة ، ولكن من UB إلغاء الإشارة إليه قبل إكمال التهيئة). ومع ذلك ، يتعين علينا أولاً اتخاذ هذا القرار للتأكد من صحته ، ولست متأكدًا من المكان المناسب لذلك. ربما RFC مخصصة؟
  • هل يجب تهيئة u8 (وأنواع الأعداد الصحيحة الأخرى ، الفاصلة العائمة ، المؤشر الخام) ، على سبيل المثال ، هو MaybeUinit<u8>::uninitialized().into_inner() insta-UB؟ أعتقد ذلك ، ولكن استنادًا إلى الشعور الغريزي بأننا نريد الاحتفاظ بالأماكن التي نسمح فيها poison / undef إلى الحد الأدنى. ومع ذلك ، يمكن إقناعي بخلاف ذلك إذا كان هناك الكثير من الاستخدامات لهذا النمط (وآمل أن أستخدم ميري للمساعدة في تحديد ذلك).

هل يتكرر أدناه المراجع؟

RalfJung هل يمكنك إظهار مثال لما

هل يجب تهيئة u8 (وأنواع الأعداد الصحيحة الأخرى ، النقطة العائمة ، المؤشر الأولي) ، أي ، ربماUinit:: غير مهيأ (). into_inner () insta-UB؟

ماذا يحدث إذا لم يكن UB فوريًا؟ ماذا يمكنني أن أفعل بهذه القيمة؟ هل يمكنني المطابقة عليه؟ إذا كان الأمر كذلك ، فهل سلوك البرنامج حتمي؟

أشعر أنه إذا لم أتمكن من مطابقة القيمة دون تقديم UB ، فقد قمنا بإعادة اختراع mem::uninitialized . إذا كان بإمكاني المطابقة على القيمة ويتم دائمًا أخذ نفس الفرع عبر جميع البنى ومستويات الاختيار وما إلى ذلك ، فقد أعدنا اختراع mem::zeroed (ونستخدم نوعًا ما MaybeUninit اكتب الصورية بت). إذا لم يكن سلوك البرنامج حتميًا ، وتغير مع مستويات التحسين ، عبر البنى ، اعتمادًا على العوامل الخارجية (مثل ما إذا كان نظام التشغيل قد أعطى العملية للصفحات الصفرية) ، وما إلى ذلك ، فأنا أشعر أننا سنقدم مسدس قدم ضخم في لغة.

هل يجب تهيئة u8 (وأنواع الأعداد الصحيحة الأخرى ، الفاصلة العائمة ، المؤشر الخام) ، على سبيل المثال ، هو MaybeUinit<u8>::uninitialized().into_inner() insta-UB؟ أعتقد ذلك ، ولكن يعتمد في الغالب على الشعور الغريزي بأننا نريد الاحتفاظ بالأماكن التي نسمح فيها بدولار poison / undef إلى الحد الأدنى. ومع ذلك ، يمكن إقناعي بخلاف ذلك إذا كان هناك الكثير من الاستخدامات لهذا النمط (وآمل أن أستخدم ميري للمساعدة في تحديد ذلك).

FWIW ، اثنان من مزايا هذا ليس UB هي أنه أ) يتماشى مع ما يفعله LLVM ، و ، ب) أنه يسمح بمزيد من التحسينات على الكتابة. يبدو أيضًا أكثر اتساقًا مع اقتراحك الأخير لتعريف السلامة في وقت الاستخدام ، وليس في وقت البناء.

ماذا يحدث إذا لم يكن UB فوريًا؟ ماذا يمكنني أن أفعل بهذه القيمة؟ هل يمكنني المطابقة عليه؟ إذا كان الأمر كذلك ، فهل سلوك البرنامج حتمي؟

أشعر أنه إذا لم أتمكن من مطابقة القيمة دون تقديم UB ، فقد قمنا بإعادة اختراع mem::uninitialized . إذا كان بإمكاني المطابقة على القيمة ويتم دائمًا أخذ نفس الفرع عبر جميع البنيات ومستويات الاختيار وما إلى ذلك ، فقد أعدنا اختراع mem::zeroed (ونستخدم نوعًا ما MaybeUninit اكتب الصورية بت). إذا لم يكن سلوك البرنامج حتميًا ، وتغير مع مستويات التحسين ، عبر البنى ، اعتمادًا على العوامل الخارجية (مثل ما إذا كان نظام التشغيل قد أعطى العملية للصفحات الصفرية) ، وما إلى ذلك ، فأنا أشعر أننا سنقدم مسدس قدم ضخم في لغة.

لماذا تريد أن تكون قادرًا على المطابقة مع شيء غير مهيأ؟ إن تعريفه على أنه UB للتفرع أو الفهرس استنادًا إلى قيم غير مهيأة يوفر لـ LLVM مساحة أكبر للتحسين ، لذلك لا أعتقد أن تكبيل يديه أكثر فكرة جيدة ، خاصة إذا لم تكن هناك حالة استخدام مقنعة.

لماذا تريد أن تكون قادرًا على المطابقة مع شيء غير مهيأ؟

لم أقل أنني أرغب في ذلك ، لقد ذكرت أنه إذا تعذر القيام بذلك ، فأنا لا أفهم الفرق بين MaybeUinit<u8>::uninitialized().into_inner() و mem::uninitialized() .

RalfJung هل يمكنك إظهار مثال لما

السؤال الأساسي هو ما إذا كنا نسمح بما يلي:

let mut b = MaybeUninit::<bool>::uninitialized();
let bref = b.get_mut(); // insta-UB?

إذا قررنا أن المرجع صالحًا فقط إذا كان يشير إلى شيء صالح (هذا ما أعنيه ب "تكرار المراجع أدناه") ، فهذا الرمز هو UB.

ماذا يحدث إذا لم يكن UB فوريًا؟ ماذا يمكنني أن أفعل بهذه القيمة؟ هل يمكنني المطابقة عليه؟ إذا كان الأمر كذلك ، فهل سلوك البرنامج حتمي؟

لا يمكنك فحص u8 غير مهيأ بأي طريقة. match بالعديد من الأشياء ، سواء كانت أسماء ملزمة أو اختبارًا فعليًا للمساواة ؛ الأول على ما يرام ولكن الأخير لا. ولكن هل يمكن أن يكتب مرة أخرى إلى الذاكرة.

بشكل أساسي ، هذا ما تنفذه ميري حاليًا.

أشعر أنه إذا لم أتمكن من مطابقة القيمة دون تقديم UB ، فسنقوم بإعادة اختراع mem :: uninitialized.

لماذا ذلك؟ كانت المشكلة الأكبر في mem::uninitialized بالأنواع التي لها قيود على قيمها الصالحة. يمكننا أن نقرر أن u8 ليس له مثل هذه القيود ، لذلك mem::uninitialized() كان جيدًا مقابل u8 . كان من المستحيل تقريبًا استخدامه بشكل صحيح في الكود العام ، لذلك من الأفضل التخلص منه تمامًا.
في كلتا الحالتين ، لا يزال من غير المقبول تمرير u8 غير مهيأ إلى رمز آمن ، ولكن قد يكون من الجيد استخدامه بعناية في رمز غير آمن.

لا يمكنك "المطابقة" على &mut للإشارة إلى بيانات غير صالحة أيضًا. IOW ، أعتقد أن المثال bool الذي قدمته أعلاه جيد ، لكن ما يلي بالتأكيد ليس كذلك:

let mut b = MaybeUninit::<bool>::uninitialized();
let bref = b.get_mut();
match bref {
  &b => // insta-UB! We have a bad bool in scope.
}

هذا هو استخدام match لعمل إشارة مرجعية عادية للمؤشر.

FWIW ، اثنان من مزايا هذا ليس UB هي أنه أ) يتماشى مع ما يفعله LLVM ، و ، ب) أنه يسمح بمزيد من التحسينات على الكتابة. يبدو أيضًا أكثر اتساقًا مع اقتراحك الأخير لتعريف السلامة في وقت الاستخدام ، وليس في وقت البناء.

ما هي التحسينات التي سيسمح بها هذا؟
لاحظ أن LLVM تقوم بالتحسينات على رمز غير مكتوب بشكل أساسي ، لذلك لا يوجد أي من هذا مصدر قلق هناك. نحن نتحدث فقط عن تحسينات MIR هنا.

أنا في الأساس أقوم بتشكيل المنظور الذي يجب أن نسمح به بأقل قدر ممكن حتى يكون لدينا استخدام واضح. يمكننا دائمًا السماح بالمزيد من الأشياء لاحقًا ، ولكن ليس العكس. ومع ذلك ، ظهرت مؤخرًا بعض الاستخدامات الجيدة لشرائح البايت التي يمكن أن تكون قديمة لأي بيانات ، والتي قد تكون كافية للحجة للقيام بذلك على الأقل مقابل u* و i* .

إذا قررنا أن المرجع صالحًا فقط إذا كان يشير إلى شيء صالح (هذا ما أعنيه ب "تكرار المراجع أدناه") ، فهذا الرمز هو UB.

مسكتك.

أكبر مشكلة في mem :: uninitized كانت حول الأنواع التي لها قيود على ماهية قيمها الصالحة.

mem::uninitialized أيضًا المشكلة التي أشرت إليها أعلاه في

let mut b = MaybeUninit::<u8>::uninitialized().into_inner();
let bref = &mut b; // Insta UB ?

اعتقدت أن أحد أسباب تقديم MaybeUninit هو تجنب هذه المشكلة من خلال تهيئة الاتحاد دائمًا (على سبيل المثال للوحدة) ، مما يسمح لك بأخذ إشارة إليه ، وتغيير محتوياته ، على سبيل المثال. الحقل النشط إلى u8 وإعطائه قيمة عبر ptr::write دون إدخال UB.

لذلك أنا في حيرة من أمري. لا أرى كيف يكون into_inner أفضل من:

let mut b: u8 = uninitialized();
let bref = &mut b; // Insta UB ? 

كلاهما يبدو وكأنه قنابل موقوتة غير محددة السلوك بالنسبة لي.

ما هي التحسينات التي سيسمح بها هذا؟
لاحظ أن LLVM تقوم بالتحسينات على رمز غير مكتوب بشكل أساسي ، لذلك لا يوجد أي من هذا مصدر قلق هناك. نحن نتحدث فقط عن تحسينات MIR هنا.

إذا قلنا أن الذاكرة غير المحددة لها بعض القيمة ، وبالتالي يُسمح لك بالتفرع عليها وفقًا لدلالات Rust ، فلا يمكننا خفضها إلى إصدار LLVM من غير محدد ، لأنه سيكون غير سليم.

أنا في الأساس أقوم بتشكيل المنظور الذي يجب أن نسمح به بأقل قدر ممكن حتى يكون لدينا استخدام واضح. يمكننا دائمًا السماح بالمزيد من الأشياء لاحقًا ، ولكن ليس العكس.

هذا عادل.

ومع ذلك ، ظهرت مؤخرًا بعض الاستخدامات الجيدة لشرائح البايت التي يمكن أن تكون قديمة لأي بيانات ، والتي قد تكون كافية للحجة للقيام بذلك على الأقل مقابل u* و i* .

هل تتضمن أي من حالات الاستخدام هذه وجود شرائح بايت تحتوي على قيم غير مهيأة؟

مكان واحد يمكن أن تكون قيمته غير مهيأة ولكن غير مسمومة &mut [u8] هو Read::read - نود أن نكون قادرين على تجنب الحاجة إلى صفر من المخزن المؤقت لمجرد أن بعض الأشياء الغريبة Read impl القراءة منه بدلاً من مجرد الكتابة فيه.

مكان واحد يمكن أن تكون قيمته غير مهيأة ولكن غير مسمومة &mut [u8] هو Read::read - نود أن نكون قادرين على تجنب الحاجة إلى صفر من المخزن المؤقت لمجرد أن بعض الأشياء الغريبة Read impl القراءة منه بدلاً من مجرد الكتابة فيه.

أرى أن الفكرة هي أن MaybeUninit سيمثل نوعًا تمت تهيئته ، ولكن بمحتويات غير محددة ، في حين أن الأنواع الأخرى من البيانات غير المهيأة (على سبيل المثال ، حقول الحشو) ستظل غير مهيأة تمامًا بمعنى سم LLVM؟

لا أعتقد أنه سيحتاج إلى التقديم على ربما Uninit بشكل عام. قد يكون هناك نظريًا بعض واجهة برمجة التطبيقات لـ "تجميد" المحتويات من غير محدد إلى محدد ولكن عشوائي.

إذا قلنا أن الذاكرة غير المحددة لها بعض القيمة ، وبالتالي يُسمح لك بالتفرع عليها وفقًا لدلالات Rust ، فلا يمكننا خفضها إلى إصدار LLVM من غير محدد ، لأنه سيكون غير سليم.

لم يكن هذا هو الاقتراح. إنه وسيظل UB للتفرع على poison .

السؤال هو ما إذا كان من UB مجرد "امتلاك" poison محلي u8 .

هل تتضمن أي من حالات الاستخدام هذه وجود شرائح بايت تحتوي على قيم غير مهيأة؟

الشرائح مثل المراجع ، لذا فإن &mut [u8] من البيانات غير المهيأة لا بأس به طالما تمت كتابتها فقط (على افتراض أن هذا هو الحل الذي نتخذه للتحقق من صحة المرجع).

تضمين التغريدة

هناك مكان واحد يمكن أن يكون فيه استخدام غير مهيأ ولكن ليس سامًا ومحولًا [u8] ذا قيمة هو قراءة :: قراءة - نود أن نكون قادرين على تجنب الحاجة إلى صفر من المخزن المؤقت لمجرد أن بعض الضمانات القراءة الغريبة يمكن أن تقرأ منه بدلاً من مجرد الكتابة فيه.

حسنًا ، بدون &out لن تتمكن من فعل ذلك إلا إذا كنت تعرف الضمني. السؤال ليس ما إذا كان يجب أن يتعامل الرمز الآمن مع poison في u8 (ليس كذلك ، هذا ليس استخدامًا جيدًا للرمز الآمن!) ، السؤال هو ما إذا كان الرمز غير الآمن يمكنه التعامل معه بعناية من هنا. (انظر منشور المدونة الذي أردت كتابته اليوم حول التمييز بين ثوابت السلامة وثوابت الصلاحية ...)

ربما تأخرت ، لكنني أقترح تغيير توقيع طريقة set() لإرجاع &mut T . بهذه الطريقة ، سيكون من الآمن كتابة رمز آمن تمامًا يعمل مع MaybeUninit (على الأقل في بعض المواقف).

fn init(dest: &mut MaybeUninit<u8>) -> &mut u8 {
    dest.set(produce_value())
}

هذا عمليا ضمان ثابت بأن init() سيهيئ القيمة أو يتباعد. (إذا حاولت إرجاع شيء آخر ، فسيكون العمر خاطئًا وسيكون &'static mut u8 مستحيلًا في الكود الآمن.) ربما يمكن استخدامه كجزء من placer API في المستقبل.

Kixunil لقد كان الأمر على هذا النحو من قبل ، وأنا أوافق على أنه لطيف. أجد نفس set محيرًا لوظيفة تُرجع شيئًا ما.

تضمين التغريدة

هذا عمليا ضمان ثابت بأن init() سيهيئ القيمة أو يتباعد. (إذا حاولت إرجاع شيء آخر ، فسيكون العمر خاطئًا وسيكون &'static mut u8 مستحيلًا في الكود الآمن.)

ليس تماما؛ يمكنك الحصول على واحدة Box::leak .

في قاعدة بيانات كتبتها مؤخرًا ، توصلت إلى مخطط مشابه ؛ إنه أكثر تعقيدًا بعض الشيء ، لكنه يوفر ضمانًا ثابتًا حقيقيًا بتهيئة المرجع المقدم. بدلا من

fn init(dest: &mut MaybeUninit<u8>) -> &mut u8

عندي

fn init<'a>(dest: Uninitialized<'a, u8>) -> DidInit<'a, u8>

الحيلة هي أن Uninitialized و DidInit كلاهما ثابتان في معلمات العمر ، لذلك لا توجد طريقة لإعادة استخدام DidInit بمعامل حياة مختلف ، حتى على سبيل المثال 'static .

يتضمن DidInit Deref و DerefMut ، لذلك يمكن أن يستخدمه الرمز الآمن كمرجع ، كما في المثال الخاص بك. لكن ضمان أن هذا المرجع الأصلي الذي تم تمريره هو الذي تمت تهيئته ، وليس مرجعًا آخر عشوائيًا ، مفيد في التعليمات البرمجية غير الآمنة . هذا يعني أنه يمكنك تحديد المُهيّئات هيكليًا:

struct Foo {
    a: i32,
    b: u8,
}

fn init_foo<'a>(dest: Uninitialized<'a, Foo>,
                init_a: impl for<'x> FnOnce(Uninitialized<'x, i32>) -> DidInit<'x, i32>,
                init_b: impl for<'x> FnOnce(Uninitialized<'x, u8>) -> DidInit<'x, u8>)
                -> &'a mut DidInit<'a, Foo> {
    let ptr: *mut Foo = dest.ptr;
    unsafe {
        init_a(Uninitialized::new(&mut (*ptr).a));
        init_b(Uninitialized::new(&mut (*ptr).b));
        dest.did_init()
    }
}

تقوم هذه الوظيفة بتهيئة مؤشر لـ Struct Foo خلال تهيئة كل حقل من حقولها بدوره ، باستخدام عمليات رد نداء التهيئة المقدمة من المستخدم. يتطلب أن تقوم عمليات الاسترجاعات بإرجاع DidInit s ، لكنها لا تهتم بقيمها ؛ يكفي حقيقة وجودهم. بمجرد أن تتم تهيئة جميع الحقول ، فإنها تعلم أن Foo بالكامل صالح - لذلك يستدعي did_init() على Uninitialized<'a, Foo> ، وهي طريقة غير آمنة تلقيها فقط إلى المقابل من النوع DidInit ، والذي يعود بعد ذلك init_foo

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

على أي حال ، أتساءل عما إذا كان يمكن تنفيذ شيء مثل هذا في المكتبة القياسية.

رابط الملعب

(ملاحظة: DidInit<'a, T> هو في الواقع اسم مستعار من النوع &'a mut _DidInitMarker<'a, T> ، لتجنب مشاكل العمر مع DerefMut .)

بالمناسبة ، في حين أن النهج المرتبط أعلاه يتجاهل المدمرات ، فإن طريقة مختلفة قليلاً تتمثل في جعل DidInit<‘a, T> مسؤولاً عن تشغيل أداة التدمير T . في هذه الحالة ، يجب أن يكون اسمًا هيكليًا وليس اسمًا مستعارًا ؛ ويمكنه فقط توزيع المراجع إلى T التي تعيش طالما DidInit نفسها ، وليس لجميع ’a (لأنه بخلاف ذلك يمكنك الاستمرار في الوصول إليها بعد التدمير).

+1 لتضمين طريقة لإعطاء السلوك الذي طلبته سابقًا في set ، لكن لا بأس من إتاحتها عبر اسم آخر.

أي أفكار جيدة لما يمكن أن يكون هذا الاسم؟ set_and_as_mut ؟ ^^

set_and_borrow_mut ؟

insert / insert_mut ؟ يحتوي النوع Entry على طريقة or_insert مشابهة إلى حد ما (لكن OccupiedEntry يحتوي أيضًا على insert الذي يُرجع القيمة القديمة ، لذلك هذا ليس متشابهًا على الإطلاق).

هل هناك حقًا سبب مقنع لوجود طريقتين منفصلتين؟ يبدو الأمر بسيطًا بما يكفي لتجاهل قيمة الإرجاع ، وأتصور أنه سيتم تمييز الوظيفة على أنها #[inline] لذلك لا أتوقع أي تكلفة حقيقية لوقت التشغيل.

هل هناك حقًا سبب مقنع لوجود طريقتين منفصلتين؟ يبدو بسيطًا بما يكفي لتجاهل القيمة المعادة

أعتقد أن السبب الوحيد هو أن رؤية set يعود شيئًا مثيرًا للدهشة.

ربما أفتقد شيئًا ما ، لكن ما الذي يمكن أن ينقذنا من وجود قيمة غير صالحة؟ يعني لو كنا

let mut foo: MaybeUninit<T> = MaybeUninit {
    uninit: (),
};
let mut foo_ref = &mut foo as *mut MaybeUninit<T>;

unsafe {
    some_native_function(&mut (*foo_ref).value, val);
}

ماذا لو كان some_native_function غير عملي ولا يقوم بالفعل بتهيئة القيمة؟ هل لا يزال UB؟ كيف يمكن التعامل معها؟

Pzixel يتم تغطية هذا كله من خلال وثائق API لـ MaybeUninit .

إذا كان some_native_function عبارة عن NOP ، فلن يحدث شيء ؛ إذا استخدمت لاحقًا foo_ref.value (أو بالأحرى استخدم foo_ref.value foo_ref.as_mut() حيث يمكنك استخدام واجهة برمجة التطبيقات العامة فقط) ، فهذا يعني UB لأنه لا يمكن استدعاء الوظيفة إلا بمجرد تهيئة كل شيء.

MaybeUninit لا يمنع وجود قيم غير صالحة - إذا أمكن ، فسيكون آمنًا ، لكن هذا غير ممكن. ومع ذلك ، فإنه يجعل العمل مع القيم غير الصالحة أقل من مسدس القدم لأنه الآن يتم تشفير المعلومات التي قد تكون القيمة غير صالحة في النوع ، ليراها كل من المترجم والمبرمج.

أردت توثيق محادثة IRC أجريتها مع sfackler بخصوص مشكلة افتراضية يمكن أن تنشأ في المستقبل.

السؤال الرئيسي هو ما إذا كان mem::zeroed هو تمثيل صالح في الذاكرة لاقتراح التنفيذ الحالي لـ MaybeUninit<NonZeroU8> . في تفكيري ، في حالة "uninit" ، تكون القيمة عبارة عن مساحة فقط ، والتي يمكن للمترجم استخدامها لأي غرض ، وفي حالة "القيمة" ، تكون جميع القيم الممكنة باستثناء mem::zeroed صالحة (بسبب NonZero ).

قد يخزن نظام تخطيط النوع المستقبلي الذي يحتوي على حزم تمييز تعداد أكثر تقدمًا (مما لدينا الآن) مميِّزًا في حشو الحالة "غير المنتهية" / الذاكرة الصفرية في حالة "القيمة". في هذا النظام الافتراضي ، حجم Option<MaybeUninit<NonZeroU8>> هو 1 ، بينما هو حاليًا 2. علاوة على ذلك ، في هذا النظام الافتراضي ، لا يمكن تمييز Some(MaybeUninit::uninitialized()) عن None . أعتقد أنه يمكننا على الأرجح إصلاح هذا عن طريق تغيير تطبيق MaybeUninit (لكن ليس واجهة برمجة التطبيقات العامة) بمجرد الانتقال إلى مثل هذا النظام.

لا أرى فرقًا بين NonZeroU8 و &'static i32 في هذا الصدد. كلا النوعين من الأنواع التي يكون فيها "0" غير صالح . لذلك بالنسبة لكليهما ، فإن MaybeUninit<T>::zeroed().into_inner() هو insta-UB.

يعتمد ما إذا كان بإمكان Option<Union> إجراء تحسينات التخطيط على صلاحية الاتحاد. لم يتم تحديد ذلك بعد في جميع الحالات ، ولكن هناك اتفاق عام على أنه بالنسبة للنقابات التي لديها متغير من النوع () ، فإن أي نمط بت صالح وبالتالي لا يمكن تحسين التخطيط. يغطي هذا MaybeUninit . لذلك لن يكون للمقاس 1 Option<MaybeUninit<NonZeroU8>> .

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

هل هذه حالة خاصة "للنقابات التي لها متغير من النوع ()"؟ هل يؤدي تثبيت هذه الميزة إلى تثبيت هذا الجزء من Rust ABI بشكل ضمني؟ ماذا عن union يحتوي على struct UnitType; أو struct NewType(()); ؟ ماذا عن struct Padded (أدناه)؟ ماذا عن union يحتوي على struct Padded ؟

#[repr(C, align(4))]
struct Padded {
    a: NonZeroU8,
    b: (),
    c: NonZeroU16
}

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

سنجري مناقشة مناسبة لقياس الإجماع الحالي وربما نتوصل إلى اتفاق بشأن المزيد من الأشياء في إحدى المناقشات

هل يؤدي تثبيت هذه الميزة إلى تثبيت هذا الجزء من Rust ABI بشكل ضمني؟

نحن نتحدث هنا عن ثوابت الصلاحية ، وليس تخطيط البيانات (الذي أفترض أنك أشرت إليه عند طرح ABI). لذلك لن يؤدي أي من هذا إلى تثبيت أي مؤشر ABI. هذه مرتبطة ولكنها متميزة ، وفي الواقع هناك مناقشة مستمرة حول ABI للنقابات .

هذه مرتبطة ولكنها مميزة ، وفي الواقع هناك مناقشة مستمرة حول ABI للنقابات.

AFAICT أن المناقشة تدور حول تمثيل الذاكرة للنقابات فقط ، ولا تتضمن كيفية تمرير النقابات عبر حدود الوظيفة والأشياء الأخرى التي قد تكون ذات صلة بـ ABI. لا أعتقد أن الهدف من UCG repo هو إنشاء ABI لـ Rust.

حسنًا ، الهدف هو تحديد أشياء كافية للتفاعل مع C. أشياء مثل "Rust bool و C bool متوافقة مع ABI".

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

أشعر بالفضول لمعرفة ما إذا كانت هناك بعض الحجة ضد تحسين التخطيط Option<Foo> حيث يتم تعريف Foo على النحو التالي:

union Foo {
   bar: NonZeroUsize,
   baz: &'static str,
}

Kixunil هل يمكنك رفع ذلك على https://github.com/rust-rfcs/unsafe-code-guidelines/issues/13؟ سؤالك ليس متعلقًا بـ MaybeUninit .

أريد أن أعرف أي قسم سيحتوي على متغيرات ثابتة بدون تهيئة؟
في "C" أستطيع أن أكتب uint8_t a[100]; في مستوى عال من الملف، وأنا أعلم أن رمز سيتم طرحه للقسم .bss. إذا أنا أكتب uint8_t a[100] = {}; وسيتم وضع رمز لقسم .data (والذي سيتم نسخ من FLASH إلى RAM قبل رئيسي).

إنه مثال صغير في Rust يستخدم ربما Uninit:

struct A {
    data: MaybeUninit<[u8; 100]>,
    len: usize,
}

impl A {
    pub const fn new() -> Self {
        Self {
            data: MaybeUninit::uninitialized(),
            len: 0,
        }
    }
}

static mut a: MaybeUninit<[u8; 100]> = MaybeUninit::uninitialized();
static mut b: A = A::new();

القسم الذي سيكون يحتوي على وباء حرف؟

ملاحظة: أعرف شيئًا عن تشويش الرموز ولكن لا يهم هذا السؤال.

@ qwerty19106 في المثال الخاص بك ، سيتم وضع كل من a و b في .bss . تتعامل LLVM مع قيم undef ، مثل MaybeUninit::uninitialized() ، كأصفار عند تحديد القسم الذي سيدخل فيه المتغير.

إذا قام A::new() بتهيئة len إلى 1 ، فإن b سينتهي عند .data . إذا احتوى static على أي قيمة غير صفرية ، فإن المتغير سينتقل إلى .data . يتم التعامل مع المساحة المتروكة كقيم صفرية.

هذا ما تفعله LLVM. Rust لا يقدم أي ضمانات وعود (*) حول أي قسم رابط سوف يدخل فيه متغير static . إنه يرث سلوك LLVM فقط.

(*) ما لم تستخدم #[link_section]

حقيقة ممتعة: في مرحلة ما ، اعتبرت LLVM undef قيمة غير صفرية ، لذا فإن المتغير a في مثالك انتهى بـ .data . انظر # 41315.

شكرا japaric على إجابتك. كان ذلك مساعدا جدا لي.

لدي فكرة جديدة.
يمكن للمرء استخدام قسم .init_array لتهيئة المتغيرات الثابتة والمتغيرة قبل استدعاء main .

هذا دليل على المفهوم:

#[macro_export]
macro_rules! static_singleton {
    ($name_var: ident, $ty:ty, $name_init_fn: ident, $name_init_var: ident, $init_block: block) => {
        static mut $name_var: MaybeUninit<$ty> = unsafe {MaybeUninit::uninitialized()};

        extern "C" fn $name_init_fn() {
            unsafe {
                $init_block
            }
        }

        #[link_section = ".init_array"]
        #[used]
        static $name_init_var: [extern "C" fn(); 1] = [$name_init_fn];
    };
}

كود الاختبار :

static_singleton!(A, u8, a_init_fn, A_INIT_VAR, {
    let ptr = A.get_mut();
    *ptr = 5;
});

fn main() {
    println!("A inited to {}", unsafe {&A.get_ref()});
}

النتيجة : A inited to 5

المثال الكامل : الملعب

سؤال لم يتم حله :
لم أتمكن من استخدام concat_idents لإنشاء a_init_fn و A_INIT_VAR . يبدو أن # 1628 ليس جاهزًا للاستخدام بعد.

هذا الاختبار ليس مفيدًا جدًا. ولكن يمكن أن يكون مفيدا في جزءا لا يتجزأ من البنيات معقدة تهيئة (سيتم وضعها في .bss، مما يسمح لFLASH الاقتصاد).

لماذا rustc لا تستخدم قسم .init_array؟ إنه قسم قياسي بتنسيق ELF ( رابط ).

@ qwerty19106 لأن الحياة قبل main () تعتبر خطأً وتم استبعادها صراحةً من دلالات Rust.

حسنًا ، إنه تصميم لانج جيد.

لكن في # [no_std] ليس لدينا بديل جيد الآن (ربما كنت أبحث بشكل سيء).

يمكننا استخدام spin :: Once ، ولكنه مكلف للغاية ( Ordering :: SeqCst على كل مرجع يحصل).

أرغب في التحقق من وقت الترجمة المضمنة .

إنه مكلف للغاية ( Ordering::SeqCst عند الحصول على كل مرجع).

هذا لا يبدو صحيحًا بالنسبة لي. أليس من المفترض أن يتم تخفيف كل التجريدات "مرة واحدة" عند الوصول ومزامنتها عند التهيئة؟ أم أفكر في شيء آخر؟
تضمين التغريدة

@ qwerty19106 :

عندما تقول "مضمن" ، هل تشير إلى المعدن المكشوف؟ من الجدير بالذكر أن .init_array ليس في الحقيقة جزءًا من تنسيق ELF نفسه ¹ - إنه ليس جزءًا من System V ABI ² الذي يمتد ؛ فقط .init هو. لن تجد .init_array حتى تصل إلى تحديث مسودة System V ABI ، الذي يرث Linux ABI منه.

نتيجة لذلك ، إذا كنت تعمل على معدن مكشوف ، فقد لا يعمل .init_array حتى بشكل موثوق لحالة الاستخدام الخاصة بك - بعد كل شيء ، يتم تنفيذه على المعادن غير المكشوفة بواسطة الكود في المحمل الديناميكي و / أو libc. ما لم يتحمل محمل الإقلاع مسؤولية تشغيل الكود المشار إليه في .init_array ، فلن يفعل شيئًا على الإطلاق.

1: انظر الصفحة 28 ، الشكل 1-13 "الأقسام الخاصة"
2: انظر الصفحة 63 ، الشكل 4-13 "الأقسام الخاصة (تابع)"

eddyb تحتاج إلى تحميل Acquire على الأقل عند قراءة Once . هذا حمل عادي على x86 وحمل + سياج على ARM.

يستخدم التطبيق الحالي load(SeqCst) ، ولكن من الناحية العملية ، فإن هذا يولد نفس الشيء مثل load(Acquire) على جميع البنى.

(هل تمانع في نقل هذه المناقشات إلى مكان آخر؟ ليس لديهم أي علاقة بـ ربما Uninit vs mem :: غير مهيأ بعد الآن. كلاهما يتصرفان بنفس الطريقة التي تتصرف بها LLVM - إنشاء undef. ما يحدث مع ذلك undef لاحقًا ليس موضوعًا هنا. )

صباحًا. 13 سبتمبر 2018 00:59:20 MESZ schrieb Amanieu [email protected] :

eddyb تحتاج إلى تحميل Acquire على الأقل عند قراءة ملف
Once . هذا حمل عادي على x86 وحمل + سياج على ARM.

يستخدم التطبيق الحالي load(SeqCst) ، لكن هذا عمليًا
يولد نفس الاسم مثل load(Acquire) على جميع البنى.

-
أنت تتلقى هذا لأنه تم ذكرك.
قم بالرد على هذا البريد الإلكتروني مباشرةً أو قم بعرضه على GitHub:
https://github.com/rust-lang/rust/issues/53491#issuecomment -420825802

MaybeUninit إلى المستوى الرئيسي وسيكون في الليلة التالية. :)

يقترح https://github.com/rust-lang/rust/issues/54470 استخدام Box<[MaybeUninit<T>]> في RawVec<T> . لتمكين هذا وربما مجموعات أخرى مثيرة للاهتمام مع مربعات وشرائح مع عدد أقل من التحويلات ، ربما يمكننا إضافة المزيد من واجهات برمجة التطبيقات إلى المكتبة القياسية؟

على وجه الخصوص للتخصيص دون التهيئة (أعتقد أن Box::new(MaybeUninit::uninitialized()) سيظل ينسخ size_of::<T>() بايت حشو؟):

impl<T> Box<MaybeUninit<T>> {
    pub fn new_uninit() -> Self {…}
    pub unsafe fn assert_init(s: Self) -> Box<T> { transmute(s) }
}

impl<T> Box<[MaybeUninit<T>]> {
    pub fn new_uninit_slice(len: usize) -> Self {…}
    pub unsafe fn assert_init(s: Self) -> Box<[T]> { transmute(s) }
}

في core::slice / std::slice ، يمكن استخدامها بعد أخذ شريحة فرعية:

pub unsafe fn assert_init<T>(s: &[MaybeUninit<T>]) -> &[T] { transmute(s) }
pub unsafe fn assert_init_mut<T>(s: &mut [MaybeUninit<T>]) -> &mut [T] { transmute(s) }

أعتقد أن Box :: new (mightUninit :: uninitialized ()) سيظل ينسخ size_of ::() المساحة المتروكة بايت

لا ينبغي ، وهناك اختبار كودجين يهدف إلى اختبار ذلك.

لا يلزم نسخ وحدات البايت المتروكة نظرًا لأن تمثيل البت الخاص بها لا يهم (أي شيء قد يلاحظ تمثيل البت هو UB على أي حال).

حسنًا ، فربما يكون Box::new_uninit غير ضروري؟ إصدار الشريحة مختلف ، على الرغم من أن Box::new يتطلب T: Sized .

أود أن أدافع عن أن يكون MaybeUninit::zeroed const fn . هناك بعض الاستخدامات المتعلقة بالمؤسسة المالية الأجنبية التي سأستخدمها لها (على سبيل المثال ، الثابت الذي يجب تهيئته إلى الصفر) وأعتقد أن الآخرين قد يجدونها مفيدة. يسعدني أن أتطوع بوقتي في وظيفة zeroed .

mjbshaw ستحتاج إلى استخدام #[rustc_const_unstable(feature = "const_maybe_uninit_zeroed")] لذلك نظرًا لأن zeroed يقوم بأشياء لا min_const_fn الشيك (https://github.com/rust-lang/ rust / issues / 53555) مما يعني أن ثبات MaybeUninit::zeroed سيكون غير مستقر حتى لو كانت الوظيفة مستقرة.

هل يمكن تقسيم تنفيذ / تثبيت هذا إلى خطوتين ، من أجل إتاحة نوع ربما Uninit للنظام البيئي الأوسع في وقت أقرب؟ يمكن أن تكون الخطوات:

1) أضف ربما Uninit
2) تحويل جميع استخدامات mem :: uninitialized / المصفرة والإهمال

تضمين التغريدة

أضف ربما Uninit

https://doc.rust-lang.org/nightly/core/mem/union.MaybeUninit.html :)

لطيف! إذن ، هل خطة تحقيق الاستقرار في ربما Uninit في أسرع وقت ممكن؟

الخطوة التالية هي معرفة سبب تراجع الأداء https://github.com/rust-lang/rust/pull/54668 بشكل سيء للغاية (في بعض المعايير). لن يكون لدي متسع من الوقت للنظر في ذلك هذا الأسبوع ، سأكون سعيدًا إذا تمكن شخص آخر من إلقاء نظرة. :د

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

ومع ذلك ، أفضل أيضًا عدم إضافة أي تأخيرات غير ضرورية ، حتى نتمكن في النهاية من إهمال مسدس القدم القديم. :)

أوه ، وشيء آخر حدث لي للتو ... مع هبوط https://github.com/rust-lang/rust/pull/54667 ، تحمي واجهات برمجة التطبيقات القديمة بالفعل من بعض أسوأ البنادق. أتساءل عما إذا كان بإمكاننا الحصول على بعض من ذلك مقابل MaybeUninit أيضًا؟ انها ليست عرقلة الاستقرار، ولكن يمكن أن نحاول أن نجد طريقة لجعل MaybeUninit::into_inner الذعر عندما دعا نوع غير مأهولة. في تصميمات تصحيح الأخطاء ، يمكنني أيضًا تخيل *x عند الذعر عندما x: &[mut] T مع T غير مأهولة.

تحديث الحالة: لإحراز تقدم مع https://github.com/rust-lang/rust/pull/54668 ، من المحتمل أن نحتاج إلى شخص ما لتعديل حساب التخطيط للنقابات. eddyb على استعداد لتقديم التوجيه ، لكننا نحتاج إلى شخص ما للقيام بالتنفيذ. :)

أعتقد أن الطريقة التي تنتقل من الغلاف ، وتستبدلها بقيمة غير مهيئة ، ستكون مفيدة:

pub unsafe fn take(&mut self) -> T

هل يجب علي تقديم هذا؟

shepmaster يبدو هذا مشابهًا جدًا into_inner . ربما يمكننا محاولة تجنب الازدواجية هنا؟

من المحتمل أيضًا أن تكون "الاستبدال بـ" هي الصورة الخاطئة هنا ، وهذا لا ينبغي أن يغير محتوى self على الإطلاق. يتم نقل الملكية فقط بعيدًا ، لذا فهي الآن بشكل فعال في نفس الحالة كما كانت عند إنشائها بدون تهيئة.

تغيير محتوى self على الإطلاق

بالتأكيد ، سيكون التنفيذ أساسًا ptr::read ، ولكن من وجهة نظر الاستخدام ، أود أن أشجع على تأطيرها كبديل لقيمة صالحة بقيمة غير مهيأة.

تجنب التكرار

ليس لدي اعتراض قوي ، حيث أتوقع تطبيق أحدهما على الآخر. أنا فقط لا أعرف ما ستكون عليه الحالة النهائية.

أشعر أن into_inner هو اسم وظيفة بريئة جدًا. الناس ، ربما بدون قراءة المستندات بعناية شديدة ، ما زالوا يفعلون MaybeUninit::uninitialized().into_inner() . هل يمكننا تغيير الاسم إلى شيء مثل was_initialized_unchecked أو يشير ذلك إلى أنه يجب عليك الاتصال بهذا الاسم فقط بعد تهيئة البيانات؟

أعتقد أن الأمر نفسه ينطبق على الأرجح على take .

بينما محرجًا بعض الشيء ، قد يعمل شيء مثل unchecked_into_initialized ؟

أم هل يجب إزالة هذه الطرق بالكامل ويعطي المستندات أمثلة بـ x.as_ptr().read() ؟

SimonSapin into_inner يستهلك self رغم أنه شيء جميل.

ولكن بالنسبة لـ shepmaster take ، فإن القيام بـ as_mut_ptr().read() سيفعل الشيء نفسه ... على الرغم من ذلك ، فلماذا تهتم بمؤشر قابل للتغيير؟

ماذا عن take_unchecked و into_inner_unchecked ؟

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

إن وضع كل من التأكيد على أنه يجب تهيئته ووصف ما يفعله (فك / إلى داخل / داخل / وما إلى ذلك) في اسم واحد يصبح أمرًا صعبًا إلى حد ما ، فكيف فقط تفعل الأول مع assert_initialized وترك الأخير ضمنيًا بالتوقيع؟ ممكن unchecked_assert_initialized لتجنب الإشارة إلى إجراء فحص وقت التشغيل مثل assert!() .

ممكن unhecked_assert_initialized لتجنب التضمين لفحص وقت التشغيل مثل تأكيد! () has.

نحن نفرق بين الافتراضات والتأكيدات بالفعل عبر intrinsics::assume(foo) مقابل assert!(foo) ، لذلك ربما assume_initialized ؟

assume هو واجهة برمجة تطبيقات غير مستقرة ، ومثال ثابت على الافتراض مقابل التأكيد هو unreachable_unchecked مقابل unreachable و get_unchecked مقابل get . لذلك أعتقد أن المصطلح الصحيح هو unchecked .

أود أن أقول إن foo_unchecked يكون منطقيًا فقط عندما يكون هناك foo ، وإلا فإن الطبيعة المطلقة للوظيفة التي تكون unsafe تشير إلي أن شيئًا "مختلفًا" سيحدث على.

من الواضح أن هذا الدراجة هو اللون الخطأ

باستخدام واجهة برمجة التطبيقات المحددة هذه ، رأينا بالفعل وسوف نستمر في رؤية المبرمجين وهم يفترضون أن عدم الأمان يرجع إلى أن "البيانات غير المهيأة هي بيانات محذوفة ، لذا يمكنك أن تتسبب في استخدام UB إذا كنت تتعامل معها بلا مبالاة" ، وليس المقصود "من UB استدعاء هذا على بيانات غير مهيأة ، فترة ". لا أعرف على وجه اليقين ما إذا كان يمكن القول إن هناك فائضًا عن الحاجة ⚠️ مثل unchecked سيساعد في ذلك ، لكنني أفضل أن أكون أكثر إرباكًا (= من المرجح أن يجعل الناس يسألون أو يقرأون المستندات حذر جدا).

تضمين التغريدة

أشعر أن into_inner هو اسم وظيفة بريئة جدًا. الناس ، ربما بدون قراءة المستندات بعناية شديدة ، ما زالوا يفعلون MaybeUninit::uninitialized().into_inner() . هل يمكننا تغيير الاسم إلى شيء مثل was_initialized_unchecked أو يشير ذلك إلى أنه يجب عليك الاتصال بهذا الاسم فقط بعد تهيئة البيانات؟

أحب هذه الفكرة حقًا . أشعر بقوة أنه يقول الشيء الصحيح عن الدلالات وأن هذا يحتمل أن يكون خطيرًا.

تضمين التغريدة

إن وضع كلٍّ من التأكيد على أنه يجب تهيئته ووصف ما يفعله (فك / إلى داخل / داخل / إلخ.) في اسم واحد يصبح أمرًا صعبًا إلى حد ما ، فكيف حول القيام بالأول بـ assert_initialized وترك الأخير ضمنيًا بالتوقيع؟ ممكن unchecked_assert_initialized لتجنب التضمين في فحص وقت التشغيل مثل assert!() .

ليس لدي أي مخاوف بشأن الأسماء الطويلة غير العملية للأشياء الخطرة. إذا كان هذا يجعل المزيد من الناس يفكرون مرتين ، فحتى was_initialized_into_inner_unchecked جيد تمامًا بالنسبة لي. جعل كتابة كود غير آمن (في حدود المعقول) ميزة وليست خطأ ؛)

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

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

shepmaster بالتأكيد ؛ أنا أستخدم IDE مع الإكمال التلقائي أيضًا ؛ لكنني أعتقد أن الاسم الأطول الذي يحتوي على unchecked بداخله متضمنًا داخل كتلة unsafe شأنه أن يمنحني بعض التوقف المؤقت الإضافي.

تضمين التغريدة

إن كتابتها من الذاكرة في نظام الدردشة ... أقل لطفًا (أنا نصف أمزح حول هذه النقطة ، لكن نصفها فقط).

سأقوم بهذه المقايضة. إذا كان الاسم مميزًا بعض الشيء ، فقد يجعله لا يُنسى. ؛)

أي من (أو الأسماء المشابهة التي تتضمن نفس الدلالات الدلالية):

  • was_initialized_unchecked
  • was_initialized_into_inner_unchecked
  • is_initialized_unchecked
  • is_initialized_into_inner_unchecked
  • was_init_unchecked
  • was_init_into_inner_unchecked
  • is_init_unchecked
  • is_init_into_inner_unchecked
  • assume_initialized_unchecked
  • assume_init_unchecked

بخير من قبلي.

ماذا عن initialized_into_inner ؟ أو initialized_into_inner_unchecked ، إذا كنت تعتقد أن unchecked ضروري حقًا ، على الرغم من أنني أميل إلى الموافقة مع shepmaster على أن unchecked ضروري فقط للتمييز عن بعض المتغيرات الأخرى التي تم التحقق منها الوظيفة ، حيث لا يتم إجراء فحوصات وقت التشغيل.

عند تنفيذ منشئ الاقتراض الذاتي يدويًا انتهى بي الأمر باستخدام ptr::drop_in_place(maybe_uninit.as_mut_ptr()) عدة مرات ، يبدو أن هذا سيعمل بشكل جيد كطريقة متأصلة unsafe fn drop_in_place(&mut self) على MaybeUninit .

توجد سابقة بـ ManuallyDrop::drop .

أود أن أقول إن foo_unchecked يكون منطقيًا فقط عندما يكون هناك foo مقابل ، وإلا فإن الطبيعة المطلقة للوظيفة غير الآمنة تشير إلي أن شيئًا "مختلفًا" يحدث.

لا أعتقد أن عدم وجود إصدار آمن هو سبب وجيه لإزالة علامة التحذير من الإصدار غير الآمن.

قم بإزالة علامة التحذير من الإصدار غير الآمن

كونه القطعي صبي، عندما يجب على unsafe ظيفة يكن لديك _unchecked عالقة على نهاية بعد ذلك؟ ما الفائدة من وجود تحذيرين يقولان نفس الشيء؟

هذا سؤال عادل. :) لكني أعتقد أن الإجابة هي "تقريبًا أبدًا" ، وأنا في الحقيقة آسف لأن لدينا offset وظيفة غير آمنة على المؤشرات التي لا تعبر بأي حال من الأحوال عن أنها غير آمنة. لا يجب أن يكون حرفيا unchecked ، ولكن يجب أن يكون هناك شيء IMO. عندما أكون في كتلة غير آمنة على أي حال وأكتب عن طريق الخطأ .offset بدلاً من .wrapping_offset ، فقد قطعت وعدًا للمجمع الذي لم أنوي القيام به.

كوظيفة غير آمنة على المؤشرات التي لا تعبر بأي حال من الأحوال عن أنها غير آمنة

هذا يلخص دهشتي في هذه المرحلة.

shepmaster لذلك لا تعتقد أنه من الواقعي أن يقوم شخص ما بتحرير الكود داخل كتلة unsafe موجودة (ربما كتلة كبيرة ، ربما داخل unsafe fn يحتوي ضمنيًا على unsafe block) ، ولا تدرك أن المكالمة التي يضيفونها هي unsafe ؟

سيقوم شخص ما بتحرير الكود داخل كتلة unsafe [...] ولن يكون على علم بأن المكالمة التي يضيفها هي unsafe

آسف ، لم أكن أنوي استبعاد هذا الاحتمال ويبدو أنه حقيقي. رأيي هو أن إدراج المؤهلات على اسم الوظيفة للإشارة إلى أن الوظيفة غير آمنة لأن المؤهل unsafe لا يساعدنا على ما يبدو في الإشارة إلى فشل أعمق.

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

ربما كبيرة ، ربما داخل unsafe fn الذي يحتوي ضمنيًا على كتلة unsafe

لقد سئلت لماذا يسمح Rust بظلال المتغيرات لأن التظليل من الواضح أن فكرة سيئة عندما يكون لديك وظيفة متعددة مئات الأسطر. أنا شخصياً أرفض بشدة مثل هذه الحالات لأنني أعتقد أن هذا الرمز مقبول بشكل عام على أنه شكل سيء للبدء به.

الآن، إذا كان يجبر بعض جوانب الصدأ كتل غير آمنة ليكون أكبر مما كانت "الحاجة" إلى أن تكون، وربما هذا يشير أيضا إلى قضية أكثر جوهرية.


جانبا ، أتساءل عما إذا كان بإمكان IDEs + RLS تحديد أي وظيفة تم تمييزها على أنها غير آمنة وتمييزها بشكل خاص. قام المحرر بالفعل بتسليط الضوء على الكلمة الرئيسية unsafe ، على سبيل المثال.

الآن ، إذا كانت بعض جوانب Rust تفرض على الكتل غير الآمنة أن تكون أكبر مما يجب أن تكون عليه ، فربما يشير ذلك أيضًا إلى مشكلة أكثر جوهرية.

حسنًا ، هناك https://github.com/rust-lang/rfcs/pull/2585 ؛)

جانبا ، أتساءل عما إذا كان بإمكان IDEs + RLS تحديد أي وظيفة تم تمييزها على أنها غير آمنة وتمييزها بشكل خاص.

هذا سيكون رائع! لا يقرأ الجميع التعليمات البرمجية في IDEs فقط - على سبيل المثال ، لا تتم المراجعات عادةً في IDEs.

الآن ، إذا كانت بعض جوانب Rust تفرض على الكتل غير الآمنة أن تكون أكبر مما يجب أن تكون عليه ، فربما يشير ذلك أيضًا إلى مشكلة أكثر جوهرية.

أعتقد أن الطرق غير الآمنة في السلاسل هي واحدة من أكبر الأمثلة - تقسيم طريقة في المنتصف إلى رابط let يمكن أن يكون غير مريح إلى حد ما ، ولكن ما لم تفعل ذلك ، فإن السلسلة بأكملها مغطاة.

ليست "قوى" تمامًا ، لكنها بالتأكيد "محفزات".

ل هناك الصدأ لانج / rfcs # 2585 ؛)

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

ومع ذلك ، لا يقرأ الجميع التعليمات البرمجية في IDEs فقط

نعم ، ولهذا السبب أضع ذلك جانبًا. أفترض أنه كان يجب أن أوضح ذلك بشكل أوضح.


أعتقد أن مشكلتي هي أنك تدافع بشكل فعال عن هذا:

unsafe fn unsafe_real_name_of_function() { ... }
          ^~~~~~ for humans
^~~~~~           for the compiler

هذا يسمح لك برؤية كل وظيفة غير آمنة بوضوح عند قراءة الكود. يزعجني التكرار بشدة ويشير إلى أن شيئًا ما دون المستوى الأمثل.

هذا يسمح لك برؤية كل وظيفة غير آمنة بوضوح عند قراءة الكود. يزعجني التكرار بشدة ويشير إلى أن شيئًا ما دون المستوى الأمثل.

أنا أفهم. يمكنك أيضًا رؤية هذا التكرار على أنه تنفيذ لمبدأ العيون الأربع ، حيث يوفر المترجم عينين. ؛)

shepmaster أعتقد أن هذا أصبح بعيدًا قليلاً عن المسار الصحيح ، لكن النقطة الأصلية IMO هي أنه ليس من الواضح ما هي ثوابت هذه الطريقة - أي متى يكون رمز unsafe ليس UB ، مع الاسم الأبسط.

أوافق على أن "عدم التحديد" ليس الخيار الأفضل ، ولكن له سابقة "انتهاك الثوابت بسهولة".

هذا يجعلني أتمنى أن يكون لدينا اصطلاح تسمية على غرار initialized_or_ub .

أعتقد أن هذا قد انحرف قليلاً عن المسار الصحيح

كنت على وشك أن أقول ذلك بنفسي. لقد قلت مقالتي (ويبدو أن لا أحد يتفق معي) ، لذا سأتركها تكذب ؛ لكم جميعا اختيار ما تريد.

كان لدينا اصطلاح تسمية على غرار initialized_or_ub

تقصد مثل maybe_uninit(ialized) ؟ شيء يمكن تطبيقه على نطاق واسع على مجموعة من الأساليب ذات الصلة بطريقة ما؟ 😇

لا ، أعني مثل unwrap_or_else - وضع ما يحدث في "الحالة غير السعيدة" في اسم الطريقة.

eddyb يا هذا ليس سيئًا للغاية ... ربما .initialized_or_unsound ؟

بشكل عام ، تعتبر إضافة معلومات النوع إلى أسماء المعرفات مضادًا للنمط (على سبيل المثال ، foo_i32 ، bar_mutex ، baz_iterator ) لأن هذه الأنواع موجودة.

ومع ذلك ، عندما يتعلق الأمر بالوظائف ، على الرغم من أن unsafe جزء من النوع fn ، إضافة _unchecked ، _unsafe ، _you_better_know_what_you_are_doing يبدو كن شائعًا جدًا.

أتساءل ، لماذا هذا هو الحال؟

أيضًا ، FYI ، هناك مشكلة (https://github.com/rust-analyzer/rust-analyzer/issues/190) في rust-analyzer لفضح ما إذا كانت الوظائف unsafe أم لا. يجب أن يكون المحررون و IDE قادرين على التأكيد على العمليات التي تتطلب unsafe ضمن كتل unsafe ، والتي لا تتضمن فقط استدعاء وظائف unsafe (بصرف النظر عما إذا كانت ملحقة بمعرف مثل ، على سبيل المثال ، _unchecked أو لا) ، ولكن أيضًا إلغاء الإشارة إلى المؤشرات الأولية ، إلخ.

يمكن القول إن rust-analyzer لا يمكنه فعل ذلك بعد (تحرير: intellij-Rust نوع من العلبة: https://github.com/intellij-rust/intellij-rust/issues/3013#issuecomment-440442306) ، ولكن إذا كان الهدف هو توضيح أن استدعاء هذا داخل كتلة unsafe يتطلب unsafe ، تمييز بناء الجملة هو بديل محتمل لإلحاق هذا بأي شيء. أعني ، إذا كنت تريد هذا حقًا الآن ، يمكنك على الأرجح إضافة اسم هذه الوظيفة كـ "كلمة رئيسية" إلى أداة تمييز بناء الجملة في غضون دقيقتين وتسميتها يوميًا.

تضمين التغريدة

بشكل عام ، تعتبر إضافة معلومات النوع إلى أسماء المعرفات مضادًا للنمط (على سبيل المثال ، foo_i32 ، bar_mutex ، baz_iterator ) لأن هذه الأنواع موجودة.

بالتأكيد ، يعتبر الترميز المجري بشكل عام مضادًا للنمط. أنا أوافق. ومع ذلك ، بشكل عام ، لا يتم أخذ السلامة في الاعتبار في هذه المناقشات وأعتقد أنه نظرًا للخطر الذي يمثله UB ، هناك أسباب وجيهة لإجراء استثناء هنا.

ومع ذلك ، عندما يتعلق الأمر بالوظائف ، على الرغم من أن unsafe جزء من النوع fn ، إضافة _unchecked ، _unsafe ، _you_better_know_what_you_are_doing يبدو كن شائعًا جدًا.

أتساءل ، لماذا هذا هو الحال؟

ببساطة: عدم الأمان. عندما يتعلق الأمر بعدم الأمان ، فإن التكرار هو صديقك. هذا ينطبق على كل من التعليمات البرمجية وسلامة الأجهزة الهامة وما شابه.

أيضًا ، لمعلوماتك ، هناك مشكلة ( محلل الصدأ / محلل الصدأ # 190 ) في rust-analyzer لفضح ما إذا كانت الوظائف unsafe أم لا. يجب أن يكون المحررون و IDE قادرين على التأكيد على العمليات التي تتطلب unsafe ضمن كتل unsafe ، والتي لا تتضمن فقط استدعاء وظائف unsafe (بصرف النظر عما إذا كانت ملحقة بمعرف مثل ، على سبيل المثال ، _unchecked أو لا) ، ولكن أيضًا إلغاء الإشارة إلى المؤشرات الأولية ، إلخ.

يمكن القول إن rust-analyzer لا يمكنه القيام بذلك حتى الآن ، ولكن إذا كان القصد هو توضيح أن استدعاء هذا داخل كتلة unsafe يتطلب unsafe ، فإن تمييز بناء الجملة هو بديل محتمل للواحق هذا مع أي شيء.

كل هذا رائع جدا. ومع ذلك ، كما لاحظ RalfJung : "لا يقرأ الجميع التعليمات البرمجية في IDEs فقط - على سبيل المثال ، لا تتم المراجعات عادةً في IDEs." يبدو لي أنه من غير المحتمل أن يقوم GitHub بتضمين محلل الصدأ في واجهة المستخدم الخاصة به لإظهار ما إذا كانت الوظيفة / العملية المسماة غير آمنة أم لا.

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

على سبيل المثال ، إذا كنت تريد استخدام عملية تشفير غير آمنة في Mundane ، فيجب عليك:

  • قم باستيراده من الوحدة النمطية insecure
  • اكتب allow(deprecated) أو تعايش فقط مع تحذيرات المترجم الصادرة كلما استخدمت هذه العملية
  • اكتب رمزًا يشبه let mut hash = InsecureSha1::default(); hash.insecure_write(bytes); ...

كل ذلك موثق هنا بمزيد من التفاصيل. لا أعتقد أننا بحاجة إلى أن نكون عدوانيين في جميع الظروف ، لكنني أعتقد أن الفلسفة الصحيحة هي أنه إذا كانت العملية خطيرة بما يكفي للقلق ، فلا ينبغي أن يكون هناك أي طريقة لمبرمج ليفتقد الجاذبية لما يفعلونه.

اقتراح جاد تماما

نظرًا لأننا قلقون بنسبة 95٪ بشأن إساءة استخدام الأشخاص لهذا النوع ، و 5٪ فقط قلقون بشأن الأسماء الطويلة ، فلنبدأ بإعادة تسمية النوع إلى MaybeUninitialized . الشخصيات السبعة الإضافية تستحق ذلك.

في الغالب اقتراحات جادة

  1. قم بإعادة تسميته إلى MaybeUninitializedOrUndefinedBehavior لتطرقه إلى المنزل للمستخدمين النهائيين.

  2. اختر أن لا يكون لهذا النوع

    MaybeUninitializedOrUndefinedBehavior::into_inner(value)
    

اقتراح سخيف

MaybeUninitializedOrUndefinedBehaviorReadTheDocsAllOfThemYesThisMeansYou

حسنًا ... بصدق ، يبدو أن وجود اسم طويل مثل MaybeUninitializedOrUndefinedBehavior في هذا النوع في غير محله. إنها العملية .into_inner() التي تحتاج إلى الاسم الجيد لأن هذا هو الجزء الذي يحتمل أن يكون إشكاليًا والذي يحتاج إلى مزيد من الاهتمام. عدم وجود طرق يمكن أن يكون فكرة جيدة. MaybeUninit::initialized_or_undefined(foo) واضحًا جدًا.

IMO لا ينبغي لنا أن نبذل قصارى جهدنا لجعل العمليات غير الآمنة غير عملية مثل هذا. نحتاج إلى أسماء مريحة وطرق لكتابة التعليمات البرمجية الصحيحة غير الآمنة. إذا قمنا بتشويشها بمجموعة من الأسماء الطويلة للغاية والأدوات المساعدة والتحويلات غير الواضحة ، فسيؤدي ذلك إلى تثبيط المستخدمين عن كتابة التعليمات البرمجية الصحيحة غير الآمنة وتجعل من الصعب قراءة التعليمات البرمجية غير الآمنة والتحقق من صحتها.

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

حتى يصبح RLS أكثر فاعلية ، هذا ليس هو الحال بالنسبة لي على الأقل.

أعتقد أن معظمنا يوافق على ذلك

  • المزيد من الأسماء الوصفية جيدة

  • الأسماء الأقل راحة سيئة

والسؤال هو حول أي طريقة لحل الأمور عندما يكون هناك توتر.

على الرغم من ذلك ، أعتقد أن into_inner وجه الخصوص هو اسم سيء لهذه الطريقة (لاستخدام مصطلح خيالي ، فهو ليس على حدود Pareto). الاصطلاح العام هو أن لدينا into_inner عندما يحتوي Foo<T> بالضبط على T ، وتريد الحصول عليه. لكن هذا لا ينطبق على MaybeUninit<T> . يحتوي على صفر أو T s.

لذا فإن الخيار الأقل سوءًا ، على الأقل ، هو تسميته unwrap ، أو ربما unwrap_unchecked .

أعتقد أيضًا أن الصوت from_initialized أو from_initialized_unchecked بأس به ، على الرغم من أن كلمة "from" تظهر عادةً في أسماء الأساليب الثابتة.

ربما unwrap_initialized_unchecked سيكون بخير؟

أطلق عليه take_initialized واجعله &mut self بدلاً من self . يوضح الاسم أنه يتوقع تهيئة القيمة الداخلية. في سياق MaybeUninit و unsafe وحقيقة أنه لا يُرجع Option / Result يوضح أيضًا أن هذه العملية لم يتم التحقق منها.

يبدو أن أخذ الأمر يتطلب &mut self يبدو وكأنه مسدس قدم يجعل من السهل أن تفقد مسار ما إذا كنت قد انتقلت بشكل جوهري من MaybeUninit .

الاسم البديل: نظرًا لأنه يتم بالفعل نقل الملكية تمامًا كما تدل الطرق المسماة into ، فربما into_initialized_unchecked ؟

يبدو أن الحصول على & mut بذاتك يبدو وكأنه مسدس قدم يجعل من السهل فقدان المسار فيما إذا كنت قد انتقلت معنوية من ربما Uninit.

إنها طريقة تم طلبها على الرغم

ولا يبدو أنه يستحق الحصول على المتغير المقترض والمستهلك.

أحب take_initialized ، أو المتغير الأكثر وضوحًا take_initialized_unchecked .

لنبدأ بإعادة تسمية النوع إلى ربما غير مهيأ

أي شخص مستعد لإعداد العلاقات العامة؟

لتحضير العلاقات العامة؟

يمكنني استخدام مهاراتي الرائعة sed منذ أن اقترحتها ؛-)

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

تحرير: يبدو take_initialized جيدًا

ماذا عن assume_initialized ؟ هذه:

  • يتصل بنموذج "التزام الإثبات"
  • يرتبط بشكل جوهري بـ `` الافتراضات محفوفة بالمخاطر ''
  • يحتاج فقط كلمتين
  • يصف المعنى الدلالي للعملية
  • يقرأ بشكل طبيعي
  • يشبه إلى حد كبير LLVM assume ، هو UB إذا تم افتراضه بشكل خاطئ

أي شخص مستعد لإعداد العلاقات العامة؟

لا يهم. قرر فريق libs أن هذا لا يستحق كل هذا العناء.

هل هناك سبب يجعل MaybeUninit<T> ليس Copy عندما T: Copy ؟

tommit نظرًا لأن MaybeUninit<T> يعتمد على ManuallyDrop<T> ، يجب علينا نحن المبرمجين ضمان إسقاط القيمة الداخلية عندما تكون هياكلنا خارج النطاق. إذا نفذت Copy ، أعتقد أنه قد يكون من الصعب على رواد Rust الجدد تذكر إسقاط القيمة الداخلية T كل مرة ، سواء للبنية نفسها أو نسخها. وبهذه الطريقة ، قد ينتج عن ذلك المزيد من تسريبات الذاكرة غير الواضحة التي لا نتوقعها.

@ luojia65 لست متأكدًا من تطبيق هذا المنطق عندما يكون T نفسه Copy ، بغض النظر عما يفعله ManuallyDrop و MaybeUninit .

لا أعتقد أن هناك أي سبب. فقط لم يفكر أحد في إضافة #[derive(Copy)] ؛)

ملاحظة ربما جانب دقيق إلى حد ما من هذا:
أعتقد أنه على الرغم من أن MaybeUninit<T> يجب أن يكون Copy عندما T: Copy ، MaybeUninit<T> يجب ألا يكون Clone عندما T: Clone و T ليس Copy .

أوه نعم ، بالتأكيد لا يمكننا فقط الاتصال بـ clone .

ما زلت أنسى أن Copy: Clone ...

لا بأس ، يمكننا تنفيذ Clone for MaybeUninit<T> where T: Copy بناءً على إرجاع *self .

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

وثائق ManuallyDrop::drop تقول

تعمل هذه الوظيفة على تشغيل أداة تدمير القيمة المضمنة ، وبالتالي فإن القيمة المغلفة تمثل الآن بيانات غير مهيأة. الأمر متروك لمستخدم هذه الطريقة للتأكد من عدم استخدام البيانات غير المهيأة بالفعل.

أي اقتراحات حول كيفية تحسين هذه الصياغة بحيث لا يمكن الخلط بينها وبين نوع "عدم التهيئة" الذي يتعامل معه MaybeUninit ؟

في شروطي، أسقط ManuallyDrop<T> لم تعد آمنة T ، وإنما هو ساري المفعول T ... على الأقل بقدر ما يهتمون أمثل التخطيط.

"قديم" / "غير صالح" ، ربما؟ تمت تهيئته .

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

هل يمكننا تحقيق الاستقرار في MaybeUninit قبل إهمال غير مهيأ؟

RalfJung أود أن أقول إن المكان "تم نقله من". FWIW يجب أن نستخدم نفس المصطلحات في std::ptr::read ، لكن ليس واضحًا جدًا هناك أيضًا.

bluss ، لا ينبغي أبدًا
حل "/" مسار الهجرة "للمستخدمين الحاليين.

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

خلاف ذلك ، سنرسل رسالة غريبة حقًا.

cramertj "غير صالح" ليس اختيارًا جيدًا ، لأنه لا يزال (يجب) أن يفي بثبات الصلاحية.

إذا كان لدينا مستند صغير في UCG يُعرِّف "الأمان" ، فربما ينبغي على المرء أن يربطه تشعبيًا. يمكنك إضافة أن T يجب أن يكون تعريفًا "صالحًا" والارتباط التشعبي "صالحًا" ، ولكن نظرًا لعدم وجود هذه التعريفات مكتوبة في أي مكان حتى الآن ...

يجب أن نفعل ذلك بالتأكيد بمجرد حصولنا على شيء: د

RalfJung لا أعتقد أن " ManuallyDrop<T> لم يعد قابلاً للاستخدام كما أ T ). إن القول بأن عليه دعم بعض ثوابت التمثيل التي يستخدمها المترجم للتحسينات لا يجعله أقل صحة من البيانات.

لا أعتقد أن "ثبات الصلاحية" موجود في قاموس معظم مستخدمي الصدأ (تقريبًا؟)

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

shepmaster كتب منذ فترة

أعتقد أن الطريقة التي تنتقل من الغلاف ، وتستبدلها بقيمة غير مهيئة ، ستكون مفيدة:

pub unsafe fn take(&mut self) -> T

أعتقد أن أكثر ما يقلقني هو أنه مع مثل هذه الوظيفة ، من السهل جدًا نسخ البيانات غير القابلة للنسخ بطريق الخطأ. إذا كنت بحاجة إلى ذلك ، فهل من السيئ فعلاً أن تفعل maybe_uninit.as_ptr().read() ؟

أعتقد أنني ربما اقترحت في مكان ما هناك شيء مثل take استبدال شيء مثل into_inner . لا أعتقد أن هذه فكرة جيدة بعد الآن: في معظم الأحيان ، يكون التقييد الإضافي الذي يقضي بأن يستهلك self into_inner self مفيدًا في معظم الأوقات.

RalfJung في النهاية ، جميع طرق MaybeUninit غير آمنة وهي مجرد أغلفة ملائمة حول as_ptr . ومع ذلك ، أتوقع أن تكون take واحدة من أكثر العمليات شيوعًا ، نظرًا لأن MaybeUninit هو فعليًا Option حيث تتم إدارة العلامة خارجيًا. هذا مفيد في كثير من الحالات ، على سبيل المثال مصفوفة لا يتم فيها تهيئة جميع العناصر (مثل جدول التجزئة).

في https://github.com/rust-lang/rust/pull/57045 ، أقترح إضافة عمليتين جديدتين إلى MaybeUninit :

    /// Get a pointer to the first contained values.
    pub fn first_ptr(this: &[MaybeUninit<T>]) -> *const T {
        this as *const [MaybeUninit<T>] as *const T
    }

    /// Get a mutable pointer to the first contained values.
    pub fn first_mut_ptr(this: &mut [MaybeUninit<T>]) -> *mut T {
        this as *mut [MaybeUninit<T>] as *mut T
    }

انظر إلى أن العلاقات العامة للتحفيز والمناقشة.

عند إزالة zeroed يبدو أنه تم استبداله فقط بـ MaybeUninit::zeroed().into_inner() والذي يصبح طريقة مكافئة لكتابة نفس الشيء. لا يوجد تغيير عملي. مع القيم غير المهيأة ، لدينا بدلاً من ذلك التغيير العملي لجميع البيانات غير المهيأة المخزنة في قيم من النوع MaybeUninit أو ما يعادله من الاتحاد.

لهذا السبب ، أفكر في الاحتفاظ std::mem::zeroed كما هو لأنه وظيفة مستخدمة على نطاق واسع في FFI. قد يؤدي الإهمال إلى إصدار تحذيرات صاخبة ، وهو ما يعادل تقريبًا إزالته ، ومزعجًا جدًا على الأقل - يمكن أن يؤدي ذلك أيضًا إلى زيادة عدد #[allow(deprecated)] والتي يمكن أن تخفي مشكلات أخرى أكثر أهمية.

يعد هذا التمرين لتوضيح نموذج Rust وإرشاداته لـ unsafe كود مميز مفيدًا للغاية ، ولكن دعونا نتجنب التغييرات مثل zeroed حيث يتم فقط إعادة صياغة نفس التأثير العملي باستخدام طريقة جديدة لقول ذلك .

bluss ما أفهمه (والذي قد يكون خاطئًا) هو أن std::mem:zeroed بنفس خطورة std::mem::uninitialized ، ومن المحتمل أن ينتج عنه UB. ربما يتم استخدامه لتهيئة مصفوفات البايت ، والتي يمكن تهيئتها بشكل أفضل باستخدام vec![0; N] أو [0; N] ، وفي هذه الحالة ربما يمكن إضافة قاعدة rustfix لأتمتة التغيير؟ خارج تهيئة مصفوفات البايت أو الأعداد الصحيحة ، على الرغم من ذلك ، أفهم أن هناك فرصة جيدة لاستخدام std::mem::zeroed يمكن أن يؤدي إلى UB.

scottjmaddox من السهل جدًا استدعاء UB بـ std::mem:zeroed ، ولكن على عكس std::mem::uninitialized ، هناك بعض الأنواع التي يكون std::mem:zeroed صالحًا لها تمامًا (على سبيل المثال ، الأنواع الأصلية ، العديد من FFI ذات الصلة struct s ، إلخ.). مثل العديد unsafe وظائف، و zeroed() ليس لاستخدامها على محمل الجد، ولكنها ليست ما يقرب من إشكالية كما uninitialized() . سأكون حزينًا على استخدام MaybeUninit::zeroed().into_inner() بدلاً من std::mem:zeroed() نظرًا لعدم وجود أي فرق بين الاثنين من حيث الأمان وإصدار MaybeUninit أكثر صعوبة وأقل وضوحًا (وعندما يتعين علي استخدام رمز غير آمن ، فإنني أقدر بشدة الوضوح).

تضمين التغريدة

على عكس std :: mem :: uninitialized ، هناك بعض الأنواع التي تكون std :: mem: zeroed صالحة تمامًا لها (على سبيل المثال ، الأنواع الأصلية ،

هناك بعض الأنواع التي يكون فيها mem::uninitialized آمنًا تمامًا ( مثل unit ) ، بينما توجد بعض الأنواع "الأصلية" (مثل bool ، &T ، إلخ. .) التي يستدعي فيها mem::zeroed سلوكًا غير محدد.


يبدو أن هناك اعتقادًا خاطئًا هنا بأن MaybeUninit يتعلق بطريقة ما بالذاكرة غير المهيأة (ويمكنني أن أرى السبب: "غير مهيأ" في اسمها).

الخطر الذي نحاول منعه هو الخطر الناجم عن إنشاء قيمة _invalid_ ، سواء كانت القيمة _invalid_ تحتوي على جميع الأصفار ، أو وحدات بت غير مهيأة ، أو أي شيء آخر (على سبيل المثال bool من نمط بت ليس true أو false ) ، لا يهم حقًا - يمكن استخدام mem::zeroed و mem::uninitialized لإنشاء قيمة _invalid_ ، وبالتالي فهي كذلك إلى حد كبير بنفس القدر من الخطورة من وجهة نظري.

OTOH MaybeUninit::zeroed() و MaybeUninit::uninitialized() طريقتان _safe_ لأنهما ترجعان union . MaybeUninit::into_inner هو unsafe ، واستدعائه هو _safe_ فقط إذا تم استيفاء الشرط المسبق بأن البتات الحالية في MaybeUninit<T> تمثل _valid_ بقيمة T . إذا كان نمط البت هو _invalid_ ، يكون السلوك غير معرف. لا يهم حقًا ما إذا كان نمط البت غير صالح لأنه يحتوي على جميع الأصفار أو وحدات بت غير مهيأة أو أي شيء آخر.

RalfJung بدأت أشعر أن الاسم MaybeUninit قد يكون مضللًا بعض الشيء. ربما يجب أن نعيد تسميته إلى MaybeInvalid أو شيء من هذا القبيل لنقل المشكلة التي تحلها والمخاطر التي تتجنبها بشكل أفضل. تحرير: اتباع اقتراحات Centril التي نشرتها حول قضية الدراجات.


تحرير: FWIW ، أعتقد أن وجود طريقة مريحة (على سبيل المثال بدون استخدام MaybeUninit ) لإنشاء ذاكرة صفرية بأمان سيكون مفيدًا ، لكن mem::zeroed ليس كذلك. يمكننا إضافة سمة Zeroed مشابهة لـ Default والتي يتم تنفيذها فقط للأنواع التي يكون نمط بت بها جميع الأصفار صالحًا أو شيء من هذا القبيل ، كطريقة لتحقيق تأثير مماثل لما mem::zeroed يفعل الآن ، لكن بدون عيوبه.

بشكل عام ، أعتقد أنه لا ينبغي لنا إهمال الوظيفة حتى يتم وضع مسار ترحيل للمستخدمين الحاليين إلى حل أفضل. MaybeUninit هو حل أفضل من mem::zeroed في عيني ، على الرغم من أنه قد لا يكون مثاليًا (إنه أكثر أمانًا ، لكنه ليس مريحًا) ، لذلك سأكون بخير مع إهمال mem::zeroed بمجرد وصول MaybeUninit ، حتى لو لم يكن لدينا بديل أكثر راحة في الوقت الذي يحدث فيه ذلك.

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

ركوب الدراجة في https://github.com/rust-lang/rust/pull/56138.

تضمين التغريدة

هناك بعض الأنواع "الأصلية" (مثل bool

طالما أن bool آمن FFI (وهو ما يُعتبر عمومًا ، على الرغم من رفض RFC 954 ثم قبوله رسميًا بعد ذلك) ، يجب أن يكون استخدام mem::zeroed آمنًا.

، &T ، وما إلى ذلك) والتي يستدعي فيها mem::zeroed سلوكًا غير محدد.

نعم ، لكن هذه الأنواع التي تحتوي على UB مقابل mem::zeroed لها أيضًا UB مقابل MaybeUninit::zeroed().into_inner() (كنت حريصًا على تضمين .into_inner() عمدًا في تعليقي الأصلي). MaybeUninit لا يضيف شيئًا إذا اتصل المستخدم فورًا بـ .into_inner() (وهو بالضبط ما كنت سأفعله أنا والعديد من الآخرين إذا تم إهمال mem::zeroed ، لأنني أستخدم mem::zeroed للأنواع الآمنة صفر).

طالما أن Bool آمن FFI (وهو ما يعتبر عمومًا ، على الرغم من رفض RFC 954 ثم قبوله بشكل غير رسمي) ، يجب أن يكون استخدام mem :: zeroed آمنًا له.

لم أكن أرغب في الخوض في تفاصيل هذا ، ولكن bool آمن FFI بمعنى أنه معرّف بأنه مساو لـ C _Bool . ومع ذلك ، فإن قيم true و false لـ C _Bool لم يتم تعريفها في معيار C (على الرغم من أنها قد تكون يومًا ما ، ربما في C20) ، لذا ما إذا كان mem::zeroed ينشئ bool صالحًا أو لا يكون محددًا من الناحية الفنية.

نعم ، ولكن هذه الأنواع التي تحتوي على UB لـ mem :: zeroed لها أيضًا UB لـ MaybeUninit :: zeroed (). into_inner () (كنت حريصًا على تضمين .into_inner () عن قصد في تعليقي الأصلي). ربما لا تضيف ربما Uninit شيئًا إذا اتصل المستخدم فورًا بـ .into_inner () (وهو بالضبط ما سأفعله أنا والعديد من الآخرين إذا تم إهمال mem :: zeroed ، لأنني أستخدم mem :: zeroed فقط لأنواع آمنة صفريًا) .

لا أفهم حقًا النقطة التي تحاول توضيحها هنا. يضيف MaybeUninit خيار الاتصال أو عدم الاتصال بـ into_inner ، والذي لا يتوفر به mem::zeroed ، وتوجد قيمة في ذلك لأن هذه هي العملية التي يمكنها تقديم سلوك غير محدد ( بناء الاتحاد على أنه غير مهيأ أو صفري آمن).

لماذا يقوم أي شخص بترجمة mem::zeroed إلى MayeUninit + into_inner ؟ هذه ليست الطريقة المناسبة "لإصلاح" تحذير الإيقاف mem::zeroed ، وإسكات تحذير الإيقاف له نفس التأثير وتكلفة أقل بكثير.

الطريقة المناسبة للانتقال من mem::zeroed إلى MaybeUninit هي تقييم ما إذا كان من الآمن الاتصال بـ into_inner ، وفي هذه الحالة يمكن للمرء فقط القيام بذلك وكتابة تعليق يشرح سبب ذلك آمن ، أو استمر في العمل مع MaybeUninit باعتباره union حتى يصبح الاتصال بـ into_inner آمنًا (قد يحتاج المرء إلى تغيير الكثير من التعليمات البرمجية حتى يصبح الأمر كذلك ، قم بكسر API التغييرات لإرجاع MaybeUninit بدلاً من T s ، إلخ.).

لم أكن أرغب في الخوض في تفاصيل هذا ، ولكن bool آمن FFI بمعنى أنه معرّف بأنه مساو لـ C _Bool . ومع ذلك ، فإن قيم true و false لـ C's _Bool are not defined in the C standard (although they might be some day, maybe in C20), so whether mem :: صفر creates a valid bool` أو لا يتم تحديدها من الناحية الفنية .

نعتذر عن استمرار الظل ، لكن C11 تتطلب أن تمثل جميع وحدات البت التي تم ضبطها على الصفر القيمة 0 لأنواع الأعداد الصحيحة (انظر القسم 6.2.6.2 "أنواع الأعداد الصحيحة" ، الفقرة 5) (التي تتضمن _Bool ) . بالإضافة إلى ذلك ، تم تحديد قيم true و false بشكل واضح (راجع القسم 7.18 "النوع المنطقي والقيم <stdbool.h> ").

لا أفهم حقًا النقطة التي تحاول توضيحها هنا. يضيف MaybeUninit خيار الاتصال أو عدم الاتصال بـ into_inner ، وهو الخيار الذي لا يتوفر به mem::zeroed ، وهناك قيمة في ذلك لأن هذه هي العملية التي يمكن أن تقدم سلوكًا غير محدد ( بناء الاتحاد على أنه غير مهيأ أو صفري آمن).

هناك قيمة في MaybeUninit و MaybeUninit::zeroed . كلانا يتفق على ذلك. أنا لا أطالب بإزالة MaybeUninit::zeroed . نقطتي هي أن هناك أيضًا قيمة في std::mem::zeroed .

هناك بعض الأنواع التي تعتبر mem :: uninitialized لها آمنة تمامًا (على سبيل المثال ، وحدة) ، بينما هناك بعض الأنواع "الأصلية" (مثل bool ، & T ، إلخ) التي تتطلب mem :: zeroed سلوكًا غير محدد.

هذا هو ذر الرماد في العيون. فقط لأن كلاً من zeroed و uninitialized صالحان لبعض المجموعات الفرعية من الأنواع لا يجعلها قابلة للمقارنة في الاستخدام الفعلي. تحتاج إلى إلقاء نظرة على حجم تلك المجموعات الفرعية. عدد الأنواع التي يكون mem::uninitialized صالحًا لها صغير جدًا (في الواقع ، هل هي فقط الأنواع ذات الحجم الصفري؟) ، ولن يكتب أحد بالفعل رمزًا يفعل ذلك (على سبيل المثال ، بالنسبة إلى ZSTs التي ستستخدمها منشئ النوع). من ناحية أخرى ، هناك العديد من الأنواع التي يكون mem::zeroed صالحًا لها. mem::zeroed صالح للأنواع التالية على الأقل (آمل أن يكون هذا صحيحًا):

  • جميع أنواع الأعداد الصحيحة (بما في ذلك bool كما هو مذكور أعلاه)
  • جميع أنواع المؤشر الخام
  • Option<T> حيث يؤدي T إلى تحسين تخطيط التعداد. T يشمل:

    • NonZeroXXX (جميع أنواع الأعداد الصحيحة)

    • NonNull<U>

    • &U

    • &mut U

    • نقاط fn

    • أي مجموعة من أي نوع في هذه القائمة

    • أي struct حيث يكون أي حقل نوعًا في هذه القائمة.

  • أي مصفوفة ، struct ، أو union تتكون فقط من الأنواع الموجودة في هذه القائمة.

نعم ، يتعامل كلا من uninitialized و zeroed مع قيم يحتمل أن تكون غير صالحة. ومع ذلك ، يستخدم المبرمجون هذه الأوليات بطرق مختلفة جدًا .

النمط الشائع لـ mem::uninitialized هو:

let val = MaybeUninit::uninitialized();
initialize_value(val.as_mut_ptr()); // or val.set
val.into_inner()

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

الاستخدام الأكثر شيوعًا لـ mem::zeroed اليوم هو للأنواع الموصوفة أعلاه ، وهذا صالح تمامًا. أتفق تمامًا مع bluss على أنني لا أرى أي مكسب لمنع استخدام مسدس القدم من خلال استبدال mem::zeroed() كل مكان بـ MaybeUninit::zeroed().into_inner() .

للتلخيص ، الاستخدام الشائع لـ uninitialized للأنواع التي يمكن أن تحتوي على قيم غير صالحة. الاستخدام الشائع لـ zeroed للأنواع الصالحة إذا كانت صفرية.

A Zeroed سمة أو ما شابه (على سبيل المثال Pod ، لكن لاحظ أن T: Zeroed لا يعني T: Pod ) كما تم اقتراحه ، يبدو أنه شيء جيد لإضافته في المستقبل ، لكن دعونا لا نتوقف عن استخدام fn zeroed<T>() -> T حتى نحصل على fn zeroed2<T: Zeroed>() -> T مستقر.

تضمين التغريدة

نعتذر عن استمرار الظل ، لكن C11 تتطلب ذلك

في الواقع! إنه فقط bool لـ C ++ الذي يترك القيم الصالحة غير محددة! شكرًا لتصحيح الأمر ، سأرسل PR إلى UCG مع هذا الضمان.

تضمين التغريدة

تحتاج إلى إلقاء نظرة على حجم تلك المجموعات الفرعية. عدد الأنواع التي يكون mem::uninitialized صالحًا لها صغير جدًا (في الواقع ، هل هي فقط أنواع ذات حجم صفري فقط؟) ، ولن يكتب أحد بالفعل رمزًا يفعل ذلك (على سبيل المثال بالنسبة لـ ZSTs التي ستستخدمها فقط منشئ النوع).

إنه ليس صحيحًا حتى بالنسبة لجميع ZSTs إذا كنت تأخذ في الاعتبار الخصوصية التي يمكن من خلالها الحصول على ZST كنوع من "إثبات العمل" أو "الرمز المميز للمورد" أو مجرد "شاهد الإثبات" بشكل عام. مثال تافه :

mod refl {
    use core::marker::PhantomData;
    use core::mem;

    /// Having an object of type `Id<A, B>` is a proof witness that `A` and `B`
    /// are nominally equal type according to Rust's type system.
    pub struct Id<A, B> {
        witness: PhantomData<(
            // Make sure `A` is Id is invariant wrt. `A`.
            fn(A) -> A,
            // Make sure `B` is Id is invariant wrt. `B`.
            fn(B) -> B,
        )>
    }

    impl<A> Id<A, A> {
        /// The type `A` is always equal to itself.
        /// `REFL` provides a proof of this trivial fact.
        pub const REFL: Self = Id { witness: PhantomData };
    }

    impl<A, B> Id<A, B> {
        /// Casts a value of type `A` to `B`.
        ///
        /// This is safe because the `Id` type is always guaranteed to
        /// only be inhabited by `Id<A, B>` types by construction.
        pub fn cast(self, value: A) -> B {
            unsafe {
                // Transmute the value;
                // This is safe since we know by construction that
                // A == B (including lifetime invariance) always holds.
                let cast_value = mem::transmute_copy(&value);

                // Forget the value;
                // otherwise the destructor of A would be run.
                mem::forget(value);

                cast_value
            }
        }
    }
}

fn main() {
    use core::mem::uninitialized;

    // `Id<?A, ?B>` is a ZST; let's make one out of thin air:
    let prf: refl::Id<u8, String> = unsafe { uninitialized() };

    // Segfault:
    let _ = prf.cast(42u8);
}

Centril هذا نوع من الظل ، لكنني لست متأكدًا مما إذا كان الرمز الخاص بك هو في الواقع مثال على نوع يؤدي استدعاء uninitialized إنشاء قيمة غير صالحة. أنت تستخدم رمزًا غير آمن لخرق الثوابت الداخلية التي من المفترض أن يدعمها Id . هناك العديد من الطرق للقيام بذلك ، على سبيل المثال transmute(()) ، أو كتابة المؤشرات الأولية.

jethrogb نقاطي الوحيدة هي أ) من فضلك كن أكثر حرصًا في الصياغة ، ب) لا يبدو أن الخصوصية منطقية بشكل كافٍ في المناقشات حول القيم الصالحة. يبدو لي أن "انتهاك الثوابت الداخلية" و "القيمة غير الصالحة" هما نفس الشيء ؛ هناك حالة جانبية هنا "إذا كان A != B فإن Id<A, B> غير مأهول.".

يبدو لي أن "انتهاك الثوابت الداخلية" و "القيمة غير الصالحة" هما نفس الشيء ؛ هناك حالة جانبية هنا "إذا كان A != B فإن Id<A, B> غير مأهول.".

الثوابت "التي تفرضها كود مكتبة" تختلف عن الثوابت التي تفرضها "مترجم" في عدة طرق، ترى RalfJung الصورة بلوق وظيفة حول هذا الموضوع . في هذا المصطلح ، يحتوي المثال Id الخاص بك على ثابت أمان و mem::zeroed أو طرق أخرى لتجميع Id<A, B> لا يمكن أن تكون آمنة ، لكنها ليست فورية UB لمجرد إنشاء قيمة خاطئة Id مع mem::zeroed أو mem::uninitialized لأن Id لا يحتوي على ثابت صلاحية بينما يحتاج مؤلفو الأكواد غير الآمنة بالتأكيد إلى وضع كلا النوعين من الثوابت في الاعتبار ، إلا أن هناك بعض الأسباب التي تجعل هذه المناقشات تركز في الغالب على الصلاحية:

  • إن ثوابت الأمان محددة من قبل المستخدم ، ونادرًا ما تكون رسمية ، ويمكن أن تكون معقدة بشكل تعسفي ، لذلك هناك أمل ضئيل في التفكير بشكل عام حولها أو مساعدة المترجم / اللغة في دعم أي ثابت معين للسلامة.
  • كسر ثابتة السلامة يمكن في بعض الأحيان تكون هناك حاجة (داخليا ضمن مكتبة الصوت)، وذلك حتى لو تمكنا من حكم ميكانيكيا من mem::zeroed::<T>() على أساس T الصورة ثابتة السلامة، ونحن قد لا ترغب في ذلك.
  • وبالمثل ، فإن عواقب كسر ثوابت الصلاحية هي في بعض النواحي أسوأ من ثوابت الأمان المعطلة (فرصة أقل لتصحيحها لأن كل الجحيم ينهار على الفور ، وغالبًا ما يكون السلوك الفعلي الناتج عن UB أقل قابلية للفهم لأن كل من المترجم والمحسن العوامل في ذلك ، في حين يتم استغلال ثوابت السلامة بشكل مباشر فقط عن طريق الكود في نفس الوحدة / الصندوق).

بعد قراءة تعليق jethrogb ، أوافق على عدم إهمال mem::zeroed مع إدخال MaybeUninit .

jethrogb الصئبان الصغيرة:

أي مجموعة من أي نوع في هذه القائمة
أي هيكل حيث يكون أي حقل نوعًا في هذه القائمة.

لست متأكدًا مما إذا كان هذا خطأ إملائيًا بسيطًا أو اختلافًا دلاليًا ، لكنني أعتقد أنك بحاجة إلى التخلص من هاتين النقطتين - لا أعتقد أن هذا هو الحال بالضرورة أن None على سبيل المثال Option<[&u8; 2]> لديه bitwise-zeros كتمثيل صالح (يمكن على سبيل المثال استخدام [0, 24601] None كتمثيل للحالة @ eddyb للتحقق من هذا). أشك في أننا نفعل هذا اليوم ، لكن لا يبدو أنه من المستحيل تمامًا ظهور شيء كهذا في المستقبل.

تضمين التغريدة

الاستخدام الأكثر شيوعًا لـ mem :: zeroed اليوم هو للأنواع الموضحة أعلاه ، وهذا صحيح تمامًا.

هل هناك مصدر لهذا؟

من ناحية أخرى ، هناك العديد من الأنواع التي تكون mem :: zeroed صالحة لها.

هناك أيضًا عدد لا نهائي من الحالات التي يمكن استخدامها بشكل غير صحيح.

أفهم أنه بالنسبة لأولئك الذين يستخدمون mem::zeroed بكثافة وبشكل صحيح ، فإن تأخير الإيقاف حتى يتوفر حل أكثر راحة هو بديل جذاب للغاية.

أفضل المفاضلة بين تقليل أو إزالة عدد الاستخدامات غير الصحيحة البالغة mem::zeroed حتى لو كان ذلك ينطوي على تكلفة مريحة مؤقتة. يحذر الإيقاف المستخدمين من أن ما يفعلونه يحتمل أن يستدعي سلوكًا غير محدد (خاصة المستخدمين الجدد الذين يستخدمونه لأول مرة) ، ولدينا حل سليم لما يجب فعله بدلاً من ذلك ، مما يجعل التحذير قابلاً للتنفيذ.

أستخدم MaybeUninit غالبًا وهو استخدام أقل راحة من mem::zeroed و mem::uninitialized ، لكنه لم يكن غير مريح بالنسبة لي بشكل مؤلم. إذا كان MaybeUninit مؤلمًا مثل بعض التعليقات في مطالبة المناقشة هذه ، فستظهر مكتبة و / أو RFC لبديل آمن mem::zeroed في أي وقت من الأوقات (لا شيء يمنع أي شخص هنا AFAICT).

بدلاً من ذلك ، يمكن للمستخدمين تجاهل التحذير والاستمرار في استخدام mem::zeroed ، الأمر متروك لهم ، لا يمكننا أبدًا إزالة mem::zeroed من libcore على أي حال.

لكن الأشخاص الذين يستخدمون mem::zeroed بكثافة يجب أن يفحصوا بنشاط ما إذا كانت جميع استخداماتهم صحيحة على أي حال. لا سيما أولئك الذين يستخدمون mem::zeroed بكثافة ، وأولئك الذين يستخدمونه في رمز عام ، وأولئك الذين يستخدمونه كبديل "أقل رعبًا" لـ mem::uninitialized ، وما إلى ذلك. يؤدي تأخير الإيقاف إلى تأخير تحذير المستخدمين من أن ما يفعلونه قد يكون سلوكًا غير محدد.

تضمين التغريدة

عند إزالة الصفر ، يبدو أنه يتم استبداله فقط بـ MaybeUninit :: zeroed (). into_inner () والتي تصبح طريقة مكافئة لكتابة الشيء نفسه. لا يوجد تغيير عملي. مع القيم غير المحددة ، لدينا بدلاً من ذلك التغيير العملي لجميع البيانات غير المهيأة المخزنة في قيم من النوع ربما Uninit أو اتحاد مكافئ.

هذا صحيح عندما نتحدث عن الأعداد الصحيحة ، ولكن بمجرد النظر إلى أنواع المراجع ، على سبيل المثال ، يصبح mem::zeroed() مشكلة أيضًا.

ومع ذلك ، أوافق على أنه من المرجح أن يدرك الناس بالفعل أن mem::zeroed::<&T>() يمثل مشكلة ، أكثر من إدراك الناس أن mem::uninitialized::<bool>() يمثل مشكلة. لذلك ربما يكون من المنطقي الاحتفاظ بـ mem::zeroed() .

لاحظ ، مع ذلك ، أننا قد نقرر أن mem::uninitialized::<u32>() ما يرام - إذا سمحنا بالبتات غير المهيأة في أنواع الأعداد الصحيحة ، فإن mem::uninitialized() يصبح صالحًا لجميع "أنواع POD" تقريبًا. لا أعتقد أننا يجب أن نسمح بذلك ، لكن لا يزال يتعين علينا إجراء هذه المناقشة.

عدد الأنواع التي تكون mem :: غير مهيأة صالحة لها صغير جدًا (في الواقع ، هل هي فقط أنواع ذات حجم صفري؟) ، ولن يكتب أحد بالفعل رمزًا يفعل ذلك (على سبيل المثال ، بالنسبة لـ ZST ، ستستخدم فقط النوع البناء).

FWIW ، يجب على بعض كود مكرر الشرائح في الواقع إنشاء ZST في كود عام دون التمكن من كتابة مُنشئ نوع. يستخدم mem::zeroed() / MaybeUninit::zeroed().into_inner() لذلك.

mem::zeroed() مفيدًا لبعض حالات FFI حيث يُتوقع منك عدم وجود قيمة بقيمة memset(&x, 0, sizeof(x)) قبل استدعاء دالة C. أعتقد أن هذا سبب كاف لإبقائه غير مهين.

Amanieu يبدو هذا غير ضروري. بناء Rust المطابق memset هو write_bytes .

mem :: zeroed () مفيد في بعض حالات FFI

أيضًا ، في المرة الأخيرة التي تحققت فيها ، كانت mem::zeroed هي الطريقة الاصطلاحية لتهيئة هياكل libc بحقول خاصة أو تعتمد على النظام الأساسي.

RalfJung عادةً ما يكون الرمز الكامل المعني هو Type x; memset(&x, 0, sizeof(x)); والجزء الأول لا يحتوي على مكافئ Rust رائع. استخدام MaybeUninit لهذا النمط هو الكثير من ضجيج الخطوط (وأسوأ بكثير من كودجين بدون تحسينات) عندما تكون الذاكرة غير صالحة أبدًا بعد memset .

لدي سؤال حول تصميم MaybeUninit : هل هناك طريقة للكتابة إلى حقل واحد من T الموجود داخل MaybeUninit<T> بحيث يمكنك الكتابة إليه بمرور الوقت كل الحقول وينتهي بها الأمر بنوع صالح / مهيأ؟

افترض أن لدينا هيكلًا كالتالي:

// Let us suppose that Foo can in principle be any struct containing arbitrary types
struct Foo {bar: bool, baz: String}

هل يؤدي إنشاء مرجع & mut Foo ثم الكتابة إليه إلى تشغيل UB؟

main () {
    let uninit_foo = MaybeUninitilized::<Foo>::uninitialized();
    unsafe { *uninit_foo.get_mut().bar = true; }
    unsafe { *uninit_foo.get_mut().baz = "hello world".to_owned(); }
}

هل استخدام مؤشر خام بدلاً من مرجع يجنب هذه المشكلة؟

main () {
    let uninit_foo = MaybeUninitilized::<Foo>::uninitialized();
    unsafe { *uninit_foo.as_mut_pointer().bar = true; }
    unsafe { *uninit_foo.as_mut_pointer().baz = "hello world".to_owned(); }
}

أم أن هناك طريقة أخرى يمكن من خلالها تنفيذ هذا النمط دون تشغيل UB؟ حدسيًا ، يبدو لي أنه طالما أنني لا أقرأ ذاكرة غير مهيأة / غير صالحة ، فيجب أن يكون كل شيء على ما يرام ، ولكن العديد من التعليقات في هذا الموضوع تقودني إلى الشك في ذلك

ستكون حالة الاستخدام الخاصة بي لهذه الوظيفة هي نمط منشئ موضعي للأنواع التي يُطلب فيها تحديد بعض الحقول بواسطة المستخدم (وليس لديها قيمة افتراضية معقولة) ، ولكن بعض الحقول بها افتراضي القيمة.

هل توجد أي طريقة للكتابة في حقل واحد من الحرف T الموجود داخل ربما Uninitبحيث يمكنك بمرور الوقت الكتابة إلى جميع الحقول وينتهي بك الأمر بنوع صالح / مُهيأ؟

نعم. استعمال

ptr::write(&mut *(uninit.as_mut_ptr()).bar, val1);
ptr::write(&mut *(uninit.as_mut_ptr()).baz, val2);
...

يجب ألا تستخدم get_mut() لهذا الغرض ، ولهذا السبب تقول مستندات get_mut أنه يجب تهيئة القيمة قبل استدعاء هذه الطريقة. قد نخفف هذه القاعدة في المستقبل ، والتي تتم مناقشتها على https://github.com/rust-rfcs/unsafe-code-guidelines/.

RalfJung ألن *(uninit.as_mut_ptr()).bar = val1; بإسقاط القيمة سابقًا في bar ، والتي قد تكون غير مهيأة؟ أعتقد أنه من الضروري القيام بذلك

ptr::write(&mut (*uninit.as_mut_ptr()).bar, val1);

@ scottjmaddox آه ، صحيح. لقد نسيت حوالي Drop . سوف أقوم بتحديث المنشور.

ما هي الطريقة التي يُظهر بها هذا النوع من الكتابة إلى الحقول غير المهيأة سلوكًا غير معرف أقل من get_mut() ؟ عند نقطة الكود حيث يتم تقييم الوسيطة الأولى لـ ptr::write ، قام الكود بإنشاء &mut _ للحقل الداخلي والذي يجب أن يكون غير معرف تمامًا مثل المرجع إلى الهيكل بأكمله الذي قد يكون خلقت. هل يجب ألا يُسمح للمترجم بافتراض أن هذا كان في حالة تهيئة بالفعل؟

ألن يستلزم ذلك طريقة جديدة لإسقاط المؤشر لا تتطلب عرض وسيطة &mut _ ؟


مثال مثير للاهتمام إلى حد ما:

pub struct A { inner: bool }

pub fn init(mut uninit: MaybeUninit<A>) -> A {
    unsafe {
        let mut previous: [u8; std::mem::size_of::<bool>()] = [0];

        {
            // Doesn't the temorary reference assert inner was in valid state before?
            let inner_ptr: *mut _ = &mut (*uninit.as_mut_ptr()).inner;
            ptr::copy(inner_ptr as *const [u8; 1], (&mut previous) as *mut _, 1);

            // With the assert below, couldn't the compiler drop this?
            std::ptr::write(inner_ptr, true);
        }

        // Assert Inner wasn't false before, so it must have been true already!
        assert!(previous[0] != 0);

        // initialized all fields, good to proceed.
        uninit.into_inner()
    }
}

ولكن إذا كان المترجم يفترض أن &mut _ يمثل تمثيلاً صالحًا ، فقد يتخلص تمامًا من ptr::write ؟ إذا تجاوزنا التأكيد ، فإن المحتوى لم يكن 0 ولكن المنطق الصحيح الآخر هو true/1 . لذلك يمكن أن نفترض أن هذا أمر محظور إذا تجاوزنا التأكيد. نظرًا لأنه لم يتم الوصول إلى القيمة من قبل ، فبعد إعادة الترتيب يمكن أن ينتهي بنا الأمر بهذا؟ لا يبدو أن llvm يستغل هذا في الوقت الحالي ، لكنني غير متأكد تمامًا مما إذا كان هذا سيكون مضمونًا.


إذا قمنا بدلاً من ذلك بإنشاء MaybeUninit خاصتنا داخل الوظيفة ، فسنحصل على حقيقة مختلفة قليلاً. في الملعب ، اكتشفنا بدلاً من ذلك أنه يفترض أن التأكيد لا يمكن إطلاقه أبدًا ، ويفترض أنه يفترض أن str::ptr::write هو الكتابة الوحيدة إلى inner لذلك يجب أن يكون قد حدث بالفعل قبل أن نقرأ من previous ؟ هذا يبدو مريب بعض الشيء على أي حال. لدعم هذه النظرية ، شاهد ما يحدث عند تغيير المؤشر إلى false بدلاً من ذلك.


أدرك أن مشكلة التتبع هذه قد لا تكون أفضل مكان لهذا السؤال.

RalfJungscottjmaddox شكرا لإجاباتك. هذه الفروق الدقيقة هي بالضبط سبب سؤالي.
HeroicKatora نعم ، كنت أتساءل عن ذلك.

ربما هذا هو التعويذة الصحيحة؟

struct Foo {bar: bool, baz: String}

fn main () {
    let mut uninit_foo = MaybeUninit::<Foo>::uninitialized();
    unsafe { ptr::write_unaligned(&mut ((*uninit_foo.as_mut_ptr()).bar) as *mut bool, true); }
    unsafe { ptr::write_unaligned(&mut ((*uninit_foo.as_mut_ptr()).baz) as *mut String, "".to_string()); }
}

( ملعب )

لقد قرأت تعليقًا على Reddit (والذي لم يعد بإمكاني العثور عليه للأسف) والذي اقترح أن إرسال مرجع على الفور إلى مؤشر ( &mut foo as *mut T ) يتم تجميعه في الواقع لإنشاء مؤشر فقط. ومع ذلك ، فإن *uninit_foo.as_mut_ptr() بت يقلقني. هل من المقبول عدم إحالة المؤشر إلى الذاكرة الموحدة مثل هذا؟ نحن لا نقرأ أي شيء في الواقع ، لكن ليس من الواضح بالنسبة لي ما إذا كان المترجم يعرف ذلك.

لقد اكتشفت أن المتغير unaligned لـ ptr::write قد يكون مطلوبًا للرمز العام الذي يزيد عن MaybeUninit<T> حيث لن تحتوي جميع الأنواع على حقول متوافقة؟

لا حاجة ل write_unaligned . المترجم يعالج محاذاة المجال نيابة عنك. ولا يجب أن يكون as *mut bool ضروريًا أيضًا ، حيث يمكن للمجمع أن يستنتج أنه يحتاج إلى إجبار &mut على *mut . أعتقد أن هذا الإكراه المستنتج هو سبب أمانه / صلاحيته. إذا كنت تريد أن تكون صريحًا وتقوم بعمل as *mut _ ، فلا بأس بذلك أيضًا. إذا كنت تريد حفظ المؤشر في متغير ، فمن الضروري أن تقوم بإكراهه في مؤشر.

scottjmaddox هل لا ptr::write آمنًا حتى لو كان الهيكل #[repr(packed)] ؟ ptr::write أنه يجب محاذاة المؤشر بشكل صحيح ، لذلك أفترض أن ptr::write_unaligned مطلوب في الحالات التي تكتب فيها بعض التعليمات البرمجية العامة التي تحتاج إلى التعامل مع التمثيلات المحزومة (على الرغم من أنني لأكون صادقًا ، لست متأكدًا يمكنني التفكير في مثال على "رمز عام يزيد عن MaybeUninit<T> " لا يعرف ما إذا كان الحقل محاذيًا بشكل صحيح أم لا).

تضمين التغريدة

التي تشير إلى أن إرسال مرجع على الفور إلى مؤشر (& mut foo كـ * mut T) يؤدي في الواقع إلى تجميع المؤشر فقط.

يختلف ما يتم تجميعه عن الدلالات التي يُسمح للمترجم باستخدامها لإجراء هذا التجميع. حتى لو كانت no-op في IR ، فلا يزال من الممكن أن يكون لها تأثير دلالي مثل تأكيد افتراضات إضافية للمترجم. scottjmaddox صحيح في أي العمليات تلعب هنا ولكن الجزء الحاسم من السؤال هو إنشاء المرجع القابل للتغيير الذي يحدث قبل وبشكل مستقل عن إكراه ref-to-ptr. إذن ، mjbshaw صحيح تقنيًا بشأن الأمان العام الذي يتطلب ptr::write_unaligned عندما تكون الوسيطة عبارة عن وسيطة عامة غير معروفة.

لا أتذكر أين قرأت هذا (nomicon؟ إحدى منشورات مدونة

ما الطريقة التي يُظهر بها هذا النوع من الكتابة إلى الحقول غير المهيأة سلوكًا غير محدد أقل من get_mut ()؟ عند نقطة الكود حيث يتم تقييم الوسيطة الأولى لـ ptr :: write ، قام الكود بإنشاء & mut _ للحقل الداخلي الذي يجب أن يكون غير معرف تمامًا مثل المرجع إلى البنية الكاملة التي سيتم إنشاؤها بخلاف ذلك. هل يجب ألا يُسمح للمترجم بافتراض أن هذا كان في حالة تهيئة بالفعل؟

سؤال جيد جدا! هذه المخاوف هي أحد أسباب فتح https://github.com/rust-lang/rfcs/pull/2582. مع قبول RFC ، لا يُنشئ الرمز الذي عرضته &mut ، بل يُنشئ *mut .

mjbshaw Touché . نعم ، أفترض أنك محق بشأن إمكانية تعبئة الهيكل ، وبالتالي تحتاج إلى ptr::write_unaligned . لم أفكر في ذلك من قبل ، في المقام الأول لأنني لم أستخدم الهياكل المعبأة في الصدأ. من المحتمل أن يكون هذا الوبر كليبي ، إذا لم يكن كذلك بالفعل.

تحرير: لم أشاهد نصًا كليبيًا ذي صلة ، لذلك أرسلت مشكلة: https://github.com/rust-lang/rust-clippy/issues/3659

فتحت PR لإلغاء الإهمال mem::zeroed : https://github.com/rust-lang/rust/pull/57825

لقد فتحت مشكلة في RFC repo لتقسيم المناقشة حول التصفير الآمن للذاكرة ، حتى نتمكن من إهمال mem::zeroed في مرحلة ما بمجرد أن يكون لدينا حل أفضل لهذه المشكلة: https://github.com / rust-lang / rfcs / قضايا / 2626

هل من الممكن تحقيق الاستقرار في const uninitialized و as_ptr و
as_mut_ptr قبل باقي واجهة برمجة التطبيقات؟ يبدو لي أن هؤلاء على الأرجح
ستستقر كما هي الآن. بالإضافة إلى ذلك ، يمكن البناء على باقي واجهة برمجة التطبيقات
أعلى من as_ptr و as_mut_ptr ، لذلك بمجرد الاستقرار ، سيكون من الممكن
لديك سمة MaybeUninitExt على crates.io التي توفر واجهة برمجة التطبيقات (API) المستقرة
تتم مناقشتها حاليًا للسماح لمزيد من الأشخاص (على سبيل المثال ، المستخدمون المستقرون فقط)
إبداء الرأي بشأنها.

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

لإعطائك فكرة عن مدى أهمية هذا بالنسبة للمجتمع المضمن ، قمنا بذلك
[مسح] يسأل المجتمع عن نقاط الألم واحتياجاتهم. استقرار
MaybeUninit كثاني أكثر شيء مطلوب لتحقيق الاستقرار (خلف
const fn بحدود السمات) ، وبشكل عام ، احتل المركز السابع من بين عشرات
الصدأ لانج / * الطلبات ذات الصلة. بعد مزيد من المداولات داخل WG صدمنا
أولويتها ، بشكل عام ، المركز الثالث بسبب تأثيرها المتوقع على النظام البيئي.

(في ملاحظة شخصية أكثر ، أنا مؤلف إطار عمل التزامن مضمن
التي قد تستفيد داخليًا من استخدام MaybeUninit (استخدام الذاكرة بتنسيق
يمكن تقليل التطبيقات بنسبة 10-50٪ بدون أي تغييرات في رمز المستخدم). أنا
يمكن أن توفر ميزة شحن ليلية لهذا الغرض ، ولكن بعد سنوات من ذلك
ليلا فقط جزءا لا يتجزأ من الاستقرار ومؤخرا فقط أشعر بذلك
قد يكون تقديم ميزة ليلية فقط هو الرسالة الخاطئة التي يجب إرسالها إلى المستخدمين
لذلك أنتظر بفارغ الصبر استقرار واجهة برمجة التطبيقات هذه.)

japaric سيؤدي ذلك بالتأكيد إلى تجنب مناقشات التسمية حول into_inner والأصدقاء. ومع ذلك ، ما زلت قلقًا بشأن المناقشة الدلالية ، على سبيل المثال حول الأشخاص الذين يقومون بعمل let r = &mut *foo.as_mut_ptr(); ومن ثم التأكيد على أن لديهم مرجعًا صالحًا ، في حين أننا لسنا متأكدين حتى الآن ما هي متطلبات الصلاحية للمراجع - أي نحن لست متأكدًا حتى الآن مما إذا كان وجود مرجع للبيانات غير الصالحة هو insta-UB. للحصول على مثال ملموس:

let x: MaybeUninit<!> = MaybeUninit::uninitialized();
let r: &! = &*x.as_ptr() // is this UB?

بدأت هذه المناقشة مؤخرًا في مجموعة عمل UCG.

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

لكن في كلتا الحالتين أعتقد أنه لا ينبغي لنا تثبيت أي شيء قبل أن نقبل https://github.com/rust-lang/rfcs/pull/2582 ، حتى نتمكن على الأقل من إخبار الناس على وجه اليقين بأن ما يلي ليس UB:

let x: MaybeUninit<(!, u32)> = MaybeUninit::uninitialized();
let r1: *const ! = &(*x.as_ptr()).1; // immediately coerced to raw ptr, no UB
let r2 = &(*x.as_ptr()).1 as *const !; // immediately cast to raw ptr, no UB

(لاحظ أنه كالمعتاد ! عبارة عن رنجة حمراء هنا ، وجميع الأمثلة في هذا المنشور هي نفسها ، UB ، إذا استخدمنا bool بدلاً من ذلك.)

كان أملي أن يتمكن من تثبيت ربما Uninit في "حزمة" واحدة متماسكة مع قصة مناسبة للبيانات غير المهيأة ، بحيث يتعين على الأشخاص إعادة تعلم هذه الأشياء مرة واحدة فقط ، بدلاً من إصدارها قطعة قطعة وربما يضطرون إلى تغيير بعض القواعد على طول الطريق.

أجد هذه الحجة مقنعة للغاية.

أعتقد أن الحاجة الأكثر إلحاحًا هي الحصول على بعض الرسائل الواضحة حول كيفية التعامل مع الذاكرة غير المهيأة بدون UB. إذا كان هذا في الوقت الحالي ببساطة "استخدم مؤشرات أولية و ptr::read_unaligned و ptr::write_unaligned " ، فلا بأس بذلك ، لكننا بالتأكيد بحاجة إلى طريقة محددة جيدًا للحصول على مؤشرات أولية لقيم مكدس غير مهيأة ولإنشاء / ترتيب الحقول . يبدو أن rust-lang / rfcs # 2582 (بالإضافة إلى بعض الوثائق) يلبي الاحتياجات الفورية ، في حين أن MaybeUninit لا يفعل ذلك.

@ scottjmaddox كيف يكون ذلك RFC ولكن بدون MaybeUninit أي جيد للذاكرة (المكدسة) غير المهيأة؟

RalfJung أفترض أن ذلك يعتمد على ما إذا كان ما يلي هو UB أم لا:

let x: bool = mem::uninitialized();
ptr::write(&x as *mut bool, false);
assert_eq!(x, false);

كان افتراضي الضمني أن

تضمين التغريدة

let x: bool = mem::uninitialized();

هذا هو UB. لا علاقة له بالمراجع.

كان افتراضي الضمني أن rust-lang / rfcs # 2582 سيجعل المثال أعلاه صالحًا ومحدّدًا جيدًا.

أنا مندهش تمامًا من هذا. أن RFC هو حول المراجع فقط. لماذا تفترض أنه يغير شيئًا عن القيم المنطقية؟

تضمين التغريدة

هذا هو UB. لا علاقة له بالمراجع.

يقول التوثيق إلى mem :: uninitialized () :

يتجاوز اختبارات تهيئة الذاكرة العادية لـ Rust بالتظاهر بإنتاج قيمة من النوع T ، مع عدم القيام بأي شيء على الإطلاق.

لا تذكر الوثائق شيئًا عن T* .

kpp ماذا تحاول أن تقول؟ لا يوجد * ولا يوجد & في هذا السطر من التعليمات البرمجية:

let x: bool = mem::uninitialized();

لماذا تدعي أن هذا الخط هو UB؟

لأن bool يجب أن يكون دائمًا true أو false ، وهذا ليس كذلك. راجع أيضًا https://github.com/rust-rfcs/unsafe-code-guidelines/blob/master/reference/src/glossary.md#validity -and-safety-invariant.

kpp لهذه العبارة لتحديد السلوك mem::uninitialized يجب أن تتحقق _valid_ bool .

على جميع الأنظمة الأساسية المدعومة حاليًا ، يحتوي bool على قيمتين _valid_ فقط ، true (نمط البت: 0x1 ) و false (نمط البت: 0x0 ).

ومع ذلك ، ينتج عن mem::uninitialized نمط بت حيث تكون قيمة كل وحدات البت هي uninitialized . نمط البت هذا ليس 0x0 ولا 0x1 ، لذلك ، الناتج bool هو _invalid_ ، والسلوك غير محدد.

لتحديد السلوك ، سنحتاج إلى تغيير تعريف bool لدعم ثلاث قيم صالحة: true ، false ، أو uninitialized . ومع ذلك ، لا يمكننا القيام بذلك ، لأن T-lang و T-compiler قد صرّحا بالفعل RFC أن bool مطابق لـ C _Bool ولا يمكننا كسر هذا الضمان (هذا يسمح bool لاستخدامه بشكل قابل للنقل في C FFI).

يمكن القول أن C ليس لها نفس تعريف الصلاحية تمامًا مثل Rust ، لكن C "تمثيلات المصيدة" تقترب جدًا. باختصار ، ليس هناك الكثير الذي يمكن للمرء فعله في C بـ _Bool الذي لا تمثل true أو false بدون استدعاء سلوك غير محدد.

إذا كنت على صواب ، فيجب أن يكون الرمز الآمن التالي هو UB أيضًا:

let x: bool;
x = true;

الذي من الواضح أنه ليس كذلك.

إذا كنت صحيحًا ، فيجب أن يكون الرمز الآمن التالي هو UB أيضًا:

لم يقم let x: bool; بتهيئة x إلى نمط uninitialized بت ، ولا يقوم بتهيئة x على الإطلاق. يقوم x = true; بتهيئة x (ملاحظة: إذا لم تقم بتهيئة x قبل استخدامه فستتلقى خطأ تجميع).

هذا يختلف عن سلوك C ، حيث ، بناءً على السياق ، يقوم _Bool x; بتهيئة x إلى قيمة _indeterminate_.

كلا ، هناك يعلم المترجم أن x لم تتم تهيئته.

المشكلة مع mem::uninitialized هي أنه يقوم بتهيئة متغير ، بقدر ما يتعلق الأمر بتتبع تهيئة المترجم.

لا يحتفظ let x: bool; بمفرده حتى بأي مساحة لتخزين x في ، بل يحتفظ فقط باسم. يحتفظ let x = foo; ببعض المساحة ويقوم بتهيئتها باستخدام foo . let x: bool = mem::uninitialized(); احتياطيات 1 بايت من مساحة ل x بل يتركها غير مهيأة، وهذه مشكلة.

هذه طريقة سهلة لتصوير واجهة برمجة تطبيقات مصممة لساقك بحيث يجب توثيقها في كل من mem :: uninitialized و intrinsics :: uninit مع تخصص لـ mem :: uninitializedللذعر أثناء التجميع.

هل هذا يعني أيضًا أن تهيئة أي بنية مع منطقي بداخلها mem :: uninitialized هي UB أيضًا؟

kpp

هل هذا يعني أيضًا أن تهيئة أي بنية مع منطقي بداخلها mem :: uninitialized هي UB أيضًا؟

نعم - كما قد تكتشف ، mem::uninitialized يجعل إطلاق النار على نفسك أمرًا تافهًا ، سأذهب إلى حد القول بأنه يكاد يكون من المستحيل استخدامه بشكل صحيح. لهذا السبب نحاول إهماله لصالح MaybeUninit ، وهو أكثر تفصيلاً للاستخدام ، لكن له فائدة ، لأنه اتحاد ، يمكنك تهيئة القيم "حسب الأجزاء" دون أن تتحقق فعليًا القيمة نفسها في حالة _invalid_. يجب فقط أن تكون القيمة _valid_صالحة بالكامل عند استدعاء الشخص into_inner() .

قد تكون مهتمًا بقراءة أقسام nomicon حول التهيئة المحددة وغير المحددة (un): https://doc.rust-lang.org/nomicon/checked-uninit.html فهي تغطي كيفية تهيئة let x: bool; يعمل في صدأ آمن. يُرجى ملء المشكلات إذا كان التفسير غير واضح أو كان هناك أي شيء لا تفهمه. ضع في اعتبارك أيضًا أن معظم التفسيرات هناك "غير معيارية" لأنها لم تخضع لعملية RFC بعد. ستحاول مجموعة العمل الخاصة بإرشادات التعليمات البرمجية غير الآمنة تقديم توثيق RFC وضمان السلوك الحالي في وقت ما من هذا العام.

هذه طريقة سهلة لتصوير واجهة برمجة تطبيقات مصممة لساقك بحيث يجب توثيقها في كل من mem :: uninitialized و intrinsics :: uninit

المشكلة هي أنه لا توجد حاليًا طريقة صحيحة للقيام بذلك - وهذا هو السبب في أننا نعمل بجد للحصول على MaybeUninit حتى يمكن استبدال هذه الوظائف بوثائقها بعبارة "لا تستخدم".


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

هل سيكون من الممكن تثبيت const غير مهيأة ، as_ptr و
as_mut_ptr قبل بقية واجهة برمجة التطبيقات؟ يبدو لي أن هؤلاء على الأرجح
ستستقر كما هي الآن.

+1 لهذا. سيكون من الرائع أن تتوفر هذه الوظيفة على الوضع الثابت. سيمكن الأشخاص من تجربة مجموعة متنوعة من واجهات برمجة التطبيقات (API) ذات المستوى الأعلى (وربما الآمنة) أعلى هذا المستوى المنخفض الأساسي. ويبدو أن هذا الجانب من API غير مثير للجدل.

بالإضافة إلى ذلك ، أود أن أقترح عدم استقرار get_ref و get_mut مطلقًا ، وإزالتهم تمامًا. عادةً ما يكون العمل مع المراجع أكثر أمانًا من العمل باستخدام المؤشرات الأولية (وبالتالي قد يميل الأشخاص إلى استخدام هذه الأساليب على as_ptr و as_mut_ptr على الرغم من أنها تم وضع علامة عليها على أنها غير آمنة) ، ولكن في هذه الحالة هم هي أكثر خطورة من طرق المؤشر الخام لأنها يمكن أن تسبب UB بينما لا تستطيع طرق المؤشر ذلك.

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

بافتراض https://github.com/rust-lang/rfcs/pull/2582 ، هل نحن على يقين تمامًا من أن (1) ليس حتى UB على الرغم من أن (2) هو ، و (1) يحتوي أيضًا على إلغاء تعريف للمؤشر يشير إلى ذاكرة غير مهيأة؟

(1) unsafe { ptr::write_unaligned(&mut ((*uninit_foo.as_mut_ptr()).bar) as *mut bool, true); }
(2) let x: bool = mem::uninitialized();

وإذا كان الأمر كذلك ، فما المنطق وراء ذلك (نأمل أن نتمكن من وضع بعض المناقشة حول هذه المسألة في وثائق ربما Uninit)؟ أظن شيئًا من هذا القبيل لأنه في (1) تظل القيمة غير المرجعية دائمًا "rvalue" ولا تصبح أبدًا و "lvalue" ، بينما في (2) يصبح المنطقي غير الصالح "lvalue" وبالتالي يجب تحقيقه في الذاكرة (لست متأكدًا تمامًا من المصطلح الصحيح لهذا في Rust ، لكنني رأيت هذه المصطلحات المستخدمة في C ++).

وهل يعتقد الآخرون أنه سيكون من المفيد إنشاء RFC لبناء جملة الوصول إلى المجال على مؤشرات أولية يتم تقييمها مباشرة في مؤشر خام للحقل لتجنب هذا الالتباس في المقام الأول؟

إذا كانت القاعدة "لم يتم إنشاء مرجع إلى ذاكرة غير مهيأة مطلقًا"

لا أعتقد أنه ينبغي أن تكون هذه هي القاعدة ، لكنها قد تكون كذلك. تتم مناقشته الآن في UCG.

هل نحن متأكدون تمامًا من أن (1) ليس حتى UB على الرغم من أن (2) هو ، و (1) يحتوي أيضًا على إلغاء تعريف لمؤشر يشير إلى ذاكرة غير مهيأة؟

سؤال جيد! لكن نعم ، نحن - بدافع القص ، أساسًا. فكر في &mut foo as *mut bool كـ &raw mut foo ، وهو تعبير ذري من النوع *mut bool . لا توجد إشارة هنا ، مجرد ptr خام للذاكرة غير المهيأة - وهذا أمر جيد بالتأكيد.

let x: bool = mem::uninitialized();

هذا هو UB. لا علاقة له بالمراجع.

كان افتراضي الضمني أن

أنا مندهش تمامًا من هذا. أن RFC هو حول المراجع فقط. لماذا تفترض أنه يغير شيئًا عن القيم المنطقية؟

RalfJung أفترض أنني اعتقدت أنه لم يكن UB لأن القيمة غير المحددة كانت غير قابلة للرصد لأنه تم استبدالها على الفور بقيمة منطقية صالحة. لكن أعتقد أن هذا ليس هو الحال؟

للحصول على أمثلة أكثر تعقيدًا ، حيث تنفذ القيمة في x Drop ، سيكون المؤشر الأولي مطلوبًا للكتابة فوق القيمة ، ولهذا السبب اعتقدت أن rfc 2582 ضروري لتجنب UB.

أفترض أنني اعتقدت أنه لم يكن UB لأن القيمة غير المحددة كانت غير قابلة للرصد لأنه تم استبدالها على الفور بقيمة منطقية صالحة. لكن أعتقد أن هذا ليس هو الحال؟

تستمر الدلالات بيانًا ببيان (بالنظر إلى MIR). يجب أن يكون لكل بيان معنى. يتجسد let x: bool = mem::uninitialized(); منطقيًا سيئًا ، ولا يهم ما سيحدث لاحقًا - يجب ألا تتجسد منطقية سيئة.

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

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

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

هل يستلزم ذلك سلوك غير محدد؟

نختار السلوك غير المحدد وغير المحدد. هذا جزء من تصميم اللغة. سلوك غير معرف لا يكاد أي وقت مضى "ضروري" في حد ذاتها - ولكن من الضروري لتمكين المزيد من التحسينات. لذا فإن الفن هنا هو إيجاد تعريف للسلوك غير المحدد (على أنه متناقض كما قد يبدو ذلك ^ ^) والذي يتيح كلاهما التحسينات المرغوبة ويتوافق مع توقعات المبرمجين (غير الآمنة).

لا أفهم تمامًا معنى "السلوك غير المحدد".

لقد كتبت منشور مدونة حول ذلك ، ولكن الإجابة المختصرة هي أن السلوك غير المحدد هو عقد بينك وبين المترجم -

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

على وجه الخصوص ، لقد قمنا بالفعل بإرسال معلومات لـ LLVM لم نتمكن من إصدارها بشكل صحيح مع عقد أضعف. يمكننا أن نقرر تغيير ما نقول لـ LLVM ، بالطبع - ولكن إذا كان الاختيار بين "يجب أن يستخدم الرمز غير الآمن MaybeUninit عند التعامل مع ذاكرة غير مهيأة" و "يمكن تحسين كل التعليمات البرمجية بشكل أقل" ، يبدو الأول مثل الخيار الأفضل.

أخذ مثالك:

let x: bool = mem::uninitialized();

هذا الرمز هو UB في rustc اليوم. إذا نظرت إلى LLVM IR (غير المُحسَّن) مقابل mem::uninitialized::<bool>() ، فهذا ما تحصل عليه:

; core::mem::uninitialized
; Function Attrs: inlinehint nonlazybind uwtable
define zeroext i1 @_ZN4core3mem13uninitialized17h6c99c480737239c2E() unnamed_addr #0 !dbg !5 {
start:
  %tmp_ret = alloca i8, align 1
  %0 = load i8, i8* %tmp_ret, align 1, !dbg !14, !range !15
  %1 = trunc i8 %0 to i1, !dbg !14
  br label %bb1, !dbg !14

bb1:                                              ; preds = %start
  ret i1 %1, !dbg !16
}
; snip
!15 = !{i8 0, i8 2}

بشكل أساسي ، تخصص هذه الوظيفة بايتًا واحدًا على المكدس ثم تقوم بتحميل هذا البايت. ومع ذلك ، تم تمييز الحمل بـ !range ، مما يخبر LLVM أن البايت يجب أن يكون بين 0 <= x <2 ، أي يمكن أن يكون 0 أو 1. ستفترض LLVM أن هذا صحيح ، والسلوك غير محدد إذا تم انتهاك هذا القيد.

باختصار ، المشكلة ليست في كثير من المتغيرات غير المهيأة بحد ذاتها ، إنها حقيقة أنك تنسخ وتنقل القيم التي تنتهك قيود النوع.

شكرا لكما على العرض! أصبح الأمر أكثر وضوحا الآن!

أفترض أن إنهاء المكالمة الأساسي هو أنني لا أفهم تمامًا معنى "السلوك غير المحدد".

هذه السلسلة من منشورات المدونات (التي تحتوي على أمثلة مثيرة للاهتمام / مخيفة في المنشور الثاني) مفيدة جدًا ، على ما أعتقد: http://blog.llvm.org/2011/05/what-every-c-programmer-should-know .لغة البرمجة

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

(لا يعني ذلك أنني أتطوع لكتابة مثل هذه الوثائق. فأنا أرشح ... من يعرف أكثر مني عن هذا.)

فكرة مثيرة للاهتمام من https://github.com/rust-lang/rust/issues/55422#issuecomment -433943803: يمكننا تحويل طرق مثل into_inner إلى وظائف ، بحيث يتعين عليك كتابة MaybeUninit::into_inner(foo) بدلاً من foo.into_inner() - يوثق ما يحدث بشكل أوضح.

في https://github.com/rust-lang/rust/pull/58129 أقوم بإضافة بعض المستندات ، وإرجاع &mut T من set وإعادة تسمية into_inner إلى into_initialized .

أعتقد بعد ذلك ، وبمجرد حل https://github.com/rust-lang/rust/pull/56138 ، يمكننا المضي قدمًا في تثبيت أجزاء من واجهة برمجة التطبيقات (المُنشئون ، as_ptr ، as_mut_ptr ، set ، into_initialized ).

لماذا ليس MaybeUninit::zeroed() a const fn ؟ ( MaybeUninit::uninitialized() هو const fn )

تحرير: هل يمكن بالفعل جعله const fn باستخدام الصدأ الليلي؟

لماذا ليس MaybeUninit::zeroed() a const fn ؟ ( MaybeUninit::uninitialized() هو const fn )

gnzlbg حاولت ، لكنها تتطلب أحد الإجراءات التالية:

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

@ rust-lang / libs ما هي الظروف المعتادة التي بموجبها ستستخدم دالة بدلاً من طريقة؟ أتساءل عما إذا كانت بعض العمليات هنا يجب أن تكون وظائف بحيث يتعين على الأشخاص الكتابة ، على سبيل المثال ، MaybeUninit::as_ptr(...) . أنا قلق من أن هذا سيؤدي إلى تفجير الكود حتى يصبح غير قابل للقراءة - لكن OTOH ، بعض الوظائف على ManuallyDrop فعلت هذا بالضبط.

RalfJung ما أفهمه هو أنه يتم تجنب الأساليب في الأشياء التي تنتمي إلى المعلمات العامة ، لتجنب إخفاء الطرق من نوع المستخدم - وبالتالي ManuallyDrop::take .

نظرًا لأن MaybeUninit<T> لن يكون أبدًا Deref<Target = T> ، أعتقد أن الأساليب المناسبة هنا.

اسأل عن ردود الفعل وسوف تتلقى. لقد استخدمت MaybeUninit لتنفيذ وظيفة جديدة في std مؤخرًا.

  1. في sys / sgx / ext / arch.rs أستخدمه مع التجميع المضمن. لقد استخدمت get_mut بشكل غير صحيح ، ففكر في المراجع والمؤشرات الأولية ستكون متكافئة (تم إصلاحها في 928efca1). كنت بالفعل في منطقة غير آمنة ، لذا لم ألاحظ الفرق حقًا في البداية.
  2. في sys / sgx / rwlock.rs ، أستخدمه للتأكد من أن نمط البت لـ const fn new() هو نفس مُهيئ المصفوفة في ملف رأس C. أنا أستخدم zeroed متبوعًا بـ set لمحاولة التأكد من أن وحدات البت "لا تهتم" هي 0. لا أعرف ما إذا كان هذا هو الاستخدام الصحيح ، ولكن يبدو أنه يعمل بشكل جيد .
  1. سأكون في حيرة من أمري إذا كان out.get_mut() as *mut _ ! = out.as_mut_ptr() . يبدو حقا C ++ العش. آمل أن يتم إصلاحه بطريقة ما.

ما فائدة get_mut() ؟

كان أحد الأشياء التي كنت أتساءل عنها مؤخرًا هو ما إذا كان من المضمون أن يكون لـ MaybeUninit<T> نفس التنسيق مثل T ، وما إذا كان يمكن استخدام شيء من هذا القبيل لتهيئة القيم جزئيًا على الكومة ثم تحويلها إلى ملف كامل القيمة المبدئية ، مثل شيء مثل ( ملعب كامل )

struct Foo {
    x: i32,
}

let mut partial: Box<MaybeUninit<Foo>> = Box::new(MaybeUninit::uninitialized());
let complete: Box<Foo> = unsafe {
    ptr::write(&mut (*partial.as_mut_ptr()).x, 5);
    mem::transmute(partial)
};

وفقًا لـ Miri ، يعمل هذا المثال (على الرغم من أنني أدرك الآن أنني لا أعرف ما إذا كان نقل الصناديق من الأنواع ذات التخطيط المماثل هو الصوت نفسه).

@ Nemo157 لماذا تحتاج نفس تخطيط الذاكرة عندما يكون لديك into_inner ؟

Pzixel لتجنب نسخ القيمة بعد التهيئة ، تخيل أنها تحتوي على مخزن مؤقت سعة 100 ميجابايت والذي سيؤدي إلى تجاوز سعة المكدس إذا تم تخصيصه على المكدس. على الرغم من أن كتابة حقيبة الاختبار يبدو أن هذا يتطلب واجهة برمجة تطبيقات إضافية fn uninit_boxed<T>() -> Box<MaybeUninit<T>> للسماح بتخصيص مربع غير مهيأ دون لمس المكدس.

استخدام بناء الجملة box للسماح بتخصيص مساحة كومة الذاكرة المؤقتة غير المهيأة ، يمكنك أن ترى أن نقل مثل هذا يعمل ، أثناء محاولة استخدام into_initialized يتسبب في تجاوز المكدس: ساحة اللعب

@ Nemo157 ربما يكون من الأفضل فرض المترجم لتحسين النسخ بعيدًا؟ أعتقد أنه يجب أن يفعل ذلك على أي حال ، ولكن يمكن أن تكون هناك سمة للتأكد من أن المجمعات تفعل ذلك.

@ Nemo157

كان أحد الأشياء التي كنت أتساءل عنها مؤخرًا هو ما إذا كان من المضمون أن يكون لـ MaybeUninit<T> نفس التنسيق مثل T ، وما إذا كان يمكن استخدام شيء من هذا القبيل لتهيئة القيم جزئيًا على الكومة ثم تحويلها إلى ملف كامل القيمة الأولية ،

وأعتقد أن هذا مكفولة، وأنك الشفرة صحيحة، مع اثنين من المحاذير:

  • اعتمادًا على النوع الذي تستخدمه (وخاصة في الكود العام) ، قد تحتاج إلى ptr::write_unaligned .
  • إذا كان هناك المزيد من الحقول ، وتمت تهيئة بعضها فقط ، فلا يجب عليك الإرسال إلى T حتى تتم تهيئة جميع الحقول بالكامل .

هذه أيضًا حالة استخدام أنا مهتم بها ، حيث أعتقد أنه يمكن دمجها مع proc-macro لتوفير تجريد آمن في المكان.

Pzixel إذا كان لديه نفس تخطيط الذاكرة ، فيمكنك تجنب نسخ بنية البيانات بالكامل بمجرد إنشائها. بالطبع قد يتجاهل المترجم النسخة ، وقد لا يهم الهياكل الصغيرة. لكنها بالتأكيد شيء جميل.

nicoburns نعم ، أراها الآن. أنا أتحدث فقط أنه قد يكون هناك بعض السمات ، على سبيل المثال #[same_layout] أو #[elide_copying] ، أو كلاهما ، أو أي شيء آخر ، للتأكد من أنه يعمل بنفس طريقة transmute . أو ربما تغيير تنفيذ into_constructed لتجنب النسخ الإضافي. أتوقع أن يكون هذا سلوكًا افتراضيًا ، ليس فقط للأشخاص الأذكياء الذين يقرؤون المستندات حول التخطيط. أعني أن الكود الخاص بي يستدعي into_constructed وأحصل على نسخة إضافية ، لكن @ Nemo157 يتصل فقط بـ transmute وهو بخير. لا يوجد سبب يمنع into_constructed فعل الشيء نفسه.

سأكون في حيرة من أمري إذا كان out.get_mut() as *mut _ ! = out.as_mut_ptr() . يبدو حقا C ++ العش. آمل أن يتم إصلاحه بطريقة ما.

ما الفائدة من get_mut() ؟

لقد أوضحت نقطة مماثلة أعلاه أن get_mut() و get_ref() يحتمل أن تكون مربكة / تجعل من السهل استدعاء سلوك غير محدد عن طريق الخطأ (لأنها تعطي الوهم بأنها بدائل أكثر أمانًا لـ as_ptr() و as_mut_ptr() ، لكنها في الواقع أقل أمانًا من تلك الطرق).

أعتقد أنهم ليسوا في المجموعة الفرعية لواجهة برمجة التطبيقات التي اقترح RalfJung التثبيت (انظر: https://www.ralfj.de/blog/2019/02/12/all-hands-recap.html)

RalfJung فيما يتعلق ptr::freeze() (https://www.ralfj.de/blog/2019/02/12/all-hands-recap.html):

هل من المنطقي أن يكون لديك طريقة مماثلة لإنشاء MaybeUninit ؟ ( MaybeUninit::frozen() ، MaybeUninit::abitrary() أو ما شابه). حدسيًا ، يبدو أن مثل هذه الذاكرة ستكون فعالة مثل الذاكرة غير المهيأة حقًا للعديد من حالات الاستخدام ، دون الحاجة إلى تكلفة الكتابة على الذاكرة مثل zeroed . ربما يمكن التوصية به على مُنشئ uninitialized ما لم يكن الناس متأكدين حقًا من أنهم بحاجة إلى ذاكرة غير مهيأة؟

في هذه الملاحظة ، ما هي حالات الاستخدام التي تحتاج فيها حقًا إلى ذاكرة "غير مهيأة" بدلاً من ذاكرة "مجمدة"؟

تضمين التغريدة

1. I'd be very confused if `out.get_mut() as *mut _` != `out.as_mut_ptr()`. Looks really C++ish. I hope it would be fixed somehow.

وأشار. السبب وراء اقتراح بعض الأشخاص لهذا هو أنه قد يكون من المفيد إعلان أن &mut ! غير مأهول (كما هو الحال في ، وجود هذه القيمة هو UB). ومع ذلك ، مع MaybeUninit::<!>::uninitiailized().get_mut() ، أنشأنا مثل هذه القيمة. هذا هو السبب في أن as_mut_ptr أقل خطورة - فهو يتجنب إنشاء مرجع.

nicoburns (لاحظ أن freeze ليست فكرتي ، لقد كنت جزءًا من المناقشة وأحب الاقتراح كثيرًا.)

أعتقد أنهم ليسوا في المجموعة الفرعية لواجهة برمجة التطبيقات التي اقترح RalfJung التثبيت

صيح. وربما لا يجب أن نحصل عليها على الإطلاق.

هل من المنطقي أن يكون لديك طريقة مماثلة لإنشاء MaybeUninit ؟ ( MaybeUninit::frozen() ، MaybeUninit::abitrary() أو ما شابه).

نعم! كنت سأقترح إضافة هذا بمجرد استقرار MaybeUninit الأساسي و ptr::freeze قد هبط.

في هذه الملاحظة ، ما هي حالات الاستخدام التي تحتاج فيها حقًا إلى ذاكرة "غير مهيأة" بدلاً من ذاكرة "مجمدة"؟

هذا يحتاج إلى مزيد من الدراسة والقياس المعياري ، والتوقع هو أنه قد يكلف الأداء لأن LLVM لن تقوم بالتحسينات التي يمكن أن تفعلها بخلاف ذلك.

(سأعود إلى التعليقات الأخرى أيضًا ، عندما يكون لدي الوقت).

إن قدرةPzixel على إنشاء كائنات مباشرة في الذاكرة المخصصة مسبقًا أمر غير تافه ، وقد تم قبول طلبين من RFC لتنفيذ مثل هذا الشيء (منذ أكثر من 4 سنوات!) ، ولكن تم رفضهما منذ ذلك الحين وتمت إزالة معظم التنفيذ (باستثناء بناء الجملة box الذي استخدمته أعلاه). إذا كنت تريد مزيدًا من التفاصيل ، فسيكون موضوع i.rl.o حول الإزالة هو أفضل مكان للبدء.

كما يذكر nicoburns ، يمكن استخدام MaybeUninit ككتلة بناء لحل أقل راحة يعتمد على المكتبة لنفس المشكلة ، وهو مفيد جدًا كطريقة لبدء تجربة المفهوم ومعرفة نوع واجهات برمجة التطبيقات يسمح بالبناء. هذا يعتمد فقط على ما إذا كان بإمكان MaybeUninit توفير الضمانات المطلوبة لبناء مثل هذا الحل.

@ Nemo157 أقترح فقط استخدامه في مكان واحد ، لا شيء للتعامل مع الحالة العامة غير التافهة.

jethrogb شكرا جزيلا! لذا يبدو أن API يعمل بشكل جيد بالنسبة لك الآن؟

2. في sys / sgx / rwlock.rs ، أستخدمه للتأكد من أن نمط البت الخاص بـ const fn new() هو نفس مُهيئ مصفوفة في ملف رأس C.

واو ، هذا جنون. ^ ^ لكن أعتقد أنه يجب أن يعمل ، إنه const fn بدون أي حجج بعد كل شيء ، لذا يجب أن يعيد نفس الشيء دائمًا ...

كان أحد الأشياء التي كنت أتساءل عنها مؤخرًا هو ما إذا كان من المضمون أن يكون لـ MaybeUninit<T> نفس التنسيق مثل T ، وما إذا كان يمكن استخدام شيء من هذا القبيل لتهيئة القيم جزئيًا على الكومة ثم تحويلها إلى ملف كامل قيمة مهيأة

في قائمة الأشياء التي يجب أن نضيفها في النهاية يوجد شيء مثل

fn into_initialized_box(Box<MaybeUninit<T>>) -> Box<T>

الذي يحول Box .

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

  • اعتمادًا على النوع الذي تستخدمه (وخاصة في الكود العام) ، قد تحتاج إلى ptr::write_unaligned .

في رمز عام لا يمكنك الوصول إلى الحقول. أعتقد أنه إذا كان بإمكانك الوصول إلى الحقول ، فعادة ما تعرف ما إذا كانت البنية معبأة ، وإذا لم تكن كذلك ، فإن ptr::write جيد بما فيه الكفاية. (لا تستخدم الواجب لأن ذلك قد يسقط! ما زلت أنسى ذلك ...)

على الرغم من أن كتابة حقيبة الاختبار يبدو أن هذا يتطلب واجهة برمجة تطبيقات إضافية fn uninit_boxed<T>() -> Box<MaybeUninit<T>> للسماح بتخصيص مربع غير مهيأ دون لمس المكدس.

هذا خطأ ، ولكن نظرًا لأنه قد يكون من الصعب إصلاح هذا zeroed_box الذي يتجنب التصفير في فتحة مكدس ثم memcpying ، وهكذا ... لا أحب كل هذا التكرار. : /

لذلك أقترح أنه بعد / بالتوازي مع الاستقرار الأولي ، يجتمع بعض الأشخاص الذين لديهم حالات استخدام لتذكر غير مهيأ على الكومة (بشكل أساسي ، خلط Box و MaybeUninit ) معًا وتصميم الحد الأدنى إمكانية تمديد API لذلك. كما أعرب eddyb عن اهتمامه بهذا. هذا لا يتعلق حقًا بإهمال mem::uninitialized ، لذلك أعتقد أن هذا يجب أن يحصل على مكانه الخاص للمناقشة ، بعيدًا عن مشكلات التتبع (الطريق - الكبيرة - بالفعل).

ملاحظاتي الخاصة: أنا سعيد بشكل عام بـ MaybeUninit<T> . ليس لدي أي شكاوى كبيرة. إنه أقل من مجرد مسدس قدم من mem::uninitialized ، وهو أمر رائع. تعد طرق const new و uninitialized رائعة. أتمنى أن تكون المزيد من الطرق ثابتة ، ولكن كما أفهمها ، يتطلب الكثير منها مزيدًا من التقدم على const fn بشكل عام قبل أن يتم صنعها const .

أرغب في الحصول على ضمان أقوى من "نفس التصميم" مقابل T و MaybeUninit<T> . أرغب في أن تكون متوافقة مع ABI (فعليًا ، #[repr(transparent)] ، على الرغم من أنني أعرف أن السمة لا يمكن تطبيقها على النقابات) و FFI-safe (على سبيل المثال ، إذا كان T آمن FFI ، إذن يجب أن يكون MaybeUninit<T> آمنًا أيضًا من مؤسسة FFI). (بشكل عرضي ، أتمنى أن نتمكن من استخدام #[repr(transparent)] على النقابات التي تحتوي على حقل واحد فقط ذو حجم إيجابي (كما يمكننا في البنيات))

أنا في الواقع أعتمد على ABI البالغ MaybeUninit<T> في مشروعي للمساعدة في تحسين (ولكن ليس بطريقة غير آمنة ، لذلك لا داعي للذعر). يسعدني الدخول في التفاصيل إذا كان أي شخص مهتمًا ، لكنني سأبقي هذا التعليق موجزًا ​​وأحذف التفاصيل في الوقت الحالي.

mjbshaw شكرا!

أتمنى أن نتمكن من استخدام #[repr(transparent)] على النقابات التي تحتوي على حقل واحد فقط ذو حجم إيجابي (كما هو الحال في البنيات)

بمجرد وجود هذه السمة ، فإن إضافتها إلى MaybeUninit لن يكون لها أي تفكير. في واقع الأمر قد نفذت منطق لهذا بالفعل في rustc ( MaybeUninit<T> بحكم الواقع هو ABI متوافق مع T ، لكننا لا نضمن ذلك.)

كل ما يتطلبه الأمر هو أن يكتب شخص ما RFC ويراه جيدًا ، وإضافة بعض الشيكات التي تتأكد من أن النقابات repr(transparent) تحتوي فقط على حقل واحد غير ZST. هل ترغب في تجربة ذلك؟ :د

كل ما يتطلبه الأمر هو أن يكتب شخص ما RFC ويراه جيدًا ، وإضافة بعض الشيكات التي تتأكد من أن النقابات repr(transparent) تحتوي فقط على حقل واحد غير ZST. هل ترغب في تجربة ذلك؟ :د

RalfJung اسأل وستتلقى!

نسخة إلى https://github.com/rust-lang/rust/pull/58468

هذا يترك فقط واجهة برمجة التطبيقات (API) التي أعتقد أنه يمكننا الاستقرار بشكل معقول في maybe_uninit ، ونقل الباقي إلى بوابات ميزة منفصلة.

حسنًا ، لقد هبطت جميع العلاقات العامة التحضيرية ، وذهب أيضًا into_inner .

ومع ذلك ، أود حقًا أن يتم قبول https://github.com/rust-lang/rfcs/pull/2582 قبل الاستقرار ، وإلا فليس لدينا حتى طريقة لتهيئة بنية مجالًا تلو الآخر - وهذا يبدو كحالة استخدام أولية لـ MaybeUninit . نحن قريبون جدًا من الحصول على جميع الصناديق اللازمة لبدء FCP.

لقد قمت للتو بتحويل الكود الخاص بي لاستخدام MaybeUninit . هناك عدد غير قليل من الأماكن حيث كان بإمكاني استخدام طريقة take والتي تعمل على &mut self بدلاً من self . أستخدم حاليًا x.as_ptr().read() لكني أشعر أن x.take() أو x.take_initialized() سيكون أكثر وضوحًا.

Amanieu يبدو هذا مشابهًا جدًا into_inner . ربما يمكننا محاولة تجنب الازدواجية هنا؟

😉

الطريقة take من Option لها دلالات أخرى. x.as_ptr().read() لا يغير القيمة الداخلية لـ x ، لكن Option::take حاول استبدال القيمة. قد يكون مضللا بالنسبة لي.

@ qwerty19106 x.as_ptr().read() على MaybeUninit _semantically_ يأخذ القيمة ويترك الغلاف غير مهيأ مرة أخرى ، يحدث فقط أن القيمة غير المهيأة المتبقية لها نفس نمط البت مثل القيمة التي تم حذفها .

أستخدم حاليًا x.as_ptr().read() لكني أشعر أن x.take() أو x.take_initialized() سيكون أكثر وضوحًا.

أجد هذا فضوليًا ، هل يمكن أن تشرح لماذا؟

من وجهة نظري ، فإن الطريقة التي تشبه take مضللة إلى حد ما لأنها على عكس كل من take و into_initialized ، فهي لا تحمي من الاستلام مرتين. في الواقع ، بالنسبة للأنواع Copy (وفي الواقع بالنسبة لقيم Copy مثل None as Option<Box<T>> ) ، فإن أخذها مرتين أمر جيد تمامًا! لذا ، فإن القياس مع take لا ينطبق حقًا ، من وجهة نظري.

يمكننا تسميتها read_initialized() ، لكن في هذه المرحلة أتساءل بجدية ما إذا كان هذا بالفعل أوضح من as_ptr().read() .

يأخذ x.as_ptr().read() على MaybeUninit _semantically_ الوادي للخارج ويترك الغلاف غير مهيأ مرة أخرى ، يحدث فقط أن القيمة غير المهيأة المتبقية لها نفس نمط البت مثل القيمة التي تم إزالتها.

MaybeUninit حقًا على ثابت دلالي مفيد ، لذلك لست متأكدًا من أنني أتفق تمامًا مع هذا البيان. TBH لست مقتنعًا بأنه من المفيد النظر في العمليات على MaybeUninit بأي طريقة أخرى غير مجرد تأثيرها التشغيلي الخام.

RalfJung هممممممممممممممممممممممممممممششششششششششششششششششششششششششششششاقششباشباشبايتهاسدةسدةسدة المنطقةالقة البدال ” فيما يتعلق بكيفية استخدام المستخدم للنوع ، يجب أن تفترض أن القيمة لم تتم تهيئتها مرة أخرى بعد قراءتها (إلا إذا كنت تعرف بشكل ملموس أن النوع هو Copy ).

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

RalfJung في جميع static mut حيث يتم وضع قيمة ، ثم أخذها لاحقًا. نظرًا لأنه لا يمكنني استهلاك ثابت ، لا يمكنني استخدام into_uninitialized .

Amanieu ما كنت x.take_initialized() أوضح من x.as_ptr().read() ؟

@ Nemo157

كنت أتمنى نوعًا ما أن تظل ميري تتعقب 0 عدد مرات القراءة من الذاكرة غير المهيأة ، لكنها لا تظهر

قراءة بطول 0 للذاكرة غير المهيأة ليست أبدًا UB ، فلماذا تهتم ميري بها؟

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

بالتأكيد، يمكنك تنتهك الثوابت السلامة دون قراءة أي وقت مضى الذاكرة غير مهيأ. يمكنك أيضًا استخدام MaybeUninit::zeroed().into_initialized() لذلك. أنا لا أرى المشكلة.
يتمثل "التفاعل الغريب" هنا في أنك قمت بإنشاء قيمتين من نوع لا يحق لك تكوينهما. هذا كله يتعلق بثوابت الأمان البالغة Spartacus ، ولا علاقة له بثوابت الصلاحية.

هذا هو السبب في أنني أعتقد أن read_initialized() ينقل بشكل أفضل ما يحدث: لقد قرأنا البيانات ، ونزعم أنها مهيأة بشكل صحيح (بما في ذلك التأكد من أنه يُسمح لنا بالفعل بإنشاء هذه القيمة في هذا النوع). هذا ليس له أي تأثير على نمط البت الذي لا يزال مخزنًا في MaybeUninit .

RalfJung أنا MaybeUninit أنه Option ، لكن بدون العلامة. في الواقع ، كنت سابقًا أستخدم صندوق الخيار غير المميز لهذا الغرض بالضبط ، ولديه طريقة take لاستخراج القيمة من الاتحاد.

Amanieushepmaster أضفت read_initialized في https://github.com/rust-lang/rust/pull/58660. ما زلت أعتقد أن هذا اسم أفضل من take_initialized . هل هذا يلبي احتياجاتك؟

يضيف هذا العلاقات العامة أيضًا أمثلة على بعض الأساليب الأخرى ، نرحب بالتعليقات!

أنا سعيد بـ read_initialized .

أثناء تواجدي فيه ، ربحت أيضًا MaybeUninit<T>: Copy إذا T: Copy . يبدو أنه لا يوجد سبب وجيه لعدم القيام بذلك.

حسنًا ، ربما يكون get_initialized اسمًا أفضل؟ إنها أنواع مكملة من set ، بعد كل شيء.

أو ربما يجب إعادة تسمية set إلى write ؟ هذا من شأنه أيضا تحقيق الاتساق.

لقد قمت بتحويل الكود الخاص بي لاستخدام MaybeUninit ووجدت أن العمل مع الشرائح غير المهيأة أمر غير مريح للغاية. أعتقد أنه يمكن تحسين هذا إذا كانت لدينا وظائف لما يلي:

  • التحويل الآمن من &mut [T] إلى &mut [MaybeUninit<T>] . يسمح هذا فعليًا بمحاكاة معلمات &out باستخدام &mut [MaybeUninit<T>] ، وهو أمر مفيد على سبيل المثال read .
  • تحويل غير آمن من &mut [MaybeUninit<T>] إلى &mut [T] (ونفس الشيء لـ &[T] ) ، ليتم استخدامه بمجرد استدعاء .set على كل عنصر من عناصر الشريحة.

تبدو واجهات برمجة التطبيقات التي أبدو عليها مثل هذا:

// The returned slice is truncated to the number of elements actually read.
fn read<T>(out: &mut [MaybeUninit<T>]) -> Result<&mut [T]>;

أوافق على أن العمل مع الشرائح غير مريح حاليًا ، ولهذا أضفت first_ptr و first_ptr_mut . ولكن ربما يكون هذا بعيدًا عن أفضل واجهة برمجة تطبيقات.

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

تعجبني فكرة إعادة تسمية set إلى write ، مع توفير التناسق مع ptr::write .

على نفس المنوال ، هل read_initialized أفضل حقًا من read ؟ إذا كان القلق يتعلق بالاستخدام العرضي الذي أصبح مخفيًا ، فربما اجعله دالة بدلاً من طريقة ، مثل MaybeUninit::read(&mut v) ؟ يمكن القيام بنفس الشيء مقابل write ، أي MaybeUninit::write(&mut v) للتوافق. تكون المقايضة في كلتا الحالتين بين قابلية الاستخدام والصراحة ، وإذا تم اعتبار الصراحة أفضل في إحدى الحالات ، فلا أرى سبب اختلافها في الحالة الأخرى.

بغض النظر ، حتى يتم الوصول إلى واجهات برمجة التطبيقات هذه ، فأنا أؤيد بشدة الاستقرار باستخدام حد أدنى من واجهة برمجة التطبيقات ، مثل new ، uninitialized ، zeroed ، as_ptr ، as_mut_ptr ، وربما get_ref و get_mut .

وربما get_ref و get_mut .

يجب أن يتم استقرارها فقط بمجرد حلها https://github.com/rust-rfcs/unsafe-code-guidelines/issues/77 ، ويبدو أن الأمر قد يستغرق بعض الوقت ...

التثبيت باستخدام الحد الأدنى من واجهة برمجة التطبيقات ، مثل new ، uninitialized ، zeroed ، as_ptr ، as_mut_ptr

كانت خطتي هي into_initialized ، set / write ، و read_initialized لتكون جزءًا من هذا الحد الأدنى من المجموعة. لكن ربما لا ينبغي أن يكون؟ set / write و read_initialized بسهولة مع الباقي ، لذلك أنا الآن أميل أيضًا إلى عدم تثبيتهم في الدفعة الأولى. لكن الحصول على شيء مثل into_initialized من البداية أمر مرغوب فيه ، IMO.

ربما تجعلها دالة بدلاً من طريقة ، مثل MaybeUninit::read(&mut v) ؟ يمكن القيام بنفس الشيء مقابل write ، أي MaybeUninit::write(&mut v) للتوافق.

مما تمت مناقشته هنا من قبل ، نستخدم فقط نهج الوظيفة الصريح لتجنب المشاكل مع حالات Deref . لا أعتقد أنه يجب علينا تقديم الأسبقية لسبب آخر لاستخدام دالة بدلاً من طريقة.

هل read_initialized أفضل حقًا من read ؟

سؤال جيد! لا أدري، لا أعرف. كان هذا للتماثل مع into_initialized . لكن into_inner طريقة شائعة حيث قد يفقد المرء النظرة العامة على النوع الذي يطلق عليه ، read أقل شيوعًا. وربما يجب أن يكون initialized بدلاً من into_initialized ؟ الكثير من الخيارات ...

مما تمت مناقشته هنا من قبل ، نستخدم فقط نهج الوظيفة الصريح لتجنب المشاكل مع حالات Deref . لا أعتقد أنه يجب علينا تقديم الأسبقية لسبب آخر لاستخدام دالة بدلاً من طريقة.

ماعدا ptr::read و ptr::write هي وظائف وليست طرق. لذلك تم بالفعل تعيين الأسبقية لصالح MaybeUninit::read و MaybeUninit::write .

تحرير : حسنًا ، من الواضح أن هناك طرقًا read و write على المؤشرات أيضًا ... لم يلاحظوا ذلك من قبل ... لكنهم يستهلكون المؤشر ، وهو أمر غير منطقي حقًا لـ MaybeUninit .

الكثير من الخيارات ...

متفق عليه. إلى أن يكون هناك الكثير من التخلص من الدراجات على الطرق الأخرى ، أعتقد فقط new ، uninitialized ، zeroed ، as_ptr ، as_mut_ptr مستعدون حقًا لتحقيق الاستقرار.

ماعدا ptr::read و ptr::write هي دالات وليست طرق. لذلك تم بالفعل تعيين الأسبقية

إنها ليست جزءًا من بنية البيانات ، فهي بالطبع وظائف قائمة بذاتها. وكما لاحظت ، فهي موجودة في الوقت الحاضر كوسائل أيضًا.

لكنهم يستهلكون المؤشر

مؤشرات Raw هي Copy ، لذلك لا يتم استهلاك أي شيء حقًا.

المؤشرات الأولية هي Copy ، لذلك لا يتم استهلاك أي شيء حقًا.

نقطة جيدة...

حسنًا ، v.as_ptr().read() هو بالفعل موجز وواضح جدًا. يجب أن تجعله as_ptr متبوعًا بـ read شيئًا يجب التفكير فيه بعناية ، أكثر من into_initialized . أنا شخصياً أؤيد تعريض as_ptr و as_mut_ptr ، على الأقل في الوقت الحالي. و new و uninitialized و zeroed بالطبع.

Amanieu ماذا عن شيء أشبه بما يمتلكه Cell ، حيث توجد تحويلات آمنة لـ &mut MaybeUninit<[T]> إلى ومن &mut [MaybeUninit<T>] ؟

سيسمح ذلك بما يلي ، والذي يبدو طبيعيًا جدًا بالنسبة لي:

fn read<T>(out: &mut MaybeUninit<[T]>) -> Result<&mut [T]> {
    let split = out.as_mut_slice_of_uninit();
    // ... operate on split ...
    return Some(unsafe { split[0..n].as_uninit_mut_slice().get_mut() })
}

يبدو أيضًا أنه يمثل بدقة أكبر الدلالات للمتصل. بالنسبة لي ، فإن الوظيفة التي تأخذ &mut [MaybeUninit<T>] ستشعر ، كما لو أنها قد تحتوي على منطق مميز لأي منها مناسب وأي منها ليس كذلك. من ناحية أخرى ، فإن أخذ &mut MaybeUninit<[T]> يعبر عن أنه لن يميز بين الخلايا عندما يتعلق الأمر بالبيانات الموجودة بالفعل فيها.

(أسماء الطرق ، بالطبع ، خاضعة لركوب الدراجات - لقد قمت فقط بتقليد ما يفعله Cell .)

eternaleye MaybeUninit<[T]> ليس نوعًا صالحًا لأن النقابات لا يمكن أن تكون DST.

مم ، صحيح

إلى أن يكون هناك الكثير من تسليط الدراجات على الطرق الأخرى ، أعتقد فقط new ، uninitialized ، zeroed ، as_ptr ، as_mut_ptr مستعدون حقًا لتحقيق الاستقرار.

حسنًا ، أعتقد أننا يجب أن نقبل RFC هذا قبل تثبيت أي شيء - وإلا فليس لدينا حتى طريقة معاقبة لتهيئة بنية مجال تلو الآخر ، والتي تبدو وكأنها الحد الأدنى.

لذلك أثناء انتظار التجارب ، يمكننا أن نتحدث قليلاً عن أسماء ما يسمى حاليًا set ، read_initialized و into_initialized . تم اقتراح إعادة التسمية التالية:

  1. set -> write . يبدو أن أفضل تعبير مجازي لـ .as_ptr().read() هو "read" وليس "get" ، ولكن بعد ذلك يجب أن يكون المكمل ( .as_ptr_mut().write() ) هو "write" وليس "set".
  2. read_initialized -> read . تتطابق بشكل رائع مع write ، لكنها غير آمنة. هل هذا (بالإضافة إلى الوثائق) كافٍ لتحذير يجب عليك التأكد يدويًا من أن البيانات قد تمت تهيئتها بالفعل؟ كان هناك اتفاق كبير على أن into_inner غير الآمن ليس كافيًا ، ولهذا السبب قمت بإعادة تسميته إلى into_initialized .
  3. into_initialized -> initialized . إذا كان لدينا كل من read_initialized و into_initialized ، فهذا له تناسق جيد مع IMO - ولكن إذا كان read ، ثم into_initialized يبقى قليلاً. اسم الطريقة طويل جدًا. ومع ذلك ، فإن معظم العمليات المستهلكة تسمى into_* ، مما أعرفه.

أي اعتراضات على (1)؟ وأنا أميل في الغالب ضد (3). (2) لم أقرر: read أسهل في الكتابة ، لكن read_initialized IMO تعمل بشكل أفضل عند قراءة هذا الكود - ويتم قراءة الكود ومراجعته أكثر من كتابته. يبدو من الجيد استدعاء المكان الذي نفترض فيه بالفعل تهيئة الأشياء.

أفكار وآراء؟

حسنًا ، أعتقد أننا يجب أن نقبل RFC هذا قبل تثبيت أي شيء - وإلا فليس لدينا حتى طريقة معاقبة لتهيئة بنية مجال تلو الآخر ، والتي تبدو وكأنها الحد الأدنى.

هل هذا هو المكان الذي أضع فيه قابسًا مقابل offset_of! ؟ :)

لاحظ أن read_initialized هو مجموعة شاملة صارمة من into_initialized (تأخذ &self بدلاً من self ). هل يعقل أن تدعم كليهما؟

هل هذا هو المكان الذي أضع فيه قابسًا مقابل offset_of! ؟ :)

إذا تمكنت من الحصول على هذا الاستقرار قبل قبول RFC الخاص بي ، بالتأكيد. ؛)

هل يعقل أن تدعم كليهما؟

نعم IMO. into_initialized أكثر أمانًا لأنه يمنع استخدام نفس القيمة مرتين ، وبالتالي يجب تفضيله على read_initialized كلما أمكن ذلك.

لذا ، فقد أوضحت nikomatsakis هذه النقطة من قبل لكنها لم

لقد قمت للتو بنقل الكثير من التعليمات البرمجية لاستخدام MaybeUninit<T> و into_initialized ووجدتها مطولة بلا داع. الشفرة مطولة بالفعل أكثر بكثير مما كانت عليه من قبل حيث كانت تستخدم "بشكل غير صحيح" باستخدام mem::uninitialized .

أعتقد أنه يجب تسمية MaybeUninit<T> فقط Uninit<T> ، لأنه لجميع الأغراض العملية ، إذا حصلت على MaybeUninit<T> غير معروف ، عليك أن تفترض أنه غير مهيأ ، لذا Uninit<T> سيلخص ذلك بشكل صحيح. أيضًا ، يجب أن يكون into_uninitialized فقط into_init() أو ما شابه ذلك لأسباب تتعلق بالاتساق.

يمكننا أيضًا استدعاء النوع Uninitialized<T> والطريقة into_initialized ، لكن استخدام اختصار للنوع والصيغة الطويلة للطريقة أو العكس هو تناقض مؤلم. من الناحية المثالية ، يجب أن أتذكر فقط أن "واجهات برمجة تطبيقات Rust تستخدم الاختصارات / النماذج الطويلة" وهذا كل شيء.

نظرًا لأن الاختصارات يمكن أن تكون غامضة بالنسبة لأشخاص مختلفين ، فإنني أفضل استخدام النماذج الطويلة في كل مكان وتسميتها يوميًا. لكن استخدام مزيج IMO هو أسوأ ما في العالمين. يميل Rust إلى استخدام الاختصارات أكثر من النماذج الطويلة ، لذلك ليس لدي أي شيء مقابل Uninit<T> كاختصار و .into_init() كاختصار آخر للطريقة.

لا أحب into_initialized() ، لأنه يبدو أن هناك تحولًا جاريًا لتهيئة القيمة. أنا أفضل take_initialized() . أدرك أن توقيع النوع ينحرف عن الطرق الأخرى take ، لكنني أعتقد أنه أكثر وضوحًا ، من الناحية الدلالية ، وأعتقد أن الوضوح الدلالي يجب أن يحل محل الاقتراض / اتساق الحركة يمكن أن تكون البدائل الأخرى التي ليس لها بالفعل أسبقية لكونها أقترض قابلة للتغيير move_initialized أو consume_initialized .

بالنسبة إلى set() مقابل write() ، أفضّل بشدة write() لاستدعاء التشابه مع as_ptr().write() ، والذي سيكون اسمًا مستعارًا له.

وأخيرًا ، إذا كان هناك take_initialized() أو ما شابه ، فأنا أفضل read_initialized() على read() بسبب توضيح السابق.

تحرير : ولكن للتوضيح ، أعتقد أن التمسك بـ as_ptr().write() و as_ptr().read() هو أكثر وضوحًا وأكثر احتمالًا لتشغيل الدوائر العقلية لـ DANGER DANGER .

gnzlbg كان لدينا FCP لاسم النوع ، ولست متأكدًا مما إذا كان يتعين علينا إعادة فتح هذه المناقشة.

ومع ذلك ، فإنني أحب اقتراح استخدام "init" بشكل ثابت ، كما في MaybeUninit::uninit() و x.into_init() .

لا أحب into_initialized() ، لأنه يبدو أن هناك تحولًا جاريًا لتهيئة القيمة.

لا تقوم طرق into كثير من الأحيان بأي تحويل بخلاف عرض نفس البيانات (المملوكة) في نوع معين - انظر على سبيل المثال طرق into_vec المتنوعة.

أنا بخير مع take_initialized(&mut self) (بالإضافة إلى in_init) ، لكنني أعتقد أنه يجب إعادة الحالة الداخلية إلى undef .

العودة إلى الحالة الداخلية

https://github.com/rust-lang/rust/issues/53491#issuecomment -437811282

هذا لا ينبغي أن يغير محتوى self على الإطلاق. يتم نقل الملكية فقط بعيدًا ، لذا فهي الآن بشكل فعال في نفس الحالة كما كانت عند إنشائها بدون تهيئة.

تمت مناقشة العديد من هذه الأشياء بالفعل في أكثر من 200 تعليق مخفي.

تمت مناقشة العديد من هذه الأشياء بالفعل في أكثر من 200 تعليق مخفي.

لقد كنت أتابع المناقشة منذ فترة ، وقد أكون مخطئًا ، لكن لا أعتقد أن هذه النقطة قد أثيرت من قبل. على وجه الخصوص ، لا يقترح التعليق الذي تقتبسه "إعادة الحالة الداخلية إلى undef " ، ولكن جعلها تعادل ptr::read (وهو ترك الحالة الداخلية دون تغيير). ما أقترحه هو المعادل المفاهيمي لـ mem::replace(self, MaybeUninit::uninitialized()) .

المعادل المفاهيمي لـ mem::replace(self, MaybeUninit::uninitialized()) .

بسبب معنى undef ، هذا يعادل read : https://rust.godbolt.org/z/e0-Gyu

@ scottmcm لا ليس كذلك. مع read ، فإن ما يلي قانوني:

let mut x = MaybeUninit::<u32>::uninitialized();
x.set(13);
let x1 = unsafe { x.read_initialized() };
// `u32` is `Copy`, so we may read multiple times.
let x2 = unsafe { x.read_initialized() };
assert_eq!(x1, x2);

مع take المقترح ، سيكون هذا غير قانوني لأن x2 سيكون undef .

فقط لأن وظيفتين تولدان نفس التجميع لا يعني أنهما متكافئتان.

ومع ذلك ، لا أرى أي فائدة من الكتابة فوق المحتويات بـ undef . إنه يقدم المزيد من الطرق للناس لإطلاق النار على أقدامهم. jethrogb أنت لم تعط أي دافع ، هل يمكن أن تشرح لماذا تعتقد أن هذه فكرة جيدة؟

أنا بخير مع take_initialized(&mut self) (بالإضافة إلى in_init) ، لكنني أعتقد أنه يجب إعادة الحالة الداخلية إلى undef .

كنت أقترح take_initialized(self) بدلاً من into_initialized(self) ، لأنني أعتقد أن الاسم السابق يصف العملية بدقة أكبر. مرة أخرى ، أتفهم أن take يأخذ عادةً &mut self و into عادةً ما يأخذ self ، لكنني أعتقد أن التسمية الدقيقة من الناحية الدلالية أكثر أهمية من كتابتها باستمرار تسمية. ربما يجب استخدام اسم مختلف ، مثل move_initialized أو transmute_initialized .

ومرة أخرى ، بالنسبة إلى v.write() و v.read_initialized() ، لا أرى أي قيمة موجبة تزيد عن v.as_ptr().write() و v.as_ptr().read() . يبدو أن الأخيرين أقل عرضة لإساءة استخدامهما.

ومرة أخرى ، بالنسبة إلى v.write() و v.read_initialized() ، لا أرى أي قيمة موجبة تزيد عن v.as_ptr().write() و v.as_ptr().read() . يبدو أن الأخيرين أقل عرضة لإساءة استخدامهما.

v.write() (أو v.set() أو أيًا كان ما نسميه هذه الأيام) آمن. v.as_ptr().write() يتطلب unsafe كتلة، الذي هو نوع من مزعج. على الرغم من أنني أوافق على v.read_init() مقابل v.as_ptr().read() . v.read_init() ضروري.

كنت أقترح take_initialized (self) بدلاً من in_initialized (self) ، لأنني أعتقد أن الاسم السابق يصف العملية بدقة أكبر. مرة أخرى ، أفهم أن الأمر يأخذ عادةً a & mut ويأخذ نفسه عادةً ، لكنني أعتقد أن التسمية الدقيقة لغويًا أكثر أهمية من التسمية المكتوبة باستمرار.

أشعر بقوة أن into_init(ialized) هو أيضًا أكثر دقة من الناحية المعنوية هنا - فهو يستهلك MaybeUninit ، بعد كل شيء.

mjbshaw آه ، نعم ، هذا هو الحال. لم ألاحظ ذلك ... حسنًا ، في هذه الحالة ، ألغيت كل تعليقاتي السابقة حول set / write . ربما يكون set أكثر منطقية ؛ حدد Cell و Pin set طرق MaybeUninit::set لن يسقط أي قيم مخزنة مسبقًا ؛ ربما لا يزال هذا أقرب إلى write ... لا أعرف. في كلتا الحالتين ، الوثائق واضحة جدًا.

RalfJung حسنًا ، نسيت take... إذن. ماذا عن الاسم الجديد ، مثل move... أو consume... أو transmute... أو شيء ما؟ أعتقد أن into_init(ialized) مربك للغاية ؛ أنا أيضًا ، فهذا يعني أن القيمة قيد التهيئة ، في حين أننا في الواقع نؤكد ضمنيًا أنها قد تمت تهيئتها بالفعل

عندما نؤكد ضمنيًا أنه قد تمت تهيئته بالفعل.

أعتقد أنه من المفيد أن نذكر مرة أخرى أن الشيء الوحيد الذي يؤكده into_init هو أن القيمة تفي بـ _validity invariant_ البالغ T ، والذي لا يجب الخلط بينه وبين T يجري "تهيئة" في أي بالمعنى العام للكلمة.

فمثلا:

pub mod foo {
    pub struct AlwaysTrue(bool);
    impl AlwaysTrue { 
        pub fn new() -> Self { Self(true) }
        /// It is impossible to initialize `AlwaysTrue` to false
        /// and unsafe code can rely on `is_true` working properly:
        pub fn is_true(x: bool) -> bool { x == self.0 }
    }
}

pub unsafe fn improperly_initialized() -> foo::AlwaysTrue {
    let mut v: MaybeUninit<foo::AlwaysTrue> = MaybeUninit::uninitialized();
    // let v = v.into_init(); // UB: v is invalid
    *(v.as_mut_ptr() as *mut u8) = 3; // OK
    // let v = v.inti_init(); // UB v is invalid
    *(v.as_mut_ptr() as *mut bool) = false; // OK
    let v = v.into_init(); // OK: v is valid, even though AlwaysTrue is false
    v
}

هنا يتم "تهيئة" القيمة المرجعة لـ improperly_initialized بمعنى أنها تفي بـ _validity invariant_ البالغ T ، ولكن ليس بمعنى أنها تفي بـ _safe ثابت_ T ، والتمييز دقيق ولكنه مهم ، لأنه في هذه الحالة هذا التمييز هو ما يتطلب الإعلان عن improperly_initialized على أنه unsafe fn .

عندما يتحدث معظم المستخدمين عن شيء ما "تمت تهيئته" ، فإنهم عادةً لا يمتلكون دلالات "صالحة ولكن ربما غير آمنة" وهي MaybeUninit::into_init .

إذا أردنا أن نتطرق بشكل مؤلم إلى هذه الأمور ، فقد يكون لدينا Invalid<T> و Unsafe<T> ، لدينا Invalid<T>::into_valid() -> Unsafe<T> ، ونطلب من المستخدمين كتابة uninit.into_valid().into_safe() . ثم فوق improperly_initialized سيعود Unsafe<T> ، وفقط بعد أن يعيّن المستخدم بشكل صحيح قيمة AlwaysTrue إلى true يمكنه بالفعل الحصول على الخزنة T:

// note: this is now a safe fn
fn improperly_uninitialized() -> Unsafe<foo::AlwaysTrue>;
fn initialized() -> foo::AlwaysTrue {
    let mut v: Unsafe<foo::AlwaysTrue> = improperly_uninitialized();
    unsafe { v.as_mut_ptr() as *mut bool } = true;
    unsafe { v.into_safe() }
}

لاحظ أن هذا يتيح لـ improperly_uninitialized أن يصبح آمنًا fn ، لأنه الآن الثابت بأن AlwaysTrue ليس آمنًا لم يتم ترميزه في "التعليقات" حول الوظيفة ، ولكن في أنواع.

لا أعرف ما إذا كان النهج المؤلم يستحق المتابعة. الهدف MaybeUninit هو التسوية ، للسماح للمستخدمين بالتعامل مع الذاكرة غير المهيأة وغير الصالحة ، ولكن دون وضع هذه الفروق في مواجهة المستخدمين. أنا شخصياً أعتقد أنه لا يمكننا أن نتوقع أن يعرف المستخدمون هذه الفروق ما لم نضعها صراحةً على وجوههم ، ويجب على المرء أن يعرف هذا التمييز حتى يتمكن من استخدام MaybeUninit بشكل صحيح. بخلاف ذلك ، قد يكتب الأشخاص fn improperly_uninitialized() -> AlwaysTrue كخزنة fn ، ويعيدون فقط AlwaysTrue لأنه حسنًا ، قاموا "بتهيئة" ذلك.

شيء واحد يمكن للمرء فعله أيضًا باستخدام Invalid<T> و Unsafe<T> له سمتان ، ValidityCheckeable و UnsafeCheckeable ، بطريقتين ، ValidityCheckeable::is_valid(Invalid<Self>) و UnsafeCheckeable::is_safe(Unsafe<Self>) ، ولديك طرق Invalid::into_valid و Unsafe::into_safe assert_validity! و assert_safety! عليها.

بدلاً من كتابة ثابت الأمان في تعليق ، يمكنك فقط كتابة رمز الشيك.

أعتقد أنه من الجدير بالذكر أن الشيء الوحيد الذي تؤكده in_init هو أن القيمة تفي بالصلاحية الثابتة لـ T ، والتي لا يجب الخلط بينها وبين أن T يتم "تهيئة" بأي معنى عام للكلمة.

هذا صحيح. OTOH ، أشعر أن "التهيئة" هو وكيل معقول لهذا في التفسير الأول.

وإلا فقد يكتب الأشخاص fn بشكل غير صحيح_uninitialized () -> AlwaysTrue باعتباره fn آمنًا ، ويعيدون رسالة AlwaysTrue غير آمنة لأنهم قاموا "بتهيئة" ذلك.

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

شيء واحد يمكن أن يفعله المرء أيضًا مع Invalidوغير آمنله سمتان ، ValidityCheckeable و UnsafeCheckeable ، بطريقتين ، ValidityCheckeable :: is_valid (غير صالح) و UnsafeCheckeable :: is_safe (غير آمن) ، ولديك الطريقتان Invalid :: into_valid و Unsafe :: into_safe assert_validity! وتأكيد السلامة! عليهم.

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

تضمين التغريدة

وماذا عن اسم جديد مثل تحرك ... ، تستهلك ... ، أو تحوِّل ... أو شيء من هذا القبيل؟ أعتقد أن in_init (ialized) محير للغاية ؛ أنا أيضًا ، فهذا يعني أن القيمة قيد التهيئة ، في حين أننا في الواقع نؤكد ضمنيًا أنها قد تمت تهيئتها بالفعل

كيف ينقل move_init "تأكيدًا" أكثر من into_init ؟

تم اقتراح assert_init(italized) سابقًا.

ومع ذلك ، لاحظ أن read أو read_initialized أو as_ptr().read أيضًا لا تقول شيئًا عن تأكيد أي شيء.

إذا أردنا أن نتطرق بشكل مؤلم إلى هذه الأمور ، فقد يكون لدينا Invalid<T> و Unsafe<T> ، لدينا Invalid<T>::into_valid() -> Unsafe<T> ، ونطلب من المستخدمين كتابة uninit.into_valid().into_safe() . ثم أعلى من improperly_initialized سيعود Unsafe<T> ، وفقط بعد أن يحدد المستخدم بشكل صحيح قيمة AlwaysTrue إلى true يمكنه بالفعل الحصول على الخزنة T:

gnzlbg مرحبًا هذا أنيق جدًا. يعجبني أن هذا يلقي بالتمييز في وجوه المستخدم بطريقة لا مفر منها. ربما تكون لحظة تدريس جيدة. "صحة" و "أمان" من شأنها أن تجعل الناس يفكرون مرتين؟ uninit.into_valid().into_safe() ليس بهذا الإسهاب على أي حال مقارنة بـ uninit.assume_initialized() أو أي شيء آخر. بالطبع لتحقيق هذا التمييز ، سنحتاج إلى إيجاد اتفاق حول النموذج في المقام الأول. 😅 أشعر أنه يجب علينا التحقيق في هذا النموذج أكثر.

تم اقتراح assert_init(italized) سابقًا.

RalfJung لدينا أيضًا assume_initialized بسبب eternaleye (على ما أظن). راجع https://github.com/rust-lang/rust/issues/53491#issuecomment -440730699 مع قائمة من المبررات المقنعة للغاية.

TBH أشعر أن وجود نوعين هو طريقة مطولة للغاية.

RalfJung هل يمكننا التعمق في ذلك؟ ربما مع بعض المقارنات بين الأمثلة التي تعتقد أنها تظهر درجة عالية من الإسهاب؟

حسنًا ... إذا كنا نفكر في المزيد من واجهات برمجة التطبيقات المطولة ، إذن

uninit.into_inner(uninit.assert_initialized());

يمكن أن تعمل بشكل جيد معناه. تقوم الطريقة الأولى بإرجاع رمز مميز يسجل تأكيدك. تقوم الطريقة الثانية بإرجاع النوع الداخلي ، ولكنها تتطلب منك تأكيد أنه صالح.

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

لقد قمنا أيضًا بتفعيل افتراضاتنا بسبب ternaleye (على ما أعتقد). انظر # 53491 (تعليق) مع قائمة من المبررات المقنعة للغاية.

معرض. assume_initialized جيدًا بالنسبة لي.

أو ربما يكون assume_init ؟ ينبغي أن المرجح أن تكون متسقة مع منشئ، MaybeUninit::uninit() مقابل MaybeUninit::uninitialized() - والتي من المقرر إلى أن يستقر مع الدفعة الأولى، لذلك علينا أن جعل هذا النداء في وقت قريب.

nicoburns لا أرى الفائدة التي

هل يمكننا التعمق في ذلك؟ ربما مع بعض المقارنات بين الأمثلة التي تعتقد أنها تظهر درجة عالية من الإسهاب؟

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

أنا في الواقع أشك بشكل عام في فائدة Unsafe . من منظور المترجم ، سيكون NOP بالكامل ؛ لا يفترض المترجم أبدًا أن بياناتك تفي بثوابت الأمان. من منظور تنفيذ المكتبة ، أشك بشدة في أن قابلية قراءة الكود ستتحسن إذا قمنا ، في تنفيذ Vec ، بتحويل الأشياء إلى Unsafe<Vec<T>> كلما انتهكنا مؤقتًا ثابت الأمان. ومن منظور تعليمي ، أشك في أن أي شخص سوف يتفاجأ عندما ينشئون Vec<T> وهو صالح لكنه غير آمن ، أعطه بعض التعليمات البرمجية الآمنة ، ثم ينفجر كل شيء
قارن هذا مع MaybeUninit المطلوب من منظور المترجم ، وحيث أنك تحتاج إلى توخي الحذر بشأن "السيئ" bool في الكود الخاص بك قد يكون مفاجأة للبعض .

نظرًا لتكلفته الكبيرة ، أعتقد أن Unsafe يحتاج إلى حافز أقوى. لا أرى كيف سيساعد ذلك بالفعل في منع الأخطاء أو تحسين قابلية قراءة الكود.

أستطيع أن أرى الحجج لإعادة تسمية MaybeUninit إلى MaybeInvalid . ومع ذلك، "باطل" غامض للغاية (غير صالح لماذا؟) الناس، لقد رأيت حيرة من التمييز بلدي بين "صالح" و "آمنة" - يمكن للمرء أن نفترض أن "صالح Vec " غير صالحة لل أي نوع من الاستخدام. "غير مهيأ" على الأقل يؤدي بشكل أساسي إلى الارتباطات الصحيحة لمعظم الناس. ربما يجب علينا إعادة تسمية "الصلاحية الثابتة" إلى "التهيئة الثابتة" أو نحو ذلك؟

بالإضافة إلى ذلك ، فإن مجرد وجود Unsafe<T> يمكن أن يكون مضللاً (من خلال الإشارة بشكل خاطئ إلى أن جميع القيم غير المغلفة بها آمنة) ما لم نعتمد اتفاقية قوية وواسعة النطاق ضد وجود قيم غير آمنة خارج هذا الغلاف. سيكون هذا مشروعًا كبيرًا ، ويتطلب RFC آخر وإجماعًا أوسع من المجتمع. أتوقع أن يكون الأمر مثيرًا للجدل إلى حد ما (أعطى RalfJung بعض الأسباب الوجيهة ضده أعلاه) ، ومع وجود حجج أضعف من جانبه عن MaybeUninit نظرًا لعدم وجود UB - إنه في الأساس سؤال يتعلق بالأسلوب. على هذا النحو ، أنا متشكك فيما إذا كانت مثل هذه الاتفاقية ستكون عالمية في مجتمع Rust حتى إذا تم قبول RFC وتم تحديث المكتبة القياسية والمستندات.

لذا ، لدى IMO أي شخص يريد رؤية هذه الاتفاقية ، لديه سمكة أكبر للقلي من bikeshedding MaybeUninit API ، وأنا أقترح عدم تأخير استقراره أكثر لانتظار حل هذه العملية. إذا قمنا باستقرار تحويلات MaybeUninit<T> -> T ، فلا يزال بإمكان أجيال Rust المستقبلية كتابة MaybeUninit<Unsafe<T>> للإشارة إلى البيانات التي لم يتم تهيئتها أولاً ، ومن ثم ربما لا تزال غير آمنة بعد التهيئة.

تضمين التغريدة

أو ربما يكون assume_init ؟ من المحتمل أن يكون ذلك متسقًا مع المُنشئ ، MaybeUninit::uninit() مقابل MaybeUninit::uninitialized() - و _that_ من المقرر أن يستقر واحد مع الدفعة الأولى ، لذلك يجب أن نجري هذه المكالمة قريبًا.

إذا كان بإمكاننا الحصول على تناسق ثلاثي مع النوع والمنشئ والدالة -> T فسيكون ذلك أفضل. نظرًا لأن النوع لا يحتوي على اللاحقة -ialized ، أعتقد أن ::uninit() و .assume_init() هو على الأرجح السبيل للذهاب.

حسنًا ، من الواضح أنه مطول أكثر من "فقط" MaybeUninit ، أليس كذلك؟

يعتمد ... أعتقد foo.assume_init().assume_safe() (أو foo.init().safe() إذا كان يميل إلى الإيجاز) ليس كل ما لفترة أطول. يمكننا أيضًا تقديم المجموعة كـ foo.assume_init_safe() إذا لزم الأمر. لا يزال للجمع ميزة أنه يوضح الافتراضين.

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

نأمل أن يأتي التعقيد من الاضطرار إلى فهم المفاهيم الأساسية وراء الصلاحية والسلامة. بمجرد القيام بذلك لا أعتقد أن هناك الكثير من التعقيد العقلي الإضافي. أشعر أن المفاهيم الأساسية مهمة للتعبير عن ذلك.

أنا في الواقع أشك بشكل عام في فائدة Unsafe . من منظور المترجم ، سيكون NOP بالكامل ؛ لا يفترض المترجم أبدًا أن بياناتك تفي بثوابت الأمان.

أكيد أوافق على أنه من POV لا فائدة منه. أي فائدة من التمييز هي نوع من واجهة "أنواع الجلسات".

نظرًا لتكلفته الكبيرة ، أعتقد أن Unsafe يحتاج إلى حافز أقوى. لا أرى كيف سيساعد ذلك بالفعل في منع الأخطاء أو تحسين قابلية قراءة الكود.

كان الجانب الذي لفت انتباهي هو قابلية التعلم. أعتقد أن الأخطاء لا بد أن تحدث عندما يعتقد الناس أن .assume_init() يعني "حسنًا ؛ لقد تحققت من ثابت الصلاحية والآن لدي T ". المخطط الحالي MaybeUninit<T> نوعًا ما غير مفيد بهذه الطريقة. لكنني لست متزوجًا من Unsafe<T> و Invalid<T> كأسماء. أعتقد فقط أن الفصل إلى نوعين ، بغض النظر عن أسمائهم ، يمكن أن يكون مفيدًا من الناحية التعليمية. ربما هناك طرق أخرى مثل تعزيز الوثائق التي يمكن أن تعوض عن ذلك في إطار العمل الحالي؟

أستطيع أن أرى الوسيطات لإعادة تسمية MaybeUninit إلى MaybeInvalid . ومع ذلك ، فإن كلمة "غير صالحة" غامضة للغاية (غير صالحة لـ _ ما_؟) ، لقد رأيت أشخاصًا محتارون بسبب تمييزي بين "صالح" و "آمن" - قد يفترض المرء أن "صالحًا لـ Vec " صالح لـ أي نوع من الاستخدام. "غير مهيأ" على الأقل يؤدي بشكل أساسي إلى الارتباطات الصحيحة لمعظم الناس. ربما يجب علينا إعادة تسمية "الصلاحية الثابتة" إلى "التهيئة الثابتة" أو نحو ذلك؟

أنا أوافق بالتأكيد على أن "الصلاحية" و "الأمان" مربكة بسبب الطريقة التي تبدو بها "الصلاحية". لقد كنت متحيزًا إلى "ثوابت الآلة" كبديل لـ "الصلاحية" و "نوع النظام الثابت" من أجل "الأمان".

تضمين التغريدة

لذا ، لدى IMO أي شخص يريد أن يرى هذه الاتفاقية ، لديه سمكة أكبر للقلي من bikeshedding MaybeUninit API ، وأنا أقترح عدم تأخير استقراره أكثر لانتظار حل هذه العملية. إذا قمنا بتثبيت تحويلات MaybeUninit<T> -> T ، فلا يزال بإمكان أجيال Rust المستقبلية كتابة MaybeUninit<Unsafe<T>> للإشارة إلى البيانات التي لم يتم تهيئتها أولاً ، ومن ثم ربما لا تزال غير آمنة بعد التهيئة.

نقاط جيدة ، ولا سيما إعادة. MaybeUninit<Unsafe<T>> ؛ ربما يمكنك أيضًا إضافة نوع مستعار لجعل اسم النوع أقل إسهابًا.

إذا كان بإمكاننا الحصول على اتساق ثلاثي مع النوع والمُنشئ والوظيفة -> T فسيكون ذلك أفضل. نظرًا لأن النوع لا يحتوي على اللاحقة المهيأة ، أعتقد أن :: uninit () و .assume_init () ربما يكون السبيل للذهاب.

متفق عليه. أنا حزين بعض الشيء لفقدان البادئة into ، لكن لا أرى طريقة جيدة للاحتفاظ بها.

فماذا عن read / read_init إذن؟ هل التشابه مع ptr::read كافٍ لتحفيز "من الأفضل أن تتأكد من أن هذا قد تمت تهيئته بالفعل"؟ هل يواجه read_init مشكلة مشابهة لـ into_init ، حيث يبدو أنه يجعله مهيئًا بدلاً من افتراضه؟ هل يجب أن يكون assume_init مثل read الآن؟

نأمل أن يأتي التعقيد من الاضطرار إلى فهم المفاهيم الأساسية وراء الصلاحية والسلامة. بمجرد القيام بذلك لا أعتقد أن هناك الكثير من التعقيد العقلي الإضافي. أشعر أن المفاهيم الأساسية مهمة للتعبير عن ذلك.

هل يمكنك إعطاء مثال على رمز إذا كان هناك شيء في Vec بشكل صحيح باستخدام هذا ليعكس متى يتم انتهاك ثوابت Vec ؟ أعتقد أنه سيكون مطولًا للغاية وغامضًا تمامًا ما يحدث بالفعل.

أعتقد أن إضافة نوع مثل هذا هو الطريقة الخاطئة لنقل المفهوم الأساسي.

أعتقد أن الأخطاء لا بد أن تحدث عندما يعتقد الناس أن .assume_init () تعني "حسنًا ؛ لقد تحققت من صحة ثابتة والآن لدي حرف T جيد".

أجد أنه من غير المرجح أن يكون أي شخص مثل "لقد قمت بتهيئة هذا Vec<i32> خلال كتابته بالكامل 0xFF ، الآن تمت تهيئته ، وهذا يعني أنه يمكنني الدفع إليه". أود أن أرى إشارة على الأقل ، بيانات صلبة أفضل ، أن هذا في الواقع خطأ يرتكبه الناس.
من واقع خبرتي ، يمتلك الناس حدسًا قويًا جدًا وهو أنه عندما يوزعون البيانات على كود غير معروف ، أو يتصلون بعمليات المكتبة على بعض البيانات ، فإن ثوابت المكتبة بحاجة إلى الدعم.

هدأت الأمور قليلا هنا. فماذا عن الخطة التالية:

  • أقوم بإعداد PR لإيقاف MaybeUninit::uninitialized وإعادة تسميته إلى MaybeUninit::uninit .
  • بمجرد أن يصل ذلك (يتطلب تحديث stdsimd ، لذلك هناك بعض الوقت هنا إذا اعتقد الناس أن هذا ليس هو السبيل للذهاب) ، أقوم بإعداد PR لتحقيق الاستقرار MaybeUninit::{new, uninit, zeroed, as_ptr, as_mut_ptr} .

يترك هذا السؤال مفتوحًا حول set / write ، into_init[ialized] / assume_init[ialized] و read[_init[italized]] . حاليًا ، أميل إلى assume_init ، write و read ، لكنني غيرت رأيي حول هذا من قبل. للأسف ، ليس لدي فكرة جيدة عن كيفية التوصل إلى قرار هنا.

  • بمجرد أن هبطت

هل يعني ذلك أنه ستكون هناك فترة لا يوجد فيها سبيل لإنشاء قيمة غير مهيأة بدون إما (أ) تحذير الإهمال أو (ب) استخدام ميزات غير مستقرة؟ هذه ليست ممارسة مستدامة.

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

هل يعني ذلك أنه ستكون هناك فترة لا يوجد فيها سبيل لإنشاء قيمة غير مهيأة بدون إما (أ) تحذير الإهمال أو (ب) استخدام ميزات غير مستقرة؟

أنا حائر. أنا أقترح إهمال طريقة غير مستقرة وتقديم طريقة أخرى غير مستقرة بدلاً من ذلك.

لاحظ أنني كنت أتحدث عن MaybeUninit::uninitialized ، وليس mem::uninitialized .

للأسف ، ليس لدي فكرة جيدة عن كيفية التوصل إلى قرار هنا.

RalfJung فقط قم بذلك (و r؟ me إذا أردت) كما فعلت من قبل مع إعادة تسمية العلاقات العامة الأخرى وإذا اعترض أي شخص يمكننا التعامل مع ذلك في FCP. :)

فقط قم بذلك (و r؟ me إذا أردت) كما فعلت من قبل مع إعادة تسمية العلاقات العامة الأخرى وإذا اعترض أي شخص يمكننا التعامل مع ذلك في FCP. :)

حسنًا ، سأنتظر قليلاً لأن هذه لا يجب أن تكون جزءًا من الاستقرار الأولي.

إهمال طريقة غير مستقرة وتقديم طريقة أخرى غير مستقرة بدلاً من ذلك

آه ، مسكتك. استمر بعد ذلك.

حسنًا ، إجراء عمليات إعادة التسمية في https://github.com/rust-lang/rust/pull/59284 :

غير مهيأ -> غير مهيأ
in_initialized -> افتراض_init
read_initialized -> اقرأ
مجموعة -> الكتابة

أنا أحب الأسماء المقترحة حديثًا. أنا قلقة قليلاً من إساءة استخدام read ، لكن يبدو أن هذا أقل احتمالًا من إساءة استخدام into_initialized ، ويرجع ذلك أساسًا إلى الارتباط بـ ptr::read . بشكل عام ، أعتقد أن التسمية الجديدة مقبولة تمامًا لتحقيق الاستقرار.

أقوم بإعداد العلاقات العامة لتحقيق الاستقرار في MaybeUninit :: {new، uninit، zeroed، as_ptr، as_mut_ptr}.

هل من المحتمل أن يصل هذا إلى 1.35 بيتا (مستحق في حوالي أسبوعين)؟

أنا متضارب قليلاً بشأن دفع هذا الأمر نظرًا لمدى ارتفاعه تمامًا في الهواء https://github.com/rust-lang/rfcs/pull/2582 . : / بدون RFC ، لا يزال التهيئة التدريجية للبنية غير ممكنة ، لكن الناس سيفعلونها على أي حال.
OTOH ، لقد انتظر MaybeUninit طويلاً بما يكفي. وهي ليست مثل رمز التهيئة التدريجية الذي يكتبه الأشخاص حاليًا أفضل مما يكتبونه باستخدام MaybeUninit .

ومع ذلك ، فإن https://github.com/rust-lang/rust/pull/59284 لم يهبط حتى الآن ، لذلك سيتعين علينا الاندفاع للوصول إلى 1.35. TBH أفضل انتظار دورة أخرى حتى يحصل الأشخاص على بعض الوقت على الأقل للعب بأسماء الأساليب الجديدة ومعرفة ما يشعرون به.

هل هناك أي احتمال أن تكون دالات الإنشاء في MaybeInit const ؟

init و new هي const . zeroed ليس كذلك ، نحتاج إلى بعض الإضافات لما يمكن أن تفعله وظائف const قبل أن تصبح const .

أردت تقديم بعض الملاحظات حول MaybeUninit ، يمكن رؤية التغييرات الفعلية في الكود هنا https://github.com/Thomasdezeeuw/mio-st/pull/71. بشكل عام ، كانت تجربتي (المحدودة) مع API إيجابية.

كانت المشكلة الصغيرة الوحيدة التي واجهتها هي أن إرجاع &mut T في MaybeUninit::set يؤدي إلى الاضطرار إلى استخدام let _ = ... (https://github.com/Thomasdezeeuw/mio-st/pull/ 71 / files # diff-1b9651542d08c6eca04e6025b1c6fd53R116) ، وهو أمر محرج بعض الشيء ولكنه ليس مشكلة كبيرة.

يجب علي أيضًا إضافة واجهات برمجة التطبيقات التي أرغب في استخدامها عند العمل مع المصفوفات الموحدة ، غالبًا مع C.

  1. طريقة الانتقال من &mut [MaybeUninit<T>] إلى &mut [T] ستكون جيدة ، يجب على المستخدم التأكد من أن جميع القيم في الشريحة مهيأة بشكل صحيح
  2. ستكون وظيفة مُبدئ المصفوفة العامة أو الماكرو ، مثل uninitialized_array ، إضافة رائعة أيضًا.

أردت تقديم بعض التعليقات حول ربما Uninit

شكرا جزيلا!

عودة & mut T في MaybeUninit :: set تؤدي إلى الاضطرار إلى استخدام let _ = ...

لماذا ذلك؟ يمكنك فقط "التخلص" من قيم الإرجاع ، في الواقع لا تفعل الأمثلة الموجودة في المستندات let _ = ... . (لا يحتوي write / set على مثال حتى الآن ... لكنه في الحقيقة يشبه إلى حد كبير read ، ربما يجب أن يكون مجرد رابط.)

foo.write(bar); يعمل بشكل جيد بدون let .

العمل مع المصفوفات أحادية الاتجاه

نعم ، هذا بالتأكيد مجال اهتمام في المستقبل.

تضمين التغريدة

عودة & mut T في MaybeUninit :: set تؤدي إلى الاضطرار إلى استخدام let _ = ...

لماذا ذلك؟ يمكنك فقط "التخلص" من قيم الإرجاع ، في الواقع لا تفعل الأمثلة الموجودة في المستندات let _ = ... . (لا يحتوي write / set على مثال حتى الآن ... لكنه في الحقيقة يشبه إلى حد كبير read ، ربما يجب أن يكون مجرد رابط.)

لقد قمت بتمكين التحذير لـ unused_results ، لذلك بدون let _ = ... ، سينتج تحذير. لقد نسيت أن هذا ليس هو الافتراضي.

آه ، لم أكن أعرف عن هذا التحذير. مثير للإعجاب.

قد يكون هذا حجة لجعل write لا يقوم بإرجاع مرجع ، وتقديم طريقة منفصلة لذلك إذا كان هناك طلب أكبر.

ستكون وظيفة مُبدئ المصفوفة العامة أو الماكرو ، مثل uninitialized_array ، إضافة رائعة أيضًا.

سيكون هذا فقط [MaybeUninit::uninit(); EVENTS_CAP] . انظر https://github.com/rust-lang/rust/issues/49147.

لقد نسيت أن هذا ليس هو الافتراضي.

قد يكون هذا حجة لجعل write لا يقوم بإرجاع مرجع ، وتقديم طريقة منفصلة لذلك إذا كان هناك طلب أكبر.

يبدو مكانة؟ إذا كان هناك المزيد من الطلب في المستقبل ، فيمكننا إضافة طريقة لا تُرجع مرجعًا.

يبدو مكانة؟

نعم ، هناك الكثير من الطرق التي تحدد قيمة ثم تعيد مرجعًا متغيرًا إليها.

Centril Heh ، لا أعتقد أنني رأيت تعليقك هنا عندما كتبت هذا في مكان آخر: https://github.com/rust-lang/rust/issues/54542#issuecomment -478261027

إزالة الوظائف القديمة المعاد تسميتها المهملة في https://github.com/rust-lang/rust/pull/59912.

بعد ذلك ، أعتقد أن الشيء التالي الذي يجب فعله هو اقتراح الاستقرار ...: تادا:

أنا متضارب قليلاً بشأن دفع هذا الأمر نظرًا لمدى استمرار الصدأ-لانج / rfcs # 2582 في الهواء. : / بدون RFC ، لا يزال التهيئة التدريجية للبنية غير ممكنة ، لكن الناس سيفعلونها على أي حال.
OTOH ، لقد انتظر MaybeUninit طويلاً بما يكفي. وهي ليست مثل رمز التهيئة التدريجية الذي يكتبه الأشخاص حاليًا أفضل مما يكتبونه باستخدام MaybeUninit .

بعد ذلك ، أعتقد أن الشيء التالي الذي يجب فعله هو اقتراح الاستقرار ... 🎉

RalfJung كيف حال التوثيق هنا؟ إذا تمكنا من التخفيف من "الناس سيفعلون ذلك على أي حال" باستخدام بعض المستندات الواضحة التي ستساعدني على النوم بشكل أفضل ... :)

عند قراءة مستندات MaybeUninit ، خاصةً تلك الخاصة بـ assume_init ، ليس من الواضح في قسم "الأمان" أنك إذا كنت تتصل بـ mu.assume_init() ثم تعيد هذه النتيجة في خزنة fn ، يجب عليك أيضًا الحفاظ على ثوابت السلامة أيضًا. قبل الاستقرار ، سيكون من الجيد تحسين تلك المستندات وإعطاء مقتطفات من ثوابت السلامة المتوفرة في المكتبة والتي يجب دعمها أيضًا عند استخدام MaybeUninit .

كيف هي حالة التوثيق هنا؟ إذا تمكنا من التخفيف من "الناس سيفعلون ذلك على أي حال" باستخدام بعض المستندات الواضحة التي ستساعدني على النوم بشكل أفضل ... :)

ربما سأضيف قسمًا حول التهيئة التدريجية للبنى ، مع ذكر أن هذا غير مدعوم حاليًا. الناس الذين يقرؤون هذا سيكونون مثل "WTF ، حقًا؟".

TBH أجد هذا محبطًا إلى حد ما. (أعتقد أنه كان من الممكن جدًا بالنسبة لنا تقديم بعض النصائح لذلك الآن وأنا حزين لأننا لم نتمكن من القيام بذلك.

ليس من الواضح في قسم "الأمان" أنه إذا كنت تتصل بـ mu.assume_init () ثم تعيد النتيجة إلى fn آمن ، فيجب عليك أيضًا الحفاظ على ثوابت السلامة أيضًا. قبل تحقيق الاستقرار ، سيكون من الجيد تحسين تلك المستندات وإعطاء مقتطفات من ثوابت السلامة المتوفرة في المكتبة والتي يجب دعمها أيضًا عند استخدام MaybeUninit.

أنت تقترح بشكل أساسي تحويل هذا إلى مستندات تشرح الفكرة الكاملة لثوابت نوع البيانات وكيف يتم عرضها في Rust. أعتقد أن MaybeUninit هو المكان الخطأ لذلك ؛ من شأنه أن يجعل الأمر يبدو وكأن هذا القلق خاص بـ MaybeUninit الحقيقة ليس كذلك. يجب شرح الأشياء التي تطلبها في مكان أعلى مستوى مثل Nomicon. أخطط لتركيز مستندات MaybeUninit على المشكلة الأساسية من هذا النوع. لا تتردد في توسيعها على الرغم من أنك تعتقد أن ذلك مفيد. :)

أنت تقترح بشكل أساسي تحويل هذا إلى مستندات تشرح الفكرة الكاملة لثوابت نوع البيانات وكيف يتم عرضها في Rust.

هذا قوي بعض الشيء ... أنا أقترح فقط "أوه ، __ بالمناسبة__ ، تذكر أهمية السلامة الثابتة أيضًا" في مكان إستراتيجي ما في وثائق MaybeUninit<T> . أنا لا أقترح أن نضيف رواية. ؛) يمكن أن توجد هذه الرواية في Nomicon ولكن هناك احتمالية أن معظم الأشخاص الذين يستخدمون MaybeUninit<T> سيتعاملون في الغالب مع وثائق المكتبة القياسية.

حسنًا ، حاولت دمج كل ذلك في العلاقات العامة لتحقيق الاستقرار: https://github.com/rust-lang/rust/pull/60445

لقد عثرت للتو على استخدام mem::uninitialized في وثائق مكتبة المعايير ، ولم أكن أعرف حقًا في أي مكان آخر لألاحظ أن المثال الأخير من core::ptr::drop_in_place يحتاج إلى التحديث (نوع من السخرية أيضًا أنه يعرض الشكل الآخر من UB الذي لن يُعاقب عليه إلا من خلال https://github.com/rust-lang/rfcs/pull/2582 ، لذلك سأزيله شخصيًا).

HeroicKatora شكرا! لقد أدرجت الإصلاح الخاص بذلك في https://github.com/rust-lang/rust/pull/60445.

لا يمكننا فعل أي شيء حيال حقل ref-to-unaligned-field حاليًا ، ولست متأكدًا مما إذا كانت إزالة المستند فكرة جيدة أم لا.

ربما تضيف السمة PartialUninit (أو PartialInit ) التي ستهيئ البيانات جزئيًا بناءً على البيانات الوصفية.

مثال: MODULEENTRY32W .
يجب تهيئة الحقل الأول ( dwSize ) حسب حجم الهيكل ( size_of::<MODULEENTRY32W>() ).

pub trait PartialUninit: Sized {
    fn uninit() -> MaybeUninit<Self>;
}

impl<T> PartialUninit for T {
    default fn uninit() -> MaybeUninit<Self> {
        MaybeUninit::uninit()
    }
}

impl PartialUninit for MODULEENTRY32W {
    unsafe fn uninit() -> MaybeUninit<MODULEENTRY32W> {
        let uninit = MaybeUninit { uninit: () };
        uninit.get_mut().dwSize = size_of::<MODULEENTRY32W>();
        uninit
    }
}

كيف تفكر؟

kgv أخشى أنني لا أفهم اقتراحك. ربما يمكن أن يساعد بعض السياق الإضافي الذي يشرح المشكلة التي تحاول حلها؟ وربما مثال أكثر اكتمالا للحل الذي اقترحته؟

@ scottjmaddox ثابت . هل هو أوضح؟

kgv ما هي المشكلة التي يتم حلها (على عكس قيام شخص ما بكتابة وظيفة مساعدة لهذا الغرض)؟ لا أفهم لماذا يجب على libstd القيام بأي شيء هنا.

لاحظ أن التهيئة الجزئية للبنى القائمة على التعيين تعمل فقط للأنواع التي لا تحتاج إلى الإفلات. uninit.get_mut().foo = bar وإلا سينخفض foo ، وهو UB.

RalfJung المشكلة التي أحاول حلها - العمل الموحد مع هياكل FFI ، بعض الحقول التي لا تعتمد على self (فقط Self أو لا تعتمد على أي شيء (ثابت)) ، على سبيل المثال - أحد الحقول بحجم Self .

kgv يجب أن أتفق مع RalfJung هنا على أن حالة الاستخدام هذه يتم التعامل معها بشكل أفضل من خلال وحدة أو صندوق مساعد.

استقرار العلاقات العامة هبطت في الوقت المناسب تماما لبيتا. :) لقد مر حوالي 8 أشهر منذ أن بدأت في البحث في الوضع حول النقابات والذاكرة غير المهيأة ، وأخيراً لدينا شيء (على الأرجح) سيتم شحنه في غضون 6 أسابيع. يا لها من رحلة! شكرا جزيلا لكل من ساعد في ذلك. :د

بالطبع ، نحن بعيدون عن الانتهاء. هناك https://github.com/rust-lang/rfcs/pull/2582 ليتم حلها. لا يزال لدى libstd عدد قليل من الاستخدامات لـ mem::uninitialized (غالبًا في رمز خاص بالمنصة) التي تحتاج إلى نقل. واجهة برمجة التطبيقات المستقرة التي لدينا الآن ضئيلة للغاية: نحتاج إلى معرفة ما يجب فعله باستخدام read و write ، ويجب أن نبتكر واجهات برمجة التطبيقات التي تساعد في العمل مع المصفوفات والمربعات MaybeUninit . ولدينا الكثير من الشرح لنفعله لنقل النظام البيئي بأكمله ببطء بعيدًا عن mem::uninitialized .

لكننا سنصل إلى هناك ، ربما كانت هذه الخطوة الأولى هي الأكثر أهمية. :)

ويجب أن نبتكر واجهات برمجة التطبيقات التي تساعد في العمل مع المصفوفات والمربعات من MaybeUninit .

RalfJung تحقيقا لهذه الغاية ؛ ربما حان الوقت الآن لبدء العمل على https://github.com/rust-lang/rust/issues/49147؟ = ص

أيضًا ، ربما ينبغي علينا تقسيم وإغلاق مشكلة التتبع هذه لصالح المشكلات الأصغر بالنسبة للبتات المتبقية.

إلى تلك النهاية؛ ربما حان الوقت الآن لبدء العمل على # 49147؟ = ص

هل تطوعت للتو؟ ؛) (أخشى ألا يكون لدي وقت لذلك.)

ربما ينبغي علينا تقسيم وإغلاق مشكلة التتبع هذه لصالح أصغرها للبتات المتبقية.

سأترك ذلك لخبراء العملية. لكني أميل إلى الموافقة.

هل تطوعت للتو؟ ؛) (أخشى ألا يكون لدي وقت لذلك.)

ما الذي قمت به ... = D - لدي بالفعل مشروع أعمل عليه لذا من المحتمل أن يستغرق الأمر بعض الوقت. ربما شخص آخر مهتم؟ (إذا كان الأمر كذلك ، فانتقل إلى مشكلة التتبع)

سأترك ذلك لخبراء العملية. لكني أميل إلى الموافقة.

سيكون هذا أنا ... ؛) سأحاول تقسيمه وإغلاقه قريبًا.

RalfJung حول let x: bool = mem::uninitialized(); هو UB ، والسؤال هو لماذا تعتبر الأوليات غير الصالحة كذلك؟ كما أفهم ، يجب عليك قراءة قيمة لملاحظة أنه من غير الصحيح تشغيل UB. لكن إذا لم تقرأها فماذا؟

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

هل هناك حالات حقيقية في المترجم عندما يعتمد على هذه الافتراضات؟

على سبيل المثال ، نقوم بتعليق دوال مثل foo(x: bool) لإخبار LLVM أن x قيمة منطقية صالحة. هذا يجعل من UB تمرير bool ليس true أو false حتى لو لم تكن الوظيفة في الأصل تبدو x . هذا مفيد لأنه في بعض الأحيان يريد المترجم تقديم استخدامات للمتغيرات غير المستخدمة سابقًا (على وجه الخصوص ، يحدث هذا عند نقل العبارات خارج الحلقات دون إثبات أن الحلقة مأخوذة مرة واحدة على الأقل).

AFAIK قمنا أيضًا بتعيين (أو نريد تعيين) بعض هذه التعليقات التوضيحية داخل دالة ، وليس فقط عند حدود الوظيفة. وقد نجد المزيد من الأماكن في المستقبل حيث يمكن أن تكون هذه المعلومات مفيدة. قد نكون قادرين على تغطية ذلك بتعريف ذكي لـ "استخدام متغير" (مصطلح استخدمته دون تعريفه ، وليس من السهل تحديده بالفعل) ، لكنني أعتقد أنه عندما يتعلق الأمر بـ UB في كود غير آمن ، من المهم أن يكون لدينا قواعد بسيطة حيثما أمكن ذلك.

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

على سبيل المثال ، نقوم بتعليق وظائف مثل foo (x: bool) لإخبار LLVM أن x هو منطقي صالح. هذا يجعل من UB تمرير منطقي غير صحيح أو خطأ حتى لو لم تنظر الوظيفة في الأصل إلى x. هذا مفيد لأنه في بعض الأحيان يريد المترجم تقديم استخدامات للمتغيرات غير المستخدمة سابقًا (على وجه الخصوص ، يحدث هذا عند نقل العبارات خارج الحلقات دون إثبات أن الحلقة مأخوذة مرة واحدة على الأقل).

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

باختصار ، سؤالي هو ما إذا كان هذا الرمز هو UB (وفقًا للمستندات - هو) ، وإذا كان الأمر كذلك ، فما الذي يمكن أن ينكسر بالضبط إذا كتبت ذلك؟

let _: bool = unsafe { mem::unitialized };

سؤال آخر حول الموضوع نفسه: نعلم أن لدينا بناء جملة box يسمح لك بتخصيص الذاكرة مباشرة على الكومة ، وهو يعمل دائمًا على عكس Box::new() الذي أحيانًا يتراكم الذاكرة. لذا إذا قمت بعمل box MaybeUninit::new() ثم قمت بملئها ، فكيف يمكنني تحويل Box<MaybeUninit<T>> إلى Box<T> ؟ هل يجب أن أكتب أي ينقل أم ماذا؟ ربما فاتني فقط هذه النقطة في الوثائق.

Pzixel لقد ناقشنا بالفعل التفاعلات بين Box و MaybeUninit بالفعل في هذا الموضوع : ابتسم:

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

نعم ، أتذكر تلك المناقشة ، لكنني لا أتذكر أي واجهة برمجة تطبيقات محددة.

باختصار ، أريد أن أحصل على شيء مثل

fn into_inner<A,T>(value: A<MaybeUninit<T>>) -> A<T> { unsafe { std::mem::transmute() } }

لكنني لا أعتقد أن هناك مثل API ، ويبدو أنه لا يمكن تنفيذها بدون دعم المترجم في هذه المرحلة من تطور اللغة.


فكرت في الأمر أكثر من ذلك بقليل ويبدو أنه يجب أن يعمل على أي مستوى من التعشيش. لذا Vec<Result<Option<MaybeUninit<u8>>>> ينبغي أن يكون into_inner طريقة التي ترجع Vec<Result<Option<u8>>>

كنت أفترض أن get_ref و get_mut سيستقران في نفس الوقت (كل الميزات تشير إلى هذه المشكلة). هل هناك سبب لعدم؟ إنها لطيفة وهي المؤشر الوحيد على أن تنفيذ الإجراء الذي يؤدونه مسموح به (والذي من الواضح أنه يجب أن يكون صحيحًا).

قد يعتبر هذا بمثابة استخدام.

لذلك لا يستخدم let x: bool = mem::uninitialized() bool (على الرغم من أنه تم تعيينه إلى x !) ، ولكن

fn id(x: bool) -> bool { x }
let x: bool = id(mem::uninitialized());

هل تستخدمه؟ ماذا عن

fn uninit() -> bool { mem::uninitialized() }
let x: bool = uninit();

هل العودة هنا استخدام؟

هذا سريعًا جدًا يصبح دقيقًا جدًا. لذا فإن الإجابة التي أعتقد أننا يجب أن نقدمها هي أن كل مهمة (حقًا كل نسخة ، كما في ، كل مهمة بعد التخفيض إلى MIR) هي استخدام ، وهذا يتضمن المهمة في let x: bool = mem::uninitialized() .


كنت أفترض أن get_ref و get_mut سيستقران في نفس الوقت (كل الميزات تشير إلى هذه المشكلة). هل هناك سبب لعدم؟ إنها لطيفة وهي المؤشر الوحيد على أن تنفيذ الإجراء الذي يؤدونه مسموح به (والذي من الواضح أنه يجب أن يكون imo صحيحًا).

تم حظر هذا عند حل https://github.com/rust-lang/unsafe-code-guidelines/issues/77 : هل من الآمن أن يكون لديك &mut bool يشير إلى ذاكرة غير مهيأة؟ أعتقد أن الإجابة يجب أن تكون "نعم" ، لكن الناس يختلفون.

تم حظر هذا عند حل إرشادات rust-lang / غير الآمنة كود # 77

لا أعتقد أن الحظر يجب أن يحدث. يمكنك تثبيته والقول "إنه UB لاستخدام هذا إذا كانت الذاكرة غير مهيأة" ثم بعد ذلك تخفيف المتطلبات إذا قررنا أنه جيد. إنها طريقة جيدة للتهيئة اللاحقة.

ثم بعد ذلك تخفيف المتطلبات

مما يعني أنه إذا قمت بالتشفير وفقًا لوثائق الإصدار المستقبلي لكن شخصًا ما قام بتجميع الكود الخاص بي باستخدام الإصدار القديم (متوافق مع API!) من المترجم ، فهناك الآن UB؟

تضمين التغريدة

لا أعتقد أن الحظر يجب أن يحدث. يمكنك تثبيته والقول "إنه UB لاستخدام هذا إذا كانت الذاكرة غير مهيأة" ثم بعد ذلك تخفيف المتطلبات إذا قررنا أنه جيد. إنها طريقة جيدة للتهيئة اللاحقة.

هذا يبدو للغاية بالنسبة لي. لماذا لا تكتب فقط &mut *foo.as_mut_ptr() ؟ بمجرد تهيئة كل شيء ، لماذا لا يعمل ذلك؟ أنا الآن أتساءل عما تقوله

الإشارة الوحيدة إلى السماح بتنفيذ الإجراء الذي يقومون به

فلماذا لا يكون؟ إذا قمنا بإدراج كل ما يمكنك القيام به بشكل شامل بمجرد تهيئة القيمة ، فستكون هذه قائمة طويلة. ^^

تضمين التغريدة

مما يعني أنه إذا قمت بالتشفير وفقًا لوثائق الإصدار المستقبلي لكن شخصًا ما قام بتجميع الكود الخاص بي باستخدام الإصدار القديم (متوافق مع API!) من المترجم ، فهناك الآن UB؟

هذا صحيح اليوم إذا كان الناس يفعلون &mut *foo.as_mut_ptr() . لا أرى أي طريقة لتجنبه.

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

صحيح ، كنت أفترض أن العملية كانت كذلك

  • قم بتثبيته بمتطلبات صارمة ولكن لا معنى لها للتنفيذ الآن
  • الشروع في العمل على نموذج الذاكرة وماذا لديك
  • بمجرد الانتهاء من النموذج

    • إذا كان يجب أن يكون UB ، رائعًا ، اترك المستندات كما هي ، أضف تحسينات إذا كانت مفيدة

    • إذا لم تكن بحاجة إلى أن تكون UB ، فاحصل عليها ، وقم بإسقاطها من المستندات واتصل بها يوميًا

تضمين التغريدة

هل العودة هنا استخدام؟

نعم ، إعادة قيمة أو تمريرها إلى أي مكان هو استخدام.

هذا سريعًا جدًا يصبح دقيقًا جدًا. لذا فإن الإجابة التي أعتقد أننا يجب أن نقدمها هي أن كل مهمة (حقًا كل نسخة ، كما في ، كل مهمة بعد التخفيض إلى MIR) هي استخدام ، وهذا يتضمن المهمة في let x: bool = mem :: uninitialized ().

تبدو صالحة.

على أي حال ، هذا عن التعشيش ربماUninit التعشيش؟ هل يمكن تحويلها بأمان دون مطالبة المستخدم بكتابة التحويل لكل نوع من أنواع الغلاف؟

Pzixel لست متأكدًا مما إذا كنت أفهم سؤالك ، لكنني أعتقد أنه تتم مناقشته على https://github.com/rust-lang/rust/issues/61011.

لقد رأيت أن طريقة MaybeUninit::write() التي لا تزال غير مستقرة ليست unsafe على الرغم من أنها يمكن أن تتخطى انخفاض الاتصال على T ، والذي كنت أفترض أنه غير آمن. هل هناك سابقة لهذا اعتبارها آمنة؟

https://doc.rust-lang.org/nomicon/leaking.html#leaking
https://doc.rust-lang.org/nightly/std/mem/fn.forget.html

لم يتم تمييز forget كـ unsafe ، لأن ضمانات أمان Rust لا تتضمن ضمانًا بأن المدمرات ستعمل دائمًا.

ومع ذلك ، انظر أيضًا https://github.com/rust-lang-nursery/nomicon/issues/135

هل يمكننا إضافة طريقة MaybeUninit<T> -> NonNull<T> إلى MaybeUninit ؟ AFAICT لا يكون المؤشر الذي تم إرجاعه بواسطة MaybeUninit::as_mut_ptr() -> *mut T فارغًا أبدًا. سيؤدي ذلك إلى تقليل الاضطرار إلى التفاعل مع واجهات برمجة التطبيقات التي تستخدم NonNull<T> ، من:

let mut x = MaybeUninit<T>::uninit();
foo(unsafe { NonNull::new_unchecked(x.as_mut_ptr() });

إلى:

let mut x = MaybeUninit<T>::uninit();
foo(x.ptr());

المؤشر الذي تم إرجاعه بواسطة MaybeUninit :: as_mut_ptr () -> * mut T ليس فارغًا أبدًا.

هذا صحيح.

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

ومع ذلك ، فإن إضافة طريقة تُرجع NonNull تبدو جيدة. ماذا يجب أن يسمى بالرغم من ذلك؟ هل هناك أسبقية؟

هناك سابقة لـ https://github.com/rust-lang/rust/issues/47336 لكن الاسم ليس جيدًا ولست متأكدًا من أننا سنقوم بتثبيت هذه الطريقة.

هل حدث تشغيل الحفرة المذكورة في https://github.com/rust-lang/rust/pull/60445#issuecomment -488818677؟

فكرة 3 أشهر من الوقت المتاح التي يذكرها centril لا تتحقق للأشخاص الذين يريدون أن يكونوا

هل يمكن تأجيل الإهمال إلى 1.40.0؟

لا يتم دائمًا عزل تحذيرات الإيقاف في الصندوق المسؤول عنها. على سبيل المثال ، عندما يعرض أحد الصناديق ماكروًا يستخدم std::mem::uninitialized داخليًا ، لا تزال الاستخدامات بواسطة صناديق الجهات الخارجية تستدعي تحذير الإيقاف. لقد لاحظت هذا اليوم عندما جمعت أحد مشاريعي مع المترجم الليلي. على الرغم من أن الكود لا يحتوي على إشارة واحدة لـ uninitialized ، فقد تلقيت تحذير الإيقاف لأنه استدعى ماكرو glium implement_vertex .

تشغيل cargo +nightly test على glium master يعطيني أكثر من 1400 سطر من الإنتاج ، تتكون في الغالب من تحذيرات الإهمال للوظيفة uninitialized (أحسب التحذير 200 مرة ، ولكن من المحتمل أن يكون الرقم المحدد لهذا rg "uninitialized" | wc -l الناتج 561).

ما هي الشواغل المتبقية التي تحول دون استقرار بقية الأساليب؟ القيام بكل شيء من خلال *foo.as_mut_ptr() يصبح مملاً للغاية ، وأحيانًا (مقابل write ) يتطلب المزيد من القطع unsafe أكثر من اللازم.

SimonSapin لمحاكاة write ، يمكنك استبدال MaybeUninit بالكامل دون استخدام غير آمن باستخدام *val = MaybeUninit::new(new_val) حيث val: &mut MaybeUninit<T> و new_val: T أو يمكنك استخدام std::mem::replace إذا كنت تريد القيمة القديمة.

@ est31 هذه نقاط جيدة. سأكون على ما يرام في صد الإهمال من خلال إصدار أو اثنين.

أي اعتراضات؟

لقد قلنا بالفعل في منشور مدونة إصدار 1.36.0:

ربما Uninitهو البديل الأكثر أمانًا ، بدءًا من Rust 1.38 ، سيتم إهمال الوظيفة mem :: uninitialized.

على هذا النحو ، أعتقد أننا يجب أن نتجنب التقليب المرن على هذا واحد لأن ذلك لا يرسل رسالة جيدة وهو محير. علاوة على ذلك ، يجب أن يكون تاريخ الإيقاف معروفًا على نطاق واسع نظرًا لأنه تم ذكره في منشور المدونة.

ربما يكون الوقت قد فات للعودة إلى إهمال uninitialized . ولكن ربما يمكننا اتخاذ قرار بشأن سياسة لإصدار تحذيرات الإهمال فقط في Nightly بعد أن يكون البديل على القناة الثابتة لبعض الوقت؟

على سبيل المثال ، تعرض Firefox للخطر بشأن طلب إصدار Rust جديد بعد أسبوعين من صدوره .

لقد قلنا بالفعل في منشور مدونة إصدار 1.36.0:

لا أوافق على أن ذكر تاريخ في منشور مدونة هو درجة مكسوّة بالحديد. إنه في مستودع ويمكننا إرسال تعديل.

على هذا النحو ، أعتقد أننا يجب أن نتجنب التقليب المرن على هذا واحد لأن ذلك لا يرسل رسالة جيدة وهو محير.

"flip-floppery" أمر سيء ، لكن تغيير أذهاننا بناءً على البيانات والتغذية المرتدة ليس كذلك.

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

"flopp-floppery" أمر سيء ، لكن تغيير أذهاننا بناءً على البيانات والتغذية المرتدة ليس كذلك.

موافق تماما. لا أرى رسالة سيئة يتم إرسالها بالقول "مرحبًا ، كان جدول الإيقاف لدينا شديد العدوانية ، لقد أعدنا الأمور إلى الوراء من خلال إصدار". بل على العكس تمامًا.
في الواقع ، ذكرت IIRC I أثناء الهبوط PR الاستقرار أن السابقة هي إيقاف 3 إصدارات في المستقبل وليس 2 ، ولكن لسبب ما ذهبنا مع 2. ثلاثة إصدارات تعني إصدارًا واحدًا كاملًا بين مستقر - يحصل - أطلق - مع - -إعلان الإهمال وإيقافه ليلاً ، يبدو أن هذا وقت مناسب للأشخاص الذين يتتبعون كل ليلة. 6 أسابيع هي دهر ، أليس كذلك؟ ؛)

لذلك أخطط لتقديم PR غدًا يغير إصدار الإهمال إلى 1.39.0. يمكنني أيضًا إرسال بيان عام لتحديث منشور المدونة هذا إذا اعتقد الناس أنه مهم.

لذلك أخطط لتقديم PR غدًا يغير إصدار الإهمال إلى 1.39.0. يمكنني أيضًا إرسال بيان عام لتحديث منشور المدونة هذا إذا اعتقد الناس أنه مهم.

سأوافق على 1.39 ولكن في موعد لا يتجاوز ذلك. ستحتاج أيضًا إلى تحديث ملاحظات الإصدار بالإضافة إلى منشور المدونة.

تم إرسال العلاقات العامة لجدول الإيقاف المتغير: https://github.com/rust-lang/rust/pull/62599.

تضمين التغريدة

ما هي الشواغل المتبقية التي تحول دون استقرار بقية الأساليب؟ القيام بكل شيء من خلال * foo.as_mut_ptr () يصبح مملاً للغاية ، وأحيانًا (للكتابة) يتضمن كتلًا غير آمنة أكثر من اللازم.

بالنسبة إلى as_ref / as_mut ، أردت بصدق الانتظار حتى نعرف ما إذا كان يجب أن تشير المراجع إلى البيانات التي تمت تهيئتها. خلاف ذلك ، فإن التوثيق لهذه الأساليب هو مجرد أولية.

بالنسبة إلى read / write ، فأنا على ما يرام بتثبيتهم إذا اتفق الجميع على أن الأسماء والتوقيعات منطقية. أعتقد أنه يجب تنسيق هذا مع ManuallyDrop::take/read ، وربما يجب أيضًا أن يكون هناك ManuallyDrop::write ؟

أردت بصدق الانتظار حتى نعرف ما إذا كانت المراجع يجب أن تشير إلى البيانات التي تمت تهيئتها.

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

في غضون ذلك ، لا يمنع عدم استقرار as_mut المستخدمين من كتابة &mut *manually_drop.as_mut_ptr() عندما يحتاجون إلى إنجاز شيء ما.

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

شهور وربما سنوات.

في غضون ذلك ، لا يمنع كون عدم الاستقرار as_mut المستخدمين من كتابة & mut * يدوياً_drop.as_mut_ptr () عندما يحتاجون إلى إنجاز شيء ما.

نعم اعرف. يكمن الأمل في حث الأشخاص على تأخير الجزء &mut قدر الإمكان ، والعمل باستخدام المؤشرات الأولية. بالطبع بدون https://github.com/rust-lang/rfcs/pull/2582 هذا صعب غالبًا.

يبدو أن التوثيق الخاص بـ ربما Uninit هو مكان رئيسي لمناقشة أن هذا غموض في دلالات اللغة وأنه يجب على المستخدمين افتراض أنها ليست على ما يرام.

صحيح ، سيكون هذا هو الخيار الآخر.

حتى مع الافتراض المتحفظ ، يكون as_mut صالحًا بعد تهيئة القيمة بالكامل.

طريقة واحدة لتكون متحفظًا مع المصفوفات هي استخدام MaybeUninit<[MaybeUninit<Foo>; N]> . تسمح الأغلفة الخارجية بإنشاء المصفوفة باستدعاء واحد uninit() . (أعتقد أن القيمة الحرفية [expr; N] تتطلب Copy ؟) تجعل الأغلفة الداخلية الأمر آمنًا حتى في الافتراض المتحفظ لاستخدام راحة slice::IterMut لاجتياز المصفوفة ، و ثم قم بتهيئة قيم Foo واحدة تلو الأخرى.

RalfJung ربما يكون uninit_array! اسمًا أفضل.

Stargateur بالتأكيد ، هذا بالتأكيد لن يستقر مع اسمه الحالي. نأمل ألا تستقر على الإطلاق إذا حدث قريبًا https://github.com/rust-lang/rust/issues/49147 (TM).

RalfJung Ugh ، هذا خطأي ، لقد كنت أحظر العلاقات العامة بدون سبب وجيه: https://github.com/rust-lang/rust/pull/61749#issuecomment -512867703

eddyb هذا يعمل من أجل libcore ، yay! لكن بطريقة ما عندما أحاول استخدام الميزة في liballoc ، لا يتم تجميعها على الرغم من أنني قمت بتعيين العلم. انظر https://github.com/rust-lang/rust/commit/4c2c7e0cc9b2b589fe2bab44173acc2170b20c09.

Building stage1 std artifacts (x86_64-unknown-linux-gnu -> x86_64-unknown-linux-gnu)
   Compiling alloc v0.0.0 (/home/r/src/rust/rustc.2/src/liballoc)
error[E0277]: the trait bound `core::mem::MaybeUninit<K>: core::marker::Copy` is not satisfied
   --> <::core::macros::uninit_array macros>:1:32
    |
1   |   ($ t : ty ; $ size : expr) => ([MaybeUninit :: < $ t > :: uninit () ; $ size])
    |   -                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `core::marker::Copy` is not implemented for `core::mem::MaybeUninit<K>`
    |  _|
    | |
2   | | ;
    | |_- in this expansion of `uninit_array!`
    | 
   ::: src/liballoc/collections/btree/node.rs:109:19
    |
109 |               keys: uninit_array![_; CAPACITY],
    |                     -------------------------- in this macro invocation
    |
    = help: consider adding a `where core::mem::MaybeUninit<K>: core::marker::Copy` bound
    = note: the `Copy` trait is required because the repeated element will be copied

error[E0277]: the trait bound `core::mem::MaybeUninit<V>: core::marker::Copy` is not satisfied
   --> <::core::macros::uninit_array macros>:1:32
    |
1   |   ($ t : ty ; $ size : expr) => ([MaybeUninit :: < $ t > :: uninit () ; $ size])
    |   -                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `core::marker::Copy` is not implemented for `core::mem::MaybeUninit<V>`
    |  _|
    | |
2   | | ;
    | |_- in this expansion of `uninit_array!`
    | 
   ::: src/liballoc/collections/btree/node.rs:110:19
    |
110 |               vals: uninit_array![_; CAPACITY],
    |                     -------------------------- in this macro invocation
    |
    = help: consider adding a `where core::mem::MaybeUninit<V>: core::marker::Copy` bound
    = note: the `Copy` trait is required because the repeated element will be copied

error[E0277]: the trait bound `core::mem::MaybeUninit<collections::btree::node::BoxedNode<K, V>>: core::marker::Copy` is not satisfied
   --> <::core::macros::uninit_array macros>:1:32
    |
1   |   ($ t : ty ; $ size : expr) => ([MaybeUninit :: < $ t > :: uninit () ; $ size])
    |   -                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `core::marker::Copy` is not implemented for `core::mem::MaybeUninit<collections::btree::node::BoxedNode<K, V>>`
    |  _|
    | |
2   | | ;
    | |_- in this expansion of `uninit_array!`
    | 
   ::: src/liballoc/collections/btree/node.rs:162:20
    |
162 |               edges: uninit_array![_; 2*B],
    |                      --------------------- in this macro invocation
    |
    = help: the following implementations were found:
              <core::mem::MaybeUninit<T> as core::marker::Copy>
    = note: the `Copy` trait is required because the repeated element will be copied

error: aborting due to 3 previous errors

تم حل اللغز: استخدامات تكرار التعبير في libcore كانت في الواقع للأنواع التي يتم نسخها.

والسبب في عدم نجاحه في liballoc هو أن MaybeUninit::uninit غير قابل للترويج.

RalfJung ربما تفتح

eddyb لقد صنعت هذا الجزء من https://github.com/rust-lang/rust/pull/62799.

بخصوص maybe_uninit_ref

بالنسبة لـ as_ref / as_mut ، أردت بصدق الانتظار حتى نعرف ما إذا كان يجب أن تشير المراجع إلى البيانات التي تمت تهيئتها. خلاف ذلك ، فإن التوثيق لهذه الأساليب هو مجرد أولية.

غير مستقر get_ref / get_mut بالتأكيد مستحسن بسبب ذلك ؛ ومع ذلك ، هناك حالات يمكن فيها استخدام get_ref / get_mut عند بدء MaybeUninit : للحصول على مقبض آمن للبيانات (التي تمت تهيئتها الآن) مع تجنب أي memcpy ( بدلاً من assume_init ، والذي قد يؤدي إلى memcpy ).

  • قد يبدو هذا موقفًا محددًا بشكل خاص ، ولكن السبب الرئيسي (رغبة) الناس في استخدام البيانات غير المهيأة هو تحديدًا لهذا النوع من المدخرات الرخيصة.

لهذا السبب ، أتخيل أن assume_init_by_ref / assume_init_by_mut قد يكون أمرًا رائعًا (نظرًا لأن into_inner تم تسميته assume_init ، يبدو أنني معقولة أن ref / ref mut تحصل أيضًا على اسم خاص يعكس هذا).

هناك خياران / ثلاثة خيارات لهذا ، تتعلق بالتفاعل Drop :

  1. نفس واجهة برمجة التطبيقات مثل get_ref و get_mut ، والتي يمكن أن تؤدي إلى تسرب في الذاكرة عند سقوط الغراء ؛

    • (متغير): نفس واجهة برمجة التطبيقات مثل get_ref / get_mut ، لكن مع Copy ملزمة ؛
  2. واجهة برمجة تطبيقات نمط الإغلاق ، لضمان السقوط:

impl<T> MaybeUninit<T> {
    /// # Safety
    ///
    ///   - the contents must have been initialised
    unsafe
    fn assume_init_with_mut<R, F> (mut self: MaybeUninit<T>, f: F) -> R
    where
        F : FnOnce(&mut T) -> R,
    {
        if mem::needs_drop::<T>().not() {
            return f(unsafe { self.get_mut() });
        }
        let mut this = ::scopeguard::guard(self, |mut this| {
            ptr::drop_in_place(this.as_mut_ptr());
        });
        f(unsafe { MaybeUninit::<T>::get_mut(&mut *this) })
    }
}

(حيث يمكن بسهولة إعادة تطبيق منطق scopeguard ، فلا داعي للاعتماد عليه)


يمكن استقرارها بشكل أسرع من get_ref / get_mut ، نظرًا للمتطلبات الصريحة assume_init .

عيوب

إذا تم اختيار متغير للخيار .1 ، و get_ref / get_mut أصبح قابلاً للاستخدام بدون الوضع assume_init ، عندها ستصبح واجهة برمجة التطبيقات هذه أقل شأناً تقريبًا (أقول تقريبًا لأنه مع واجهة برمجة التطبيقات المقترحة ، ستكون القراءة من المرجع على ما يرام ، والتي لا يمكن أن تكون أبدًا في حالة get_ref و get_mut )

على غرار ما كتبته danielhenrymantilla حول get_{ref,mut} ، بدأت أعتقد أنه من المحتمل إعادة تسمية read إلى read_init أو read_assume_init أو نحو ذلك ، وهو ما يشير إلى أن هذا قد يتم فقط بعد اكتمال التهيئة.

RalfJung لدي سؤال حول هذا:

fn foo<T>() -> T {
    let newt = unsafe { MaybeUninit::<T>::zeroed().assume_init() };
    newt
}

على سبيل المثال ، نسمي foo<NonZeroU32> . هل يؤدي هذا إلى تشغيل UB عندما نعلن عن وظيفة foo (لأنه يجب أن تكون صالحة لجميع T s أو عندما نقوم بإنشائها بنوع يؤدي إلى تشغيل UB؟ عذرًا إذا كان المكان خاطئًا لـ طرح سؤال.

يمكن أن يتسبب رمز Pzixel في ظهور UB فقط عند تشغيله.

لذا ، foo::<i32>() جيد. لكن foo::<NonZeroU32>() هو UB.

تسمى خاصية كونها صالحة لجميع الطرق الممكنة للاتصال بـ "السلاسة" ، انظر أيضًا المرجع . العقد العام في Rust هو أن سطح API الآمن للمكتبة يجب أن يكون سليمًا. هذا حتى لا يضطر مستخدمو المكتبة إلى القلق بشأن UB. تستند قصة أمان Rust بأكملها إلى مكتبات بها واجهات برمجة تطبيقات صوتية.

RalfJung شكرا.

لذلك إذا فهمتك بشكل صحيح ، فهذه الوظيفة غير صحيحة (وبالتالي فهي غير صالحة) ، ولكن إذا حددناها على أنها unsafe فسيصبح هذا الجسم صالحًا وسليمًا

Pzixel إذا قمت

نعم ، يجب وضع علامة على الوظيفة unsafe لأن بعض المدخلات يمكن أن تؤدي إلى تشغيل UB. ولكن حتى لو قمت بذلك ، فإن هذه المدخلات لا تزال تعمل على تشغيل UB ، لذلك لا يزال يتعين عدم استدعاء الوظيفة بهذه الطريقة. ليس من المقبول إطلاقًا تشغيل UB ، ولا حتى في التعليمات البرمجية غير الآمنة.

نعم بالطبع أفهم ذلك. أردت فقط أن أستنتج أنه يجب تمييز الوظيفة الجزئية على أنها unsafe . من المنطقي بالنسبة لي ولكني لم أفكر في ذلك قبل أن ترد.

نظرًا لأن المناقشة حول مشكلة التتبع هذه طويلة جدًا الآن ، هل يمكننا تقسيمها إلى عدد قليل من مشكلات التتبع الأخرى لكل ميزة من MaybeUninit والتي لا تزال غير مستقرة؟

  • maybe_uninit_extra
  • maybe_uninit_ref
  • maybe_uninit_slice

يبدو معقولا. يوجد أيضًا https://github.com/rust-lang/rust/issues/63291.

إغلاق هذا لصالح إصدار meta الذي يتتبع MaybeUninit<T> بشكل عام: # 63566

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات