Rust: Проблема отслеживания для RFC 1892: «Устарело неинициализированное использование в пользу нового типа MaybeUninit»

Созданный на 19 авг. 2018  ·  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] 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 ? (сделанный)
  • Похоже, мы не хотим осуждать mem::zeroed .
B-RFC-approved C-tracking-issue E-mentor T-lang T-libs

Самый полезный комментарий

mem::zeroed() полезен в определенных случаях FFI, когда вы должны обнулить значение с помощью memset(&x, 0, sizeof(x)) перед вызовом функции C. Я думаю, что это достаточная причина не использовать его.

Все 382 Комментарий

cc @RalfJung

[] Реализуйте RFC

Я могу помочь реализовать RFC.

Отлично, могу помочь с обзором :)

Я хотел бы пояснить эту часть RFC:

Сделайте вызов неинициализированным для пустого типа, чтобы вызвать панику во время выполнения, которая также выводит сообщение об устаревании.

Стоит ли паниковать только mem::uninitialized::<!>() ? Или это также должно охватывать структуры (и, возможно, перечисления?), Которые содержат пустой тип (например, (!, u8) )?

Насколько я знаю, мы создаем действительно опасный код только для ! . Большинство других применений 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 operand.rs # 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 будет бесполезен.

Это дает мне представление ... возможно, нам следует lint (group: "правильность") для такого рода кода? Копия @ oli-obk

Сейчас я исправляю (несколько) предупреждений об устаревании

Мы должны отправлять Nightly с этими предупреждениями только после того, как рекомендуемая замена будет доступна хотя бы на Stable. См. Аналогичное обсуждение на https://github.com/rust-lang/rust/pull/52994#issuecomment -411413493

@RalfJung

Нам может потребоваться исключение для «типов, где все битовые шаблоны в порядке» (по сути, целочисленные типы)

Вы уже участвовали в обсуждении этого вопроса, но я опубликую здесь для более широкого распространения: это уже то, что у нас уже есть много вариантов использования в Fuchsia, и у нас есть особенность для этого ( FromBytes ) и производный макрос для этих типов. Существовал также внутренний предварительный RFC для добавления их в стандартную библиотеку (cc @gnzlbg @joshlf).

Я бы не стал делать такое исключение, потому что undef не является нормальным битовым шаблоном.

Да, в этом аспекте mem::zeroed() значительно отличается от mem::uninitialized() .

@cramertj

Вы уже участвовали в обсуждении этого раньше, но я опубликую здесь, чтобы распространить более широко: это уже то, что у нас уже есть много существующих вариантов использования в Fuchsia, и у нас есть черта для этого (FromBytes) и макрос derive. для этих типов. Существовал также внутренний предварительный RFC для добавления их в стандартную библиотеку (cc @gnzlbg @joshlf).

Эти обсуждения касались способов обеспечения безопасных memcpy s для разных типов, но я думаю, что это в значительной степени ортогонально тому, инициализируется ли копируемая память или нет - если вы поместите неинициализированную память, вы получите неинициализированную память.

Консенсус также заключался в том, что для любого обсуждаемого подхода было бы необоснованно разрешать чтение байтов заполнения, которые являются формой неинициализированной памяти, в безопасном Rust. То есть, если вы поместите инициализированную память, вы не сможете извлечь неинициализированную память.

IIRC, никто там не предлагал и не обсуждал какой-либо подход, в котором вы могли бы поместить неинициализированную память и получить инициализированную память, поэтому я не понимаю, какое отношение эти обсуждения имеют к этому. Для меня они полностью ортогональны.

Чтобы еще больше приблизить точку зрения, LLVM определяет неинициализированные данные как Poison, что отличается от «некоторого произвольного, но действительного битового шаблона». Ветвление на основе значения Poison или его использование для вычисления адреса, который затем разыменовывается, - это UB. Так что, к сожалению, «типы, где все битовые шаблоны в порядке» по-прежнему небезопасно создавать, потому что их использование без отдельной инициализации будет UB.

Хорошо, извините, я должен был пояснить, что имел в виду. Я пытался сказать, что «типы, в которых все битовые шаблоны в порядке» - это уже то, что мы заинтересованы в определении по другим причинам. Как сказал @RalfJung выше,

Я бы не стал делать такое исключение, потому что undef не является нормальным битовым шаблоном.

Слава богу, есть люди, которые умеют читать, потому что, видимо, я не умею ...

Итак, я хотел сказать: у нас определенно есть типы, в которых все инициализированные битовые шаблоны в порядке - все типы i* и u* , необработанные указатели, я думаю, f* а затем кортежи / структуры, состоящие только из таких типов.

Остается открытым вопрос, при каких обстоятельствах какие из этих типов могут быть неинициализированы , т. Е. Отравлены. Я предпочитаю ответ «никогда».

Консенсус также заключался в том, что для любого обсуждаемого подхода было бы необоснованно разрешать чтение байтов заполнения, которые являются формой неинициализированной памяти, в безопасном Rust. То есть, если вы поместите инициализированную память, вы не сможете извлечь неинициализированную память.

Чтение байтов заполнения как MaybeUninit<u8> должно быть нормальным.

Консенсус также заключался в том, что для любого обсуждаемого подхода было бы необоснованно разрешать чтение байтов заполнения, которые являются формой неинициализированной памяти, в безопасном Rust. То есть, если вы поместите инициализированную память, вы не сможете извлечь неинициализированную память.

Чтение байтов заполнения как MaybeUninitвсе должно быть в порядке.

Обсуждение вкратце касалось предоставления признака 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> , может, вы могли бы уточнить это?

Единственное, что я могу себе представить, это то, что мы могли бы добавить blanket impl: unsafe impl<T> Compatible<[MaybeUninit<u8>; size_of::<T>()]> for T { ... } поскольку преобразование любого типа в [MaybeUninit<u8>; N] его размера безопасно для всех типов. Я не знаю, насколько полезным был бы такой impl, учитывая, что MaybeUninit - это объединение, и тот, кто использует [MaybeUninit<u8>; N] , не знает, инициализирован ли конкретный элемент массива или нет .

@gnzlbg тогда вы говорили о FromBits<T> for [u8] . Здесь я говорю, что вместо этого мы должны использовать [MaybeUninit<u8>] .

Я обсуждал это предложение с @nikomatsakis на RustConf, и он посоветовал мне продолжить работу с RFC. Я собирался сделать это через несколько недель, но, если будет интерес, я могу попробовать сделать это в эти выходные. Было бы это полезно для этого обсуждения?

@joshlf, о каком предложении ты говоришь?

@RalfJung

@gnzlbg тогда вы говорили о FromBitsдля [u8]. Вот где я говорю, что мы должны использовать [MaybeUninit] вместо этого.

Попался, полностью согласен здесь. Совершенно забыл, что мы тоже хотели это сделать 😆

@joshlf, о каком предложении ты говоришь?

Предложение FromBits / IntoBits . TL; DR: T: FromBits<U> означает, что любой битовый шаблон, который является допустимым U соответствует действительному T . U: IntoBits<T> означает то же самое. Компилятор автоматически определяет оба типа для всех пар типов с учетом определенных правил, и это открывает множество забавных вещей, которые в настоящее время требуют unsafe . Там в проект этого RFC здесь , что я написал некоторое время назад, но я намерен изменить большую часть этого, поэтому не принимайте этот текст как нечто большее , чем ориентировочные.

@joshlf Я думаю, что такая пара черт больше будет строиться на этом обсуждении, чем быть его частью. AFAIK у нас есть два открытых вопроса с точки зрения действительности:

  • Это рекурсивно под ссылками? Я все больше и больше думаю, что этого не должно быть, поскольку мы видим все больше примеров. Так что, вероятно, нам следует соответствующим образом адаптировать MaybeUninit::get_mut docs (на самом деле это не UB, чтобы использовать это до завершения инициализации, но это UB, чтобы разыменовать его перед завершением инициализации). Однако сначала мы должны принять это решение для подтверждения действительности, и я не уверен, какое место для этого подходит. Наверное, специальный RFC?
  • Нужно ли инициализировать u8 (и другие целочисленные типы, с плавающей запятой, необработанный указатель), то есть MaybeUinit<u8>::uninitialized().into_inner() insta-UB? Я так думаю, но в основном это основано на интуиции, что мы хотим свести к минимуму места, где мы разрешаем poison / undef . Однако меня можно было бы убедить в обратном, если бы у этого шаблона было множество применений (и я надеюсь использовать miri, чтобы помочь в определении этого).

Это рекурсивно под ссылками?

@RalfJung, можете ли вы показать пример того, что вы имеете в виду, говоря «рекурсивно под ссылками»?

Должен ли быть инициализирован u8 (и другие целочисленные типы, с плавающей запятой, необработанный указатель), т. Е. MaybeUinit:: uninitialized (). into_inner () insta-UB?

Что произойдет, если это не мгновенный UB? Что я могу сделать с этим значением? Могу я сопоставить по нему? Если да, то является ли поведение программы детерминированным?

Мне кажется, что если я не могу сопоставить значение без введения UB, то мы заново изобрели mem::uninitialized . Если я могу сопоставить значение и одна и та же ветвь всегда берется для всех архитектур, уровней выбора и т. Д., Мы заново изобрели mem::zeroed (и вроде как используют MaybeUninit типа немного спорный). Если поведение программы не является детерминированным и изменяется с уровнями оптимизации, в зависимости от архитектуры, в зависимости от внешних факторов (например, предоставила ли ОС обнуленные страницы процесса) и т. Д., То я чувствую, что мы добавили бы огромную ногу в систему. язык.

Нужно ли инициализировать u8 (и другие целые типы, с плавающей запятой, необработанный указатель), т. Е. MaybeUinit<u8>::uninitialized().into_inner() insta-UB? Я так думаю, но в основном это основано на интуиции, что мы хотим свести к минимуму места, где мы позволяем poison / undef . Однако меня можно было бы убедить в обратном, если бы у этого шаблона было множество применений (и я надеюсь использовать miri, чтобы помочь в определении этого).

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 может делать много вещей, как связывание имен, так и собственно проверку на равенство; первое - нормально, второе - нет. Но вы можете записать это обратно в память.

По сути, это то, что сейчас реализует miri.

Мне кажется, что если я не могу сопоставить значение без ввода 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 оптимизирует практически нетипизированный код, так что здесь все это не вызывает беспокойства. Мы говорим здесь только об оптимизации МИР.

По сути, я исхожу из точки зрения, что мы должны допускать как можно меньше, пока не получим ясное применение. Мы всегда можем позволить больше вещей позже, но не наоборот. Тем не менее, в последнее время появилось несколько хороших вариантов использования байтовых срезов, которые могут устаревать любые данные, и этого может быть достаточно аргумента, чтобы сделать это, по крайней мере, для u* и i* .

Если мы решим, что ссылка действительна только в том случае, если она указывает на что-то действительное (это то, что я имею в виду под «повторением ниже ссылок»), этот код будет UB.

Попался.

Самая большая проблема с mem :: uninitialized была связана с типами, у которых есть ограничения на их допустимые значения.

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 оптимизирует практически нетипизированный код, так что здесь все это не вызывает беспокойства. Мы говорим здесь только об оптимизации МИР.

Если мы скажем, что неопределенная память имеет какое-то значение, и, следовательно, вам разрешено ветвление в соответствии с семантикой Rust, то мы не можем понизить ее до версии undefined LLVM, потому что это было бы ненадежным.

По сути, я исхожу из точки зрения, что мы должны допускать как можно меньше, пока не получим ясное применение. Мы всегда можем позволить больше вещей позже, но не наоборот.

Это честно.

Тем не менее, в последнее время появилось несколько хороших применений байтовых срезов, которые могут устаревать любые данные, и этого может быть достаточно аргумента, чтобы сделать это, по крайней мере, для u* и i* .

Включает ли какой-либо из этих вариантов использования байтовые срезы, содержащие неинициализированные значения?

Одно место, где может оказаться ценным неинициализированный, но не отравленный &mut [u8] , - это Read::read - мы бы хотели избежать необходимости обнулять буфер только из-за какой-то странной Read impl может читать из него, а не просто записывать в него.

Одно место, где может оказаться ценным неинициализированный, но не отравленный &mut [u8] , - это Read::read - мы бы хотели избежать необходимости обнулять буфер только из-за какой-то странной Read impl может читать из него, а не просто записывать в него.

Я понимаю, поэтому идея состоит в том, что MaybeUninit будет представлять тип, который инициализирован, но с неопределенным содержимым, в то время как другие типы неинициализированных данных (например, поля заполнения) по-прежнему будут полностью неинициализированы в смысле ядовитости LLVM?

Я не думаю, что это нужно вообще применять к MaybeUninit. Теоретически может существовать какой-то API, чтобы «заморозить» содержимое с неопределенного до определенного, но произвольного.

Если мы скажем, что неопределенная память имеет какое-то значение, и, следовательно, вам разрешено ветвление в соответствии с семантикой Rust, то мы не можем понизить ее до версии undefined LLVM, потому что это было бы ненадежным.

Это никогда не было предложением. Это и будет UB для ветвления на poison .

Вопрос в том, является ли UB просто «иметь» poison в локальном u8 .

Включает ли какой-либо из этих вариантов использования байтовые срезы, содержащие неинициализированные значения?

Срезы подобны ссылкам, поэтому &mut [u8] неинициализированных данных подойдет, если они только записаны (при условии, что это решение, которое мы принимаем для проверки достоверности ссылки).

@sfackler

Одно место, где может быть ценным неинициализированный, но не-яд, & mut [u8], - это Read :: read - мы хотели бы иметь возможность избежать необходимости обнулять буфер только потому, что какой-то странный Read impl может читать из него а не просто писать в него.

Что ж, без &out вы сможете это сделать, только если вы знаете имп. Вопрос не в том, должен ли безопасный код обрабатывать poison в u8 (это не так, это не нормальное использование безопасного кода!), Вопрос в том, может ли небезопасный код аккуратно обрабатывать его. сюда. (См. Сообщение в блоге, которое я хотел написать сегодня о различии между инвариантами безопасности и инвариантами действительности ...)

Возможно, я опоздал, но я бы предложил изменить подпись метода set() чтобы он возвращал &mut T . Таким образом, было бы безопасно написать полностью безопасный код, работающий с MaybeUninit (по крайней мере, в некоторых ситуациях).

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

Это практически статическая гарантия того, что init() либо инициализирует значение, либо расходится. (Если бы он попытался вернуть что-то еще, время жизни было бы неправильным, и &'static mut u8 невозможно в безопасном коде.) Возможно, в будущем его можно будет использовать как часть API-интерфейса россыпи.

@Kixunil Так было раньше, и я согласен, что это хорошо. Я просто считаю, что тот же set сбивает с толку функцию, которая что-то возвращает.

@Kixunil

Это практически статическая гарантия того, что 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 impls 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()
    }
}

Эта функция инициализирует указатель на структуру Foo , поочередно инициализируя каждое из его полей, используя предоставленные пользователем обратные вызовы инициализации. Он требует, чтобы обратные вызовы возвращали DidInit s, но не заботятся об их значениях; того факта, что они существуют, достаточно. После инициализации всех полей он знает, что все Foo является действительным, поэтому он вызывает did_init() для Uninitialized<'a, Foo> , который является небезопасным методом, который просто передает его в соответствующий DidInit type, который затем возвращает 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] поэтому я не ожидаю каких-либо реальных затрат времени выполнения.

Есть ли действительно веская причина для использования двух отдельных методов? Кажется, достаточно просто игнорировать возвращаемое значение

Думаю, единственная причина в том, что увидеть что-то return 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 все это MaybeUninit .

Если some_native_function - NOP, ничего не происходит; если позже вы используете foo_ref.value (или, скорее, foo_ref.as_mut() поскольку вы можете использовать только общедоступный API), это будет UB, потому что функция может быть вызвана только после того, как все будет инициализировано.

MaybeUninit не предотвращает наличие недопустимых значений - если бы это было возможно, это было бы безопасно, но это невозможно. Однако это делает работу с недопустимыми значениями менее простой, потому что теперь информация о том, что значение может быть недопустимым, закодирована в типе, чтобы ее могли видеть и компилятор, и программист.

Я хотел задокументировать IRC-разговор, который у меня был с @sfackler относительно гипотетической проблемы, которая может возникнуть в будущем.

Главный вопрос заключается в том, является ли mem::zeroed допустимым представлением в памяти для текущего предложения по реализации для MaybeUninit<NonZeroU8> . На мой взгляд, в состоянии «uninit» значением является только заполнение, которое компилятор может использовать для любых целей, а в состоянии «значение» все возможные значения, кроме mem::zeroed , действительны (из-за NonZero ).

Будущая система компоновки типов с более продвинутой упаковкой дискриминантов перечисления (чем у нас есть сейчас) могла бы затем сохранить дискриминант в заполнении состояния «unit» / обнуленной памяти в состоянии «value». В этой гипотетической системе размер Option<MaybeUninit<NonZeroU8>> равен 1, тогда как в настоящее время он равен 2. Кроме того, в этой гипотетической системе Some(MaybeUninit::uninitialized()) было бы неотличимо от None . Я думаю, что мы, вероятно, сможем исправить это, изменив реализацию MaybeUninit (но не его общедоступный API), как только мы перейдем к такой системе.

Я не вижу разницы между NonZeroU8 и &'static i32 в этом отношении. Оба эти типа , где «0» не действует . Итак, для обоих из них MaybeUninit<T>::zeroed().into_inner() - это insta-UB.

Может ли Option<Union> выполнять оптимизацию макета, зависит от того, какова действительность объединения. Это еще не решено для всех случаев, но есть общее согласие, что для объединений, имеющих вариант типа () , допустим любой битовый шаблон и, следовательно, невозможна оптимизация макета. Это касается MaybeUninit . Итак, Option<MaybeUninit<NonZeroU8>> никогда не будет иметь размер 1.

По общему мнению, для объединений, имеющих вариант типа (), допустима любая битовая последовательность, и, следовательно, оптимизация компоновки невозможна.

Это особый случай для «объединений с вариантом типа ()»? Стабилизирует ли стабилизация этой функции неявно ту часть Rust ABI? А как насчет union содержащего struct UnitType; или struct NewType(()); ? А как насчет struct Padded (ниже)? А как насчет union содержащего struct Padded ?

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

Моя формулировка была ужасно конкретной, потому что это буквально единственное, в чем я почти уверен, что у нас есть общее согласие. :) Я думаю, что мы хотели бы сделать это зависимым только от размера (то есть, все ZST получат это), но на самом деле я думаю, что этот вариант даже не нужен, и профсоюзы просто никогда не получат оптимизацию макета по умолчанию (но в конечном итоге пользователи могут иметь возможность отказаться от использования атрибутов). Но это только мое мнение.

У нас будет надлежащее обсуждение, чтобы оценить текущий консенсус и, возможно, прийти к соглашению по другим вопросам в одном из следующих обсуждений в репозитории UCG , и вы можете присоединиться к нему, когда это произойдет.

Стабилизирует ли стабилизация этой функции неявно ту часть Rust ABI?

Здесь мы говорим об инвариантах достоверности, а не о макете данных (на который, я полагаю, вы ссылаетесь, когда вызываете ABI). Так что ничто из этого не стабилизирует какой-либо ABI. Они связаны, но отличаются друг от друга, и фактически в настоящее время продолжается обсуждение ABI союзов .

Они связаны, но отличаются друг от друга, и фактически в настоящее время продолжается обсуждение ABI союзов.

AFAICT, это обсуждение касается только представления объединений в памяти и не включает в себя то, как объединения проходят через границы функций и другие вещи, которые могут иметь отношение к ABI. Я не думаю, что целью репозитория UCG является создание 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 (которые будут скопированы из флэш - памяти в ОЗУ , прежде чем основной).

Это небольшой пример на Rust, который использовал MaybeUninit:

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();

Какой раздел будет содержать символы a и b ?

PS Я знаю про искажение символов, но это не вопрос.

@ 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()});
}

Результат : А с оценкой 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 за получение каждой ссылки).

Мне это не кажется правильным. Разве все абстракции «один раз» не должны быть ослаблены при доступе и синхронизированы при инициализации? Или я о другом думаю?
Копия @Amanieu @alexcrichton

@ qwerty19106 :

Когда вы говорите «встроенный», вы имеете в виду «голый металл»? Стоит отметить, что .init_array фактически не является частью самого формата ELF F - Это даже не часть 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) , но на практике это генерирует тот же asm, что и load(Acquire) на всех архитектурах.

(Не могли бы вы перенести эти обсуждения в другое место? Они больше не имеют ничего общего с MaybeUninit vs mem :: uninitialized. Оба ведут себя так же, как LLVM - генерируют undef. Что происходит с этим undef позже, здесь не обсуждается. )

Am 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> . Чтобы включить эту и, возможно, другие интересные комбинации с блоками и срезами с меньшим количеством преобразований, может быть, мы могли бы добавить еще несколько API в стандартную библиотеку?

В частности, для выделения без инициализации (я думаю, что 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 (MaybeUninit :: uninitialized ()) все равно будет копировать size_of ::() байты заполнения

Этого не должно быть, и есть тест на создание кода, предназначенный для проверки этого.

Байты заполнения не нужно копировать, поскольку их битовое представление не имеет значения (все, что могло бы наблюдать битовое представление, в любом случае является UB).

Хорошо, может быть, Box::new_uninit не нужно? Однако версия среза отличается, поскольку для Box::new требуется T: Sized .

Я бы хотел, чтобы MaybeUninit::zeroed стал const fn . Есть несколько применений, связанных с FFI (например, статика, которая должна быть инициализирована нулем), и я считаю, что другие могут найти ее полезной. Я был бы счастлив добровольно посвятить свое время созданию функции zeroed .

@mjbshaw вам нужно будет использовать для этого #[rustc_const_unstable(feature = "const_maybe_uninit_zeroed")] , поскольку zeroed выполняет действия, которые не проходят проверку min_const_fn (https://github.com/rust-lang/ rust / issues / 53555), что означает, что постоянство MaybeUninit::zeroed будет нестабильным, даже если функция стабильна.

Можно ли разделить реализацию / стабилизацию этого на несколько шагов, чтобы сделать тип MaybeUninit доступным для более широкой экосистемы раньше? Шаги могут быть такими:

1) добавить MaybeUninit
2) преобразовать все варианты использования mem :: uninitialized / zeroed и устареть

@scottjmaddox

добавить MaybeUninit

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

Ницца! Так есть ли план по стабилизации MaybeUninit как можно скорее?

Следующий шаг - выяснить, почему https://github.com/rust-lang/rust/pull/54668 так сильно снижает производительность (в нескольких тестах). Однако на этой неделе у меня не будет много времени смотреть на это, я был бы рад, если бы кто-нибудь еще мог взглянуть. : D

Также я не думаю, что мы должны торопиться. Мы ошиблись в последнем API для обработки неинициализированных данных, давайте не будем торопиться и снова облажаться. ;)

Тем не менее, я бы также предпочел не добавлять ненужных задержек, чтобы мы наконец могли отказаться от старого пистолета. :)

О, и кое-что еще мне пришло в голову ... после того, как https://github.com/rust-lang/rust/pull/54667 приземлился, старые API-интерфейсы фактически защищают от некоторых из худших ножных ружей. Интересно, могли бы мы получить и это за 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 ?

Думаю, это был бы запасной план, но я бы предпочел, чтобы он указывал на то, что вы должны быть инициализированы.

Помещение акцента на то, что он должен быть инициализирован, и описания того, что он делает (unwrap / into_inner / etc.) В одном имени, становится довольно громоздким, так что как насчет того, чтобы просто сделать первое с assert_initialized и оставить второе подразумевается под подписью? Возможно unchecked_assert_initialized чтобы избежать проверки выполнения, например, assert!() .

Возможно unchecked_assert_initialized, чтобы избежать проверки выполнения, например assert! ().

Мы различаем предположения и утверждения уже через intrinsics::assume(foo) vs assert!(foo) , так что, может быть, assume_initialized ?

assume - нестабильный API, стабильный пример предположения vs assert: unreachable_unchecked vs unreachable и get_unchecked vs get . Я думаю, что unchecked - правильный термин.

Я бы сказал, что foo_unchecked имеет смысл только тогда, когда есть соответствующий foo , иначе сама природа функции unsafe указывает мне, что происходит что-то "другое" на.

Этот велосипедный навес явно не того цвета

С этим конкретным API мы уже видели и будем видеть, что программисты предполагают, что небезопасность связана с тем, что «неинициализированные данные являются мусором, поэтому вы можете вызвать UB, если обрабатываете их неосторожно», а не предполагаемое «это UB называть это по неинициализированным данным, точка ». Я не знаю наверняка, поможет ли с этим возможно избыточное ⚠️ вроде unchecked , но я бы предпочел ошибиться в сторону большего недоумения (= более вероятно, что люди будут спрашивать или читать документы очень осторожно).

@RalfJung

Мне кажется, into_inner - слишком простое имя функции. Люди, вероятно, не слишком внимательно читающие документацию, все равно делают MaybeUninit::uninitialized().into_inner() . Можем ли мы изменить имя на что-то вроде was_initialized_unchecked или около того, что означает, что вы должны вызывать это только после того, как данные были инициализированы?

Мне действительно нравится эта идея; Я твердо уверен, что он говорит правильные вещи как о семантике, так и о том, что это потенциально опасно.

@rkruppe

Помещение акцента на то, что он должен быть инициализирован, и описания того, что он делает (unwrap / into_inner / etc.) В одном имени, становится довольно громоздким, так что как насчет того, чтобы просто сделать первое с assert_initialized и оставить второе подразумевается под подписью? Возможно unchecked_assert_initialized чтобы избежать проверки выполнения, например assert!() .

Я не сомневаюсь в громоздких длинных названиях опасных вещей. Если это заставляет людей думать дважды, то даже was_initialized_into_inner_unchecked меня вполне устраивает. Создание неэргономичного (в разумных пределах) небезопасного кода - это особенность, а не ошибка;)

Помните, что подавляющее большинство людей, скорее всего, будут использовать IDE с той или иной формой автозаполнения, поэтому длинное имя - второстепенная проблема.

Меня не особо заботит эргономика использования этой функции, но я действительно думаю, что после определенных пунктов названия, как правило, скорее бегут, чем читаются, и это название действительно следует читать, чтобы понять, что происходит. Кроме того, я ожидаю, что эта функция будет обсуждаться / объясняться почти так же часто, как и на самом деле (поскольку она относительно нишевая и очень тонкая), и при вводе длинных идентификаторов в исходный код может быть нормально (например, благодаря IDE), вводить их из память в системе чата ... менее приятна (я наполовину шучу по этому поводу, но только наполовину).

@shepmaster Конечно; Я также использую IDE с автозаполнением; но я думаю, что более длинное имя с unchecked внутри, включая блок unsafe , дало бы мне дополнительную паузу.

@rkruppe

печатать их по памяти в системе чата ... менее приятно (я наполовину шучу по этому поводу, но только наполовину).

Я бы пошел на этот компромисс. Если имя немного особенное, оно может даже сделать его более запоминающимся. ;)

Любое из (или подобных имён, имеющих одинаковые смысловые коннотации):

  • 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 необходимо только для того, чтобы отличать его от другого _checked_ варианта того же функциональность, при которой не выполняются проверки во время выполнения.

При ручном внедрении генератора с самозаемным заимствованием я в конечном итоге использовал 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 допускает затенение переменных, потому что затенение явно плохая идея, когда у вас есть функция с несколькими сотнями строк. Я лично очень пренебрежительно отношусь к таким случаям, потому что считаю, что такой код обычно считается плохим тоном для начала.

Теперь, если какой-то аспект Rust заставляет небезопасные блоки быть больше, чем им «нужно», возможно, это также указывает на более фундаментальную проблему.


Кстати, мне интересно, могут ли IDE + RLS идентифицировать любую функцию, помеченную как небезопасную, и выделять ее специально. Мой редактор, например, уже выделяет ключевое слово unsafe .

Теперь, если какой-то аспект Rust заставляет небезопасные блоки быть больше, чем им «нужно», возможно, это также указывает на более фундаментальную проблему.

Ну есть https://github.com/rust-lang/rfcs/pull/2585;)

Кстати, мне интересно, могут ли IDE + RLS идентифицировать любую функцию, помеченную как небезопасную, и выделять ее специально.

Это было бы прекрасно! Однако не все читают код только в IDE, т. Е. В IDE обычно не проводятся обзоры.

Теперь, если какой-то аспект Rust заставляет небезопасные блоки быть больше, чем им «нужно», возможно, это также указывает на более фундаментальную проблему.

Я думаю, что небезопасные методы в цепочках являются одним из наиболее ярких примеров - разделение метода посередине на let -binding может быть довольно неэргономичным, но если вы этого не сделаете, будет покрыта вся цепочка.

Не совсем «заставляет», но определенно «мотивирует».

l есть rust-lang / rfcs # 2585 ;)

Да, но я не упомянул об этом, потому что это тоже не поможет вашему делу. Люди всегда могут просто добавить блок unsafe вокруг всего тела (как упоминается в комментариях), и тогда вы снова вернетесь к той же проблеме: небезопасные вызовы функций «подкрадываются».

Но не все читают код только в IDE.

Ага, вот почему я отложил это в сторону. Полагаю, мне следовало более четко заявить об этом.


Думаю, моя проблема в том, что вы, по сути , поддерживаете это:

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

Это позволяет четко видеть каждую небезопасную функцию при чтении кода. Повторение меня очень раздражает и указывает на то, что что-

Это позволяет четко видеть каждую небезопасную функцию при чтении кода. Повторение меня очень раздражает и указывает на то, что что-то неоптимально.

Я понимаю. Вы также можете увидеть это повторение как реализацию принципа четырех глаз, когда компилятор предоставляет два глаза. ;)

@shepmaster Я думаю, что это немного инварианты этого метода, то есть когда код 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 выглядит как быть довольно распространенным.

Интересно, почему это так?

Кроме того, к вашему сведению, есть проблема (https://github.com/rust-analyzer/rust-analyzer/issues/190) в rust-analyzer чтобы выявить, являются ли функции unsafe или нет. Редакторы и IDE должны иметь возможность выделять операции, требующие unsafe в блоках unsafe , которые включают не только вызов unsafe функций (независимо от того, имеют ли они суффикс идентификатора, например, например, _unchecked или нет), но также разыменование необработанных указателей и т. д.

Возможно, rust-analyzer не может этого сделать (EDIT: intellij-Rust вроде может: https://github.com/intellij-rust/intellij-rust/issues/3013#issuecomment-440442306), но если намерение состоит в том, чтобы прояснить, что для вызова этого внутри блока unsafe требуется unsafe , выделение синтаксиса является возможной альтернативой добавлению суффикса к этому полю. Я имею в виду, что если вы действительно хотите этого прямо сейчас, вы, вероятно, можете добавить имя этой функции в качестве «ключевого слова» в свой синтаксис выделения за пару минут и вызвать его через день.

@gnzlbg

В общем, добавление информации о типе к именам идентификаторов считается анти-шаблоном (например, foo_i32 , bar_mutex , baz_iterator ), потому что типы существуют для этого.

Конечно, венгерская нотация вообще считается антипаттерном. Согласен. Однако в целом в этих обсуждениях безопасность не рассматривается, и я думаю, что, учитывая опасность, которую представляет UB, есть веские причины сделать здесь исключение.

Но когда дело доходит до функций, даже если unsafe является частью типа fn , добавление _unchecked , _unsafe , _you_better_know_what_you_are_doing выглядит как быть довольно распространенным.

Интересно, почему это так?

Проще говоря: небезопасность. Когда речь идет о небезопасности, избыточность - мой друг. Это относится как к коду, так и к критически важному для безопасности оборудованию и так далее.

Кроме того, к вашему сведению, в rust-analyzer есть проблема ( rust-analyzer / rust-analyzer # 190 ), чтобы выяснить, являются ли функции unsafe или нет. Редакторы и IDE должны иметь возможность выделять операции, требующие unsafe в блоках unsafe , которые включают не только вызов unsafe функций (независимо от того, имеют ли они суффикс идентификатора, например, например, _unchecked или нет), но также разыменование необработанных указателей и т. д.

Возможно, rust-analyzer пока не может этого сделать, но если намерение состоит в том, чтобы прояснить, что для вызова этого внутри блока unsafe требуется unsafe , выделение синтаксиса является возможной альтернативой суффиксу это ни с чем.

Все это круто. Однако, как заметил @RalfJung : «Не все читают код только в IDE, т. Е. Проверки обычно не выполняются в IDE». Мне кажется маловероятным, что GitHub встроит анализатор ржавчины в свой пользовательский интерфейс, чтобы показать, является ли вызываемая функция / операция небезопасной или нет.

Если есть компромисс между уродливостью и склонностью к неправильному (и, следовательно, необоснованному) использованию unsafe , я думаю, мы всегда должны предпочесть первое. Многое можно сказать о том, чтобы сделать так, чтобы программист должен был остановиться и подумать про себя: «Подождите, я правильно делаю?»

Например, если вы хотите использовать небезопасную криптографическую операцию в Mundane, вам необходимо:

  • Импортируйте его из модуля insecure
  • Напишите allow(deprecated) или просто живите с предупреждениями компилятора, генерируемыми всякий раз, когда вы используете эту операцию
  • Напишите код, который выглядит как let mut hash = InsecureSha1::default(); hash.insecure_write(bytes); ...

Это все документировано здесь более подробно. Я не думаю, что нам нужно быть _это_ агрессивными при любых обстоятельствах, но я думаю, что правильная философия состоит в том, что если операция достаточно опасна, чтобы о ней беспокоиться, то у программиста не должно быть никакого способа упустить гравитацию. того, что они делают.

Совершенно серьезное предложение

Поскольку 95% нас беспокоят люди, злоупотребляющие этим типом, и только 5% беспокоят длинные имена, давайте начнем с переименования типа в MaybeUninitialized . Дополнительные 7 символов того стоят.

В основном серьезные предложения

  1. Переименуйте его в MaybeUninitializedOrUndefinedBehavior чтобы по-настоящему доставить его конечным пользователям.

  2. Выберите этот тип, чтобы у него не было методов, и все могло быть связанной функцией, усиливая точку при каждом вызове функции, по желанию:

    MaybeUninitializedOrUndefinedBehavior::into_inner(value)
    

Глупое предложение

MaybeUninitializedOrUndefinedBehaviorReadTheDocsAllOfThemYesThisMeansYou

Ну ... честно говоря, мне кажется неуместным наличие длинного имени, такого как MaybeUninitializedOrUndefinedBehavior в типе . Это операция .into_inner() которой нужно хорошее имя, потому что это потенциально проблемный момент, требующий дополнительного внимания. Отсутствие методов могло быть хорошей идеей. MaybeUninit::initialized_or_undefined(foo) кажется довольно ясным.

ИМО, мы не должны изо всех сил делать небезопасные операции такими неэргономичными. Нам нужны эргономичные названия и способы написания правильного небезопасного кода. Если мы загромождаем его кучей чрезмерно длинных имен, нечетких утилит и преобразований, это будет препятствовать пользователям писать правильный небезопасный код и затруднить чтение и проверку небезопасного кода.

Помните, что подавляющее большинство людей, скорее всего, будут использовать IDE с той или иной формой автозаполнения, поэтому длинное имя - второстепенная проблема.

Пока RLS не станет более функциональным, по крайней мере, для меня это не так.

Я думаю, что большинство из нас согласны с тем, что

  • Более информативные имена хороши

  • Менее эргономичные имена - это плохо

и вопрос в том, как разрешить ситуацию, когда они находятся в напряжении.

Тем не менее, я думаю, что into_inner в частности, плохая репутация для этого метода (если использовать причудливый термин, это не на границе Парето). Общее соглашение состоит в том, что у нас есть 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 self, похоже на ножное ружье, которое упрощает потерю информации о том, семантически ли вы вышли из MaybeUninit.

Это метод, который был запрошен , и пока вы отслеживаете иначе, что это не произойдет дважды, это нормально.

И иметь и заимствованный, и потребляющий вариант, похоже, не стоит.

Мне нравится take_initialized или более явный вариант take_initialized_unchecked .

давайте начнем с переименования типа в MaybeUninitialized

Кто-нибудь готов подготовить PR?

для подготовки PR?

Я могу использовать свои потрясающие навыки sed так как я это предложил ;-)

Я думаю, что было бы лучше вызвать метод into_inner который подчеркивает, что он предполагает, что он инициализирован, но я думаю, что добавление unchecked излишне и бесполезно. У нас есть способ информировать пользователей о том, что небезопасные функции небезопасны: мы генерируем ошибку компилятора, если они не помещают их в небезопасный блок.

РЕДАКТИРОВАТЬ: take_initialized кажется хорошим

А как насчет assume_initialized ? Этот:

  • Подключается к модели «обязательство доказательства»
  • Интуитивно связано с тем, что предположения рискованны
  • Нужно всего два слова
  • Описывает смысловое значение операции
  • Читается довольно естественно
  • Как и внутренняя функция LLVM assume , это UB, если ошибочно принято

Кто-нибудь готов подготовить PR?

Ничего. Команда 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 "invalid" - не лучший выбор, поскольку он все еще (должен!) удовлетворять инварианту достоверности.

Если бы у нас был небольшой документ в UCG, определяющий «безопасность», вероятно, следовало бы сделать гиперссылку на него. Вы можете добавить, что T должно быть «действительным», а гиперссылка - «действительным» определением, но поскольку у нас еще нигде нет записанных этих определений ...

Мы обязательно должны это сделать, когда что-то получим: D

@RalfJung Я не думаю, что «инвариант валидности» присутствует в лексиконе большинства (почти любого?) Пользователей Rust - я думаю, что упоминание «недействительных данных» в разговорной речи приемлемо ( ManuallyDrop<T> больше не может использоваться как а T ). Утверждение, что он должен поддерживать определенные инварианты представления, которые компилятор использует для оптимизации, не делает его менее недопустимыми данными.

Я не думаю, что «инвариант достоверности» находится в лексиконе большинства (почти любого?) Пользователей Rust.

Справедливо, термин не является официальным (пока). Но в конце концов мы должны выбрать для этого официальный термин, и тогда мы должны избегать подобных столкновений. Мы можем сказать, что «достоверные» данные - это то, что я называю «безопасными» в моем сообщении, но тогда нам нужно другое слово для того, что я называю «достоверными».

@shepmaster написал некоторое время назад

Думаю, будет полезен метод, который выходит за пределы оболочки, заменяя его неинициализированным значением:

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

Я думаю, что меня больше всего беспокоит то, что с такой функцией ужасно легко случайно скопировать не-копируемые данные. Если вам это нужно, неужели так плохо делать maybe_uninit.as_ptr().read() ?

Думаю, я мог предложить где-то наверху что-то вроде take заменить чем-то вроде into_inner . Я не думаю, что это хорошая идея: в большинстве случаев дополнительное ограничение, которое 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
    }

Смотрите этот PR для мотивации и обсуждения.

При удалении 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 более громоздкая и немного менее разборчивый (а когда мне приходится использовать небезопасный код, я очень ценю разборчивость).

@mjbshaw

в отличие от 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 , и его вызов _безопасен_, только если выполнено предварительное условие, что текущие биты в MaybeUninit<T> представляют _valid_ значение T . Если битовый шаблон _ недействительный_, поведение не определено. Не имеет значения, является ли битовый шаблон недействительным, потому что он содержит все нули, неинициализированные биты или что-то еще.

@RalfJung У меня MaybeUninit может вводить в заблуждение. Возможно, нам стоит переименовать его в MaybeInvalid или что-то в этом роде, чтобы лучше передать проблему, которую он решает, и опасности, которых он избегает. РЕДАКТИРОВАТЬ: следуя предложениям @Centril , которые я опубликовал по проблеме с велосипедным навесом.


РЕДАКТИРОВАТЬ: FWIW, я действительно думаю, что наличие эргономичного способа (например, без прямого использования MaybeUninit ) для безопасного создания обнуленной памяти было бы полезно, но mem::zeroed не так. Мы могли бы добавить черту Zeroed аналогичную Default которая реализована только для типов, для которых действителен битовый шаблон со всеми нулями или что-то в этом роде, чтобы добиться аналогичного эффекта. mem::zeroed теперь работает, но без подводных камней.

В целом, я считаю, что не следует отказываться от функциональности, пока не будет создан путь миграции для текущих пользователей к лучшему решению. MaybeUninit мой взгляд, mem::zeroed , хотя оно может и не быть идеальным (безопаснее, но не так эргономично), поэтому я бы не стал рекомендовать mem::zeroed как только приземлится MaybeUninit , даже если к тому времени у нас не будет более эргономичной замены.

Возможно, нам следует переименовать его в MaybeInvalid или что-то в этом роде, чтобы лучше передать проблему, которую он решает, и опасности, которых он избегает.

Bikeshed в https://github.com/rust-lang/rust/pull/56138.

@gnzlbg

есть некоторые "собственные" типы (например, 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 () в свой исходный комментарий). MaybeUninit ничего не добавляет, если пользователь сразу же вызывает .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 требует, чтобы all-bit-set-to-zero представлял значение 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 совершенно безопасен (например, unit), в то время как есть некоторые «собственные» типы (например, bool, & T и т.д.), для которых mem :: zeroed вызывает неопределенное поведение.

Это отвлекающий маневр. Тот факт, что и zeroed и uninitialized допустимы для некоторого подмножества типов, не делает их сопоставимыми при фактическом использовании. Вам нужно посмотреть на размер этих подмножеств. Количество типов, для которых допустима mem::uninitialized , очень-очень мало (фактически, это только типы нулевого размера?), И никто на самом деле не будет писать код, который делает это (например, для ZST вы просто использовали бы конструктор типа). С другой стороны, существует много типов, для которых допустима mem::zeroed . mem::zeroed действует как минимум для следующих типов (надеюсь, я правильно понял):

  • все целочисленные типы (включая bool , как указано выше)
  • все типы необработанных указателей
  • Option<T> где T запускает оптимизацию компоновки перечисления. T включает:

    • NonZeroXXX (все целочисленные типы)

    • NonNull<U>

    • &U

    • &mut U

    • fn -поинтеры

    • любой массив любого типа в этом списке

    • any 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 используется для типов, которые допустимы, если они обнулены.

Признак Zeroed или аналогичный (например, Pod , но обратите внимание, что T: Zeroed не подразумевает T: Pod ), как было предложено, кажется хорошим добавлением в будущем, но давайте не будем осуждать fn zeroed<T>() -> T до тех пор, пока у нас не будет стабильной fn zeroed2<T: Zeroed>() -> T .

@mjbshaw

Приносим извинения за продолжение касательной, но C11 требует, чтобы

Конечно! Только C ++ bool оставляет допустимые значения неопределенными! Спасибо, что поправили меня, отправлю PR в UCG с этой гарантией.

@jethrogb

Вам нужно посмотреть на размер этих подмножеств. Количество типов, для которых допустима mem::uninitialized , очень и очень мало (на самом деле, это только типы нулевого размера?), И никто не стал бы писать код, который это делает (например, для ZST вы просто использовали конструктор типа).

Это даже не верно для всех ZST, если вы учитываете конфиденциальность, с которой можно иметь 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> необитаем.».

Инварианты, «налагаемые кодом библиотеки» , отличаются от инвариантов, «навязанных компилятором», по нескольким причинам, см. Сообщение в блоге . В этой терминологии ваш пример 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]> имеет побитовые нули в качестве допустимого представления (он может, например, использовать [0, 24601] как представление None case - только одно из внутренних значений должно принимать нишевое представление - cc @ eddyb, чтобы проверить меня). Я сомневаюсь, что мы делаем это сегодня, но не кажется совершенно невозможным, что что-то подобное может появиться в будущем.

@jethrogb

Чаще всего 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 и т. Д. Отсрочка прекращения поддержки просто задерживает предупреждение пользователей о том, что они делают может быть неопределенное поведение.

@bluss

При удалении нуля кажется, что он заменяется только на MaybeUninit :: zeroed (). Into_inner (), который становится эквивалентным способом записи того же самого. Практических изменений нет. Вместо этого с неинициализированными значениями у нас есть практическое изменение всех неинициализированных данных, которые хранятся в значениях типа MaybeUninit или эквивалентного объединения.

Это верно, когда мы говорим о целых числах, но если мы посмотрим, например, на ссылочные типы, mem::zeroed() тоже станет проблемой.

Однако я согласен с тем, что люди с большей вероятностью поймут, что mem::zeroed::<&T>() - это проблема, чем люди, осознающие, что mem::uninitialized::<bool>() - это проблема. Так что, возможно, имеет смысл оставить mem::zeroed() .

Обратите внимание, однако, что мы все же можем решить, что mem::uninitialized::<u32>() в порядке - если мы разрешаем неинициализированные биты в целочисленных типах, mem::uninitialized() становится действительным почти для всех «типов POD». Я не думаю, что мы должны позволять это, но мы все равно должны обсудить это.

Количество типов, для которых допустима mem :: uninitialized, очень-очень мало (на самом деле, это только типы нулевого размера?), И никто на самом деле не будет писать код, который делает это (например, для 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}

Запускает ли UB создание ссылки & mut Foo и последующая запись в нее?

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, содержащееся внутри MaybeUninitчтобы вы могли со временем писать во все поля и получить действительный / инициализированный тип?

Да. Использовать

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 . Таким образом, можно предположить, что это не работает, если мы пройдем через assert. Поскольку к значению раньше не обращались, после переупорядочения мы могли бы получить это? Не похоже, что llvm использует это прямо сейчас, но я очень не уверен, будет ли это гарантировано.


Если вместо этого мы создадим в функции собственный MaybeUninit , мы получим немного другую реальность. Вместо этого на игровой площадке мы обнаруживаем, что предполагается, что утверждение никогда не сработает, предположительно, поскольку предполагается, что str::ptr::write - единственная запись в inner поэтому это должно было произойти уже до того, как мы прочитаем из previous ? В любом случае это кажется немного подозрительным. Чтобы подтвердить эту теорию, посмотрите, что произойдет, если вместо этого вы измените указатель write на false .


Я понимаю, что проблема с отслеживанием может не подходить для ответа на этот вопрос.

@RalfJung @scottjmaddox Спасибо за ваши ответы. Именно эти нюансы я и спросил.
@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> », который не знает, правильно выровнено поле или нет).

@nicoburns

который предполагает, что немедленное приведение ссылки на указатель (& mut foo as * mut T) фактически компилируется для простого создания указателя.

То, что он компилирует, отличается от семантики, которую компилятор может использовать для выполнения этой компиляции. Даже если это не работает в IR, он все равно может иметь семантический эффект, например, утверждение дополнительных предположений для компилятора. @scottjmaddox прав, когда здесь @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, чтобы развить обсуждение безопасного обнуления памяти, чтобы мы могли отказаться от mem::zeroed в какой-то момент, когда у нас будет лучшее решение этой проблемы: https://github.com / ржавчина-lang / rfcs / issues / 2626

Можно ли стабилизировать const uninitialized , as_ptr и
as_mut_ptr впереди остального API? Мне кажется очень вероятным, что эти
будут стабилизированы, как и сейчас. Кроме того, остальная часть API может быть построена на
вершине as_ptr и as_mut_ptr , поэтому после стабилизации можно будет
есть трейт MaybeUninitExt на crates.io, который в стабильной версии предоставляет API
это в настоящее время обсуждается, позволяя большему количеству людей (например, стабильным пользователям)
дать отзыв об этом.

Во встроенном вместо глобального распределителя (нестабильного) мы используем статические переменные,
много . Без MaybeUninit нет возможности иметь неинициализированную память в
статические переменные на стабильном. Это мешает нам разместить фиксированную мощность
коллекции в статических переменных и инициализация статических переменных во время выполнения, в
нулевая стоимость. Стабилизация этого подмножества API разблокирует эти варианты использования.

Чтобы вы могли понять, насколько это важно для сообщества встраиваемых систем, мы сделали
[опрос], спрашивающий сообщество об их болевых точках и потребностях. Стабилизация
MaybeUninit оказался второй наиболее востребованной вещью для стабилизации (после
const fn с границами признаков) и в целом занял 7-е место из десятков
rust-lang / * запросы, связанные с. После дальнейшего обсуждения в рабочей группе мы наткнулись на
его приоритетность в целом до третьего места из-за ожидаемого воздействия на экосистему.

(В более личном плане, я являюсь автором встроенной платформы параллелизма.
что выиграет от внутреннего использования MaybeUninit (использование памяти в
количество приложений может быть уменьшено на 10-50% без изменения кода пользователя). я
может предоставить для этого функцию Cargo только в ночное время, но после многих лет
встроен только в ночное время и только недавно стал стабильным, я чувствую, что
предоставление функции только ночью было бы неправильным сообщением для моих пользователей
поэтому я с нетерпением жду стабилизации этого API.)

@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 WG.

Я надеялся, что смог бы стабилизировать 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 .)

Я надеялся, что это может стабилизировать MaybeUninit в едином, связном «пакете» с надлежащей историей для неинициализированных данных, так что людям нужно будет заново изучить эти вещи только один раз, вместо того, чтобы выпускать их по частям и, возможно, придется попутно измените некоторые правила.

Я считаю этот аргумент очень убедительным.

Я думаю, что самая насущная потребность - получить четкое представление о том, как обрабатывать неинициализированную память без 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);

Мое неявное предположение заключалось в том, что rust-lang / rfcs # 2582 сделает приведенный выше пример правильным и четко определенным. Разве это не так?

@scottjmaddox

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

Это УБ. Это не имеет никакого отношения к ссылкам.

Мое неявное предположение заключалось в том, что rust-lang / rfcs # 2582 сделает приведенный выше пример правильным и четко определенным.

Меня это полностью удивляет. Этот RFC касается только ссылок. Почему вы думаете, что это что-то меняет в логических значениях?

@RalfJung

Это УБ. Это не имеет никакого отношения к ссылкам.

В документации к 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-компилятор уже заявили в RFC, что bool идентично _Bool C, и мы не можем нарушить эту гарантию (это позволяет 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 , но оставляет его UNINITIALIZED, и что является проблемой.

Это настолько простой способ выстрелить в созданный вами API-интерфейс, что он должен быть задокументирован как в mem :: uninitialized, так и в intrinsics :: uninit со специализацией для mem :: uninitialized.паниковать во время компиляции.

Означает ли это также, что инициализация любой структуры с логическим значением в ней с помощью mem :: uninitialized тоже является UB?

@kpp

Означает ли это также, что инициализация любой структуры с логическим значением в ней с помощью mem :: uninitialized тоже является UB?

Да - как вы, вероятно, понимаете, с помощью mem::uninitialized легко прострелить себе ногу, я бы даже сказал, что это практически невозможно правильно использовать. Вот почему мы пытаемся отказаться от него в пользу MaybeUninit , который немного более подробен в использовании, но имеет то преимущество, что, поскольку это объединение, вы можете инициализировать значения "по частям" без фактической материализации само значение в состоянии _invalid_. Значение должно быть полностью _ действительным_ только к моменту вызова into_inner() .

Возможно, вам будет интересно прочитать разделы nomicon о проверенной и непроверенной (не) инициализации: https://doc.rust-lang.org/nomicon/checked-uninit.html. Они описывают, как инициализация let x: bool; работает в безопасном Rust. Заполните вопросы, если объяснение непонятно или что-то вам непонятно. Также имейте в виду, что большинство объяснений являются «ненормативными», поскольку они еще не прошли процесс RFC. Рабочая группа по рекомендациям небезопасного кода попытается представить RFC, документирующий и гарантирующий текущее поведение где-то в этом году.

Это настолько простой способ выстрелить в созданный вами API-интерфейс, что он должен быть задокументирован как в mem :: uninitialized, так и в intrinsics :: uninit

Проблема в том, что в настоящее время нет правильного способа сделать это - поэтому мы усердно работаем над стабилизацией MaybeUninit чтобы документация этих функций могла быть заменена жирным «НЕ ИСПОЛЬЗОВАТЬ».


Дискуссии , как это и такие вопросы , как этого заставляй меня согласен больше и больше с @japaric , что мы должны получить что - это и этот список флажков, чтобы поставить галочку. Тогда у нас будет достаточно информации, чтобы предоставить некоторые базовые шаблоны.

Можно ли стабилизировать const неинициализированные, as_ptr и
as_mut_ptr впереди остального API? Мне кажется очень вероятным, что эти
будут стабилизированы, как и сейчас.

+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();

И если да, то какова логика этого (надеюсь, мы сможем включить часть обсуждения этого вопроса в документацию по MaybeUninit)? Я предполагаю что-то вроде того, что в (1) разыменованное значение всегда остается «rvalue» и никогда не становится «lvalue», тогда как в (2) недопустимое bool становится «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();

Это УБ. Это не имеет никакого отношения к ссылкам.

Мое неявное предположение заключалось в том, что rust-lang / rfcs # 2582 сделает приведенный выше пример правильным и четко определенным.

Меня это полностью удивляет. Этот RFC касается только ссылок. Почему вы думаете, что это что-то меняет в логических значениях?

@RalfJung Полагаю, я думал, что это не UB, потому что неопределенное значение было ненаблюдаемым, потому что оно было немедленно перезаписано допустимым значением типа bool. Но я полагаю, что это не так?

Для более сложных примеров, в которых значение в x реализует Drop, для перезаписи значения потребуется необработанный указатель, и поэтому я подумал, что rfc 2582 необходим, чтобы избежать UB.

Полагаю, я думал, что это не UB, потому что неопределенное значение было ненаблюдаемым, потому что оно было немедленно перезаписано допустимым значением типа bool. Но я полагаю, что это не так?

Семантика продолжается заявление за утверждением (глядя на MIR). Каждое утверждение должно иметь смысл. let x: bool = mem::uninitialized(); материализует плохое логическое значение, и не имеет значения, что произойдет позже - вы не должны материализовать плохое логическое значение.

Я понимаю, что значение x недействительно, но требует ли это неопределенного поведения ? Я понимаю, как это вообще можно вынуть из контекста. Но в контексте этого конкретного примера поведение не определено четко? Я полагаю, что моя основная проблема в том, что я не совсем понимаю значение «неопределенного поведения».

Мы хотим, чтобы компилятор мог полагаться на определенные инварианты. Это инварианты, только если они выполняются всегда . Как только мы начинаем добавлять исключения, становится беспорядок.

Возможно, вы ожидаете чего-то большего в форме « проверка значения требует, чтобы инвариант достоверности соблюдался». Здесь "проверка" bool будет использовать его в if . Это разумная спецификация, но менее полезная: теперь компилятор должен доказать, что значение действительно «проверено», прежде чем он сможет принять инвариант.

это требует неопределенного поведения?

Мы выбираем, что является неопределенным, а что нет. Это часть разработки языка. Неопределенное поведение вряд ли когда - либо «надо» сами по себе - но это необходимо для того, чтобы больше оптимизаций. Итак, искусство здесь состоит в том, чтобы найти определение неопределенного поведения (как бы противоречиво это ни звучало ^^), которое одновременно обеспечивает желаемую оптимизацию и соответствует ожиданиям (небезопасных) программистов.

Я не совсем понимаю значение «неопределенного поведения».

Я написал об этом сообщение в блоге , но краткий ответ заключается в том, что неопределенное поведение - это контракт между вами и компилятором, а в контракте говорится, что вы обязаны следить за тем, чтобы не было неопределенного поведения. Это обязательство по доказательству. «разыменование указателя NULL является UB» эквивалентно высказыванию «каждый раз, когда указатель разыменовывается, программист должен доказать, что этот указатель не может быть NULL». Это помогает компилятору понять код, потому что каждый раз, когда указатель разыменовывается, компилятор теперь может вывести «ага!» Здесь программист доказал, что указатель не равен NULL, и поэтому я могу использовать эту информацию для оптимизации и генерации кода. Спасибо , программист!"

Что именно говорится в контракте, зависит от языка программирования. Конечно, есть ограничения (например, мы ограничены LLVM). В нашем случае UCG полагает (в соответствии с тем, что мы слышали от разработчиков языка и компиляторов), что мы хотим, чтобы контракт содержал следующее предложение: «Каждый раз, когда создается rvalue, программист должен доказать, что это 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}

По сути, эта функция выделяет 1 байт в стеке, а затем загружает этот байт. Однако загрузка помечается значком !range , который сообщает LLVM, что байт должен быть между 0 <= x <2, т.е. он может быть только 0 или 1. LLVM будет считать, что это правда, а поведение не определено. если это ограничение нарушено.

Таким образом, проблема не столько в самих неинициализированных переменных, сколько в том, что вы копируете и перемещаете значения, которые нарушают ограничения их типов.

Спасибо вам обоим за экспозицию! Теперь это намного яснее!

Я полагаю, что моя основная проблема в том, что я не совсем понимаю значение «неопределенного поведения».

Эта серия сообщений в блоге (в которой есть довольно интересные / пугающие примеры во втором сообщении), я думаю, весьма полезна: http://blog.llvm.org/2011/05/what-every-c-programmer-should-know .html

Я считаю, что здесь действительно нужна хорошая документация. Изменение здесь, вероятно, хорошо по нескольким причинам, которые я могу перечислить, и, вероятно, по другим, которые я не могу. Но правильное использование неинициализированной памяти (и другие виды небезопасного использования) может быть в значительной степени нелогичным. В 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 будет разрешен, мы сможем продолжить стабилизацию частей API (конструкторы, as_ptr , as_mut_ptr , set , into_initialized ).

Почему не MaybeUninit::zeroed() a const fn ? ( MaybeUninit::uninitialized() - это const fn )

РЕДАКТИРОВАТЬ: можно ли сделать const fn с помощью ночного Rust?

Почему не 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)
};

согласно Мири, этот пример работает (хотя теперь я понимаю, что не знаю, является ли само преобразование ящиков типов с идентичной компоновкой).

@ Nemo157, зачем вам такая же раскладка памяти, когда у вас into_inner ?

@Pzixel, чтобы избежать копирования значения после инициализации, представьте, что он содержит буфер размером 100 МБ, который вызовет переполнение стека, если он выделен в стеке. Хотя при написании тестового примера кажется, что для этого требуется дополнительный API fn uninit_boxed<T>() -> Box<MaybeUninit<T>> чтобы можно было выделить неинициализированный блок, не касаясь стека.

Используя синтаксис box позволяющий выделить неинициализированное пространство кучи, вы можете увидеть, что подобное преобразование работает, а попытка использования into_initialized вызывает переполнение стека: игровая площадка

@ Nemo157 Может лучше

@ Nemo157

Недавно мне было интересно узнать, гарантированно ли MaybeUninit<T> будет иметь тот же макет, что и T , и можно ли что-то подобное использовать для частичной инициализации значений в куче, а затем превратить его в полностью инициализированное значение,

Я считаю , что это гарантировано, и что вам код действителен, с несколькими оговорками:

  • В зависимости от типа, который вы используете (особенно в общем коде), вам может потребоваться ptr::write_unaligned .
  • Если есть больше полей, и только некоторые из них инициализированы, вы не должны преобразовывать в T, пока все поля не будут полностью инициализированы .

Это также вариант использования, который меня интересует, поскольку я считаю, что он может быть объединен с proc-макросом для обеспечения безопасной абстракции построителя на месте.

@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() , но на самом деле они менее безопасны, чем эти методы).

Я считаю, что они не входят в подмножество API, который @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 если люди действительно не уверены, что им нужна неинициализированная память?

В связи с этим, в каких случаях вам действительно нужна «неинициализированная» память, а не «замороженная»?

@Pzixel

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 - это не моя идея, я просто участвовал в обсуждении, и мне очень нравится это предложение.)

Я считаю, что они _не_ в подмножестве API, который @RalfJung предложил стабилизировать

Верный. И действительно, может быть, нам их вообще не следует иметь.

Имеет ли смысл иметь аналогичный метод для построения MaybeUninit ? ( MaybeUninit::frozen() , MaybeUninit::abitrary() или аналогичный).

Да! Я собирался предложить добавить это, когда базовый MaybeUninit станет стабильным и ptr::freeze приземлится.

В связи с этим, в каких случаях вам действительно нужна «неинициализированная» память, а не «замороженная»?

Это требует дополнительного изучения и тестирования, ожидается, что это может снизить производительность, потому что LLVM не будет выполнять оптимизацию, которую он мог бы сделать в противном случае.

(Я вернусь и к другим комментариям, когда у меня будет время.)

Возможность @Pzixel создавать объекты непосредственно в предварительно выделенной памяти нетривиально, в Rust было два RFC, принятых для реализации такой вещи (более 4 лет назад!), Но с тех пор они были отклонены, и большая часть реализации была удалена (кроме синтаксиса box я использовал выше). Если вы хотите получить более подробную информацию, лучше всего начать с темы i.rl.o об удалении .

Как упоминает @nicoburns, MaybeUninit потенциально может использоваться в качестве строительного блока для менее эргономичного библиотечного решения той же проблемы, что очень полезно как способ начать экспериментировать с концепцией и посмотреть, какие API-интерфейсы она позволяет строительство. Это просто зависит от того, может ли 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 достаточно. (Не используйте присваивание, потому что оно может упасть! Я все время забываю об этом ...)

Хотя при написании тестового примера кажется, что для этого требуется дополнительный API fn uninit_boxed<T>() -> Box<MaybeUninit<T>> чтобы можно было выделить неинициализированный блок, не касаясь стека.

Это ошибка , но, поскольку ее трудно исправить, было бы неплохо предложить для нее отдельный конструктор. Однако не знаю, как это реализовать. И тогда мы, вероятно, также хотим что-то вроде zeroed_box которое избегает обнуления слота стека, а затем memcpying и так далее ... Мне не нравится все это дублирование. : /

Поэтому я предлагаю, чтобы после / параллельно с начальной стабилизацией некоторые люди, у которых есть варианты использования неинициализированной памяти в куче (в основном, смешивание Box и MaybeUninit ), собрались вместе и разработали минимальный возможное расширение API для этого. @eddyb тоже проявил интерес к этому. На самом деле это больше не связано с отказом от поддержки mem::uninitialized , поэтому я думаю, что это должно найти свое собственное место для обсуждения, помимо этой (слишком большой-уже) проблемы отслеживания.

Мой собственный отзыв: я в целом доволен MaybeUninit<T> . У меня нет особых претензий. Это меньше похоже на ножное ружье, чем mem::uninitialized , что приятно. uninitialized методы const new и uninitialized . Я бы хотел, чтобы больше методов было константным, но, насколько я понимаю, многие из них требуют большего прогресса в const fn в целом, прежде чем их можно будет сделать const .

Я бы хотел более надежную гарантию, чем «одинаковый макет» для T и MaybeUninit<T> . Я бы хотел, чтобы они были ABI-совместимыми (фактически #[repr(transparent)] , хотя я знаю, что этот атрибут не может применяться к объединениям) и безопасными для FFI (т.е. если T безопасен для FFI , тогда MaybeUninit<T> тоже должно быть FFI-безопасным). (По касательной, я бы хотел, чтобы мы могли использовать #[repr(transparent)] для объединений, которые имеют только одно поле положительного размера (как мы можем для структур))

Я действительно полагаюсь на ABI MaybeUninit<T> в моем проекте, чтобы помочь с оптимизацией (но не небезопасным способом, так что не паникуйте). Если кому-то интересно, я с удовольствием расскажу подробности, но я сохраню этот комментарий кратким и пока опущу детали.

@mjbshaw Спасибо!

Я бы хотел, чтобы мы могли использовать #[repr(transparent)] для объединений, у которых есть только одно поле положительного размера (как мы можем для структур).

Как только этот атрибут существует, добавить его в MaybeUninit не составит труда. А ведь логика для этого уже был реализован в rustc ( MaybeUninit<T> де-факто ABI совместимы с T , но мы не можем гарантировать , что.)

Все, что нужно, - это написать RFC и довести его до конца, а также добавить несколько проверок, чтобы убедиться, что в объединениях repr(transparent) есть только одно поле, отличное от ZST. Вы бы хотели попробовать? : D

Все, что нужно, - это написать RFC и довести его до конца, а также добавить несколько проверок, чтобы убедиться, что в объединениях repr(transparent) есть только одно поле, отличное от ZST. Вы бы хотели попробовать? : D

@RalfJung Просите, и получите!

Копия https://github.com/rust-lang/rust/pull/58468

Остается только API, который, я думаю, мы можем разумно стабилизировать в maybe_uninit , а остальное переместится в отдельные ворота функций.

Хорошо, все подготовительные PR завершены, и into_inner тоже пропало.

Однако я бы очень хотел, чтобы https://github.com/rust-lang/rfcs/pull/2582 был принят до стабилизации, иначе у нас даже не будет способа инициализировать структуру по полю - и это похоже на простой вариант использования MaybeUninit . Мы очень близки к

Я только что преобразовал свой код в 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 самом деле не имеет полезного семантического инварианта, поэтому я не уверен, что полностью согласен с этим утверждением. ТБХ Я не уверен, что полезно рассматривать операции над MaybeUninit либо иначе, кроме как только их непосредственный операционный эффект.

@RalfJung хм, может быть, «семантически» здесь неправильное слово. Что касается того, как пользователь должен использовать тип, вы должны предположить, что значение снова не инициализировано после того, как вы его прочитаете (если вы конкретно не знаете, что тип - Copy ).

Если вы посмотрите только на необработанный операционный эффект, вы получите странные взаимодействия, подобные этому, когда вы можете нарушить инварианты безопасности других небезопасных API без технического чтения неинициализированной памяти. (Я как бы надеялся, что Мири по-прежнему будет отслеживать чтения неинициализированной памяти нулевой длины, но похоже, что это не так).

@RalfJung Во всех моих случаях это включает static mut в который значение помещается, а затем извлекается. Поскольку я не могу использовать статику, я не могу использовать into_uninitialized .

@Amanieu то, о чем я спрашивал, почему вы думаете, что x.take_initialized() яснее, чем x.as_ptr().read() ?

@ Nemo157

Я как бы надеялся, что Мири по-прежнему будет отслеживать чтения неинициализированной памяти нулевой длины, но это не так.

Чтение неинициализированной памяти нулевой длины никогда не является UB, так зачем Мири заботиться о них?

Если вы посмотрите только на необработанный операционный эффект, вы получите странные взаимодействия, подобные этому, когда вы можете нарушить инварианты безопасности других небезопасных API без технического чтения неинициализированной памяти.

Конечно, вы можете нарушить инварианты безопасности, даже не читая неинициализированную память. Вы также можете просто использовать для этого MaybeUninit::zeroed().into_initialized() . Я не вижу проблемы.
«Странное взаимодействие» здесь заключается в том, что вы создали два значения одного типа, которые не имели права создавать. Все дело в инварианте безопасности Spartacus и не имеет ничего общего с инвариантами действительности.

Вот почему я думаю, что read_initialized() лучше передает то, что происходит: мы считываем данные и утверждаем, что они правильно инициализированы (что включает в себя проверку того, что нам действительно разрешено создавать это значение для этого типа). Это не влияет на битовый шаблон, все еще хранящийся в MaybeUninit .

@RalfJung Я рассматриваю MaybeUninit как Option , но без тега. Фактически, я ранее использовал ящик с непомеченными параметрами именно для этой цели, и в нем есть метод take для извлечения значения из объединения.

@Amanieu @shepmaster Я добавил read_initialized в https://github.com/rust-lang/rust/pull/58660. Я все еще думаю, что это имя лучше, чем take_initialized . Это соответствует вашим потребностям?

Этот PR также добавляет примеры к некоторым другим методам, обратная связь приветствуется!

Я доволен read_initialized .

Пока я занимался этим, я также заработал MaybeUninit<T>: Copy if 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 для каждого элемента среза.

API, которые у меня есть, выглядят примерно так:

// 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 . Но это, наверное, далеко не лучший API.

Однако я бы предпочел, чтобы мы могли сначала сосредоточиться на поставке «базового API», а затем посмотреть на взаимодействие со срезами (и с Box ).

Мне нравится идея переименования set в write , что обеспечивает согласованность с ptr::write .

В том же духе, действительно ли read_initialized лучше, чем просто read ? Если проблема связана с случайным использованием, которое становится скрытым, возможно, сделать это функцией вместо метода, например MaybeUninit::read(&mut v) ? То же самое можно сделать для write , т.е. MaybeUninit::write(&mut v) для согласованности. В обоих случаях компромисс между удобством использования и явностью, и если явность считается лучше в одном случае, я не понимаю, почему она будет отличаться в другом.

В любом случае, пока эти API не будут выработаны, я решительно поддерживаю стабилизацию с использованием минимального API, т.е. 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 , и, похоже, это может занять некоторое время ...

стабилизация с помощью минимального API, т.е. new , uninitialized , zeroed , as_ptr , as_mut_ptr

Мой план заключался в том, чтобы into_initialized , set / write и read_initialized были частью этого минимального набора. Но может и не должно быть? set / write и read_initialized могут быть легко реализованы с остальными, поэтому теперь я также склоняюсь к тому, чтобы не стабилизировать их в первом пакете. Но иметь что-то вроде into_initialized с самого начала желательно, ИМО.

возможно, сделать это функцией вместо метода, т.е. 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 - это функции, а не методы. Итак, приоритет уже установлен

Они не являются частью структуры данных, конечно, это автономные функции. И, как вы заметили, сегодня они существуют тоже как методы.

Но они потребляют указатель

Исходные указатели - это 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() кажется «читать», а не «получать», но тогда дополнением ( .as_ptr_mut().write() ) должно быть «записать», а не «установить».
  2. read_initialized -> read . Соответствует write , но небезопасно. Достаточно ли этого (плюс документации) для предупреждения, что вы должны вручную убедиться, что данные уже инициализированы? Было много соглашения , что небезопасные into_inner не хватает, поэтому я переименовал его в into_initialized .
  3. into_initialized -> initialized . Если у нас есть и read_initialized и into_initialized , это хорошо согласуется с этим ИМО, но если это read , тогда into_initialized немного выпирает. Название метода довольно длинное. Тем не менее, насколько мне известно, наиболее трудоемкие операции называются into_* .

Есть возражения против (1)? И я больше всего опираюсь на (3). В отношении (2) я не определился: read легче набирать, но read_initialized IMO работает лучше при чтении такого кода - и код читается и проверяется чаще, чем пишется. Кажется, неплохо указать место, где мы действительно предполагаем, что объекты инициализируются.

Мысли, мнения?

Что ж, я думаю, мы должны принять этот RFC, прежде чем что-либо стабилизировать - иначе у нас даже не будет санкционированного способа инициализировать структуру поле за полем, что кажется абсолютным минимумом.

Это где я вставляю плагин для offset_of! ? :)

Обратите внимание, что read_initialized является строгим надмножеством into_initialized (принимает &self вместо self ). Имеет ли смысл поддерживать и то, и другое?

Это где я вставляю плагин для offset_of! ? :)

Если вы сможете стабилизировать это до того, как мой RFC будет принят, конечно. ;)

Имеет ли смысл поддерживать и то, и другое?

ИМО да. 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 , но использование аббревиатуры для типа и длинной формы для метода или наоборот - болезненная несогласованность. В идеале мне просто нужно помнить, что «API Rust используют аббревиатуры / длинные формы» и все.

Поскольку аббревиатуры могут быть неоднозначными для разных людей, я предпочитаю везде использовать длинные формы и называть это обычным делом. Но использование смеси - ИМО, худшее из обоих миров. Rust имеет тенденцию использовать аббревиатуры чаще, чем более длинные формы, поэтому я бы ничего не имел против Uninit<T> как аббревиатуры и .into_init() как другого аббревиатуры метода.

Мне не нравится into_initialized() , потому что похоже, что происходит преобразование для инициализации значения. Я предпочитаю take_initialized() . Я понимаю, что сигнатура типа отличается от других методов take , но я думаю, что она намного более ясна семантически, и я считаю, что семантическая ясность должна заменить согласованность заимствования / перемещения. Другими альтернативами, которые еще не являются изменяемыми заимствованиями, могут быть move_initialized или consume_initialized .

Что касается set() vs write() , я настоятельно рекомендую write() , чтобы вызвать сходство с as_ptr().write() , для которого это будет псевдоним.

И, наконец, если будет take_initialized() или аналогичный, то я предпочитаю read_initialized() read() из-за явности первого.

Изменить : но, чтобы уточнить, я думаю, что придерживаться as_ptr().write() и as_ptr().read() еще более ясно и с большей вероятностью вызовет ментальные схемы ОПАСНОСТЬ, ОПАСНОСТЬ .

@gnzlbg у нас есть FCP для имени типа, я не уверен, стоит ли нам снова открывать это обсуждение.

Однако мне нравится предложение использовать «init» последовательно, как в MaybeUninit::uninit() и x.into_init() .

Мне не нравится into_initialized() , потому что похоже, что происходит преобразование для инициализации значения.

into методы часто фактически не выполняют никаких преобразований, кроме просмотра одних и тех же (принадлежащих) данных определенного типа - см., Например, различные методы into_vec .

Меня устраивает take_initialized(&mut self) (в дополнение к into_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) (в дополнение к into_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() vs v.as_ptr().read() . v.read_init() кажется лишним.

Я предлагал take_initialized (self) вместо into_initialized (self), потому что считаю, что первое имя более точно описывает операцию. Опять же, я понимаю, что дубль обычно принимает & mut self, а into обычно принимает self, но я считаю, что семантически точное именование более важно, чем последовательно типизированное именование.

Я твердо уверен, что 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 "инициализируется" в том смысле, что оно удовлетворяет _инварианту действительности_ T , но не в том смысле, что оно удовлетворяет _инварианту_безопасности_ 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! .

Вместо того, чтобы писать инвариант безопасности в комментарии, вы можете просто написать код для проверки.

Я думаю, стоит еще раз напомнить, что единственное, что утверждает into_init, - это то, что значение удовлетворяет инварианту достоверности T, что не следует путать с T, «инициализированным» в любом общем смысле этого слова.

Это верно. OTOH, я считаю, что "инициализированный" - разумный прокси для этого в первом объяснении.

В противном случае люди могли бы написать fn incredperly_uninitialized () -> AlwaysTrue как безопасный fn и просто вернуть небезопасное AlwaysTrue, потому что они "инициализировали" его.

Я считаю, что мы можем сделать разумный вывод о том, что это неправильно "инициализировано". Я согласен с тем, что нам нужна надлежащая документация о том, как эти два инварианта где-то взаимодействуют (и я не уверен, что было бы лучше всего), но я также думаю, что интуиция большинства людей скажет, что improperly_uninitialized - это не нормально. функция для экспорта. «Нарушение инвариантов других людей» - это концепция, которая, как мне кажется, возникает естественным образом, когда вы думаете о том, что «все безопасные функции, которые я экспортирую, должны быть такими, чтобы безопасный код не мог использовать их для разрушения хаоса».

Одна вещь, которую можно сделать с Invalidи небезопасноимеет две характеристики, ValidityCheckeable и UnsafeCheckeable, с двумя методами, ValidityCheckeable :: is_valid (Invalid) и UnsafeCheckeable :: is_safe (Небезопасный) и имеют методы Invalid :: into_valid и Unsafe :: into_safe assert_validity! и assert_safety! на них.

В подавляющем большинстве случаев инвариант безопасности не поддается проверке. Даже инвариант достоверности, вероятно, не может быть проверен для ссылок. (Ну, это немного зависит от того, как мы учитываем вещи.)

@scottjmaddox

А как насчет нового имени, такого как двигаться ..., потреблять ... или трансмутировать ... или что-то в этом роде? Я думаю, что into_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 со списком довольно убедительных обоснований.

ТБХ Я считаю, что иметь два типа слишком многословно.

@RalfJung Можем ли мы углубиться в это? возможно, с некоторыми сравнениями примеров, которые, по вашему мнению, показывают высокую степень многословия?

Хм ... если мы рассматриваем более подробные API, то

uninit.into_inner(uninit.assert_initialized());

вполне может работать семантически. Первый метод возвращает токен, который записывает ваше утверждение. Второй метод возвращает внутренний тип, но требует, чтобы вы подтвердили его действительность.

Я не совсем уверен, что это стоит дополнительных усилий, поскольку абстракция может запутать людей и, следовательно, сделать ошибки.

У нас также есть accept_initialized из-за @eternaleye (я думаю). См. # 53491 (комментарий) со списком довольно убедительных обоснований.

Справедливо. assume_initialized мне нравится.

А может это assume_init ? Вероятно, это должно согласовываться с конструктором MaybeUninit::uninit() vs 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 и обновлены стандартная библиотека и документация.

Так что, ИМО, любой, кто хочет, чтобы эта конвенция состоялась, имеет больше шансов пожарить, чем велосипедный отказ от MaybeUninit API, и я бы посоветовал не откладывать его стабилизацию дальше, чтобы дождаться разрешения этого процесса. Если мы стабилизируем преобразования MaybeUninit<T> -> T , будущие поколения Rust по-прежнему могут писать MaybeUninit<Unsafe<T>> для обозначения данных, которые сначала не инициализированы, а затем, возможно, все еще небезопасны после инициализации.

@RalfJung

А может это assume_init ? Вероятно, это должно согласовываться с конструктором MaybeUninit::uninit() vs MaybeUninit::uninitialized() - и _это_ один из них должен быть стабилизирован с помощью первого пакета, поэтому мы должны сделать этот вызов в ближайшее время.

Если бы у нас была трехсторонняя согласованность с типом, конструктором и функцией -> 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> . Я просто думаю, что разделение на два типа, как бы они ни назывались, может оказаться полезным в образовании. Возможно, есть другие способы, такие как усиление документации, которые могут компенсировать это в рамках текущей структуры?

Я _can_ вижу аргументы в пользу переименования MaybeUninit в MaybeInvalid . Однако «недействительный» очень расплывчатый (недействительный для _what_?), Я видел людей, которых смущало мое различие между «действительным» и «безопасным» - можно было предположить, что «действительный Vec » действителен любой вид использования. «неинициализированный» по крайней мере вызывает у большинства людей правильные ассоциации. Может, нам стоит переименовать «инвариант достоверности» в «инвариант инициализации» или что-то в этом роде?

Я определенно согласен с тем, что «достоверность» и «безопасность» сбивают с толку из-за того, как «действительный» звучит. Я был неравнодушен к «машинному инварианту» как замене «достоверности» и «инвариант системы типов» вместо «безопасности».

@rkruppe

Так что, ИМО, любой, кто хочет, чтобы эта конвенция состоялась, имеет больше шансов пожарить, чем байкшед MaybeUninit API, и я бы посоветовал не откладывать его стабилизацию дальше, чтобы дождаться разрешения этого процесса. Если мы стабилизируем преобразования MaybeUninit<T> -> T , будущие поколения Rust по-прежнему могут писать MaybeUninit<Unsafe<T>> для обозначения данных, которые сначала не инициализированы, а затем, возможно, все еще небезопасны после инициализации.

Хорошие моменты, особенно re. MaybeUninit<Unsafe<T>> ; Вы, вероятно, также можете добавить псевдоним типа, чтобы сделать имя типа менее подробным.

Если бы у нас была 3-сторонняя согласованность с типом, конструктором и функцией -> T, это было бы еще лучше. Поскольку тип не имеет суффикса -ialized, я думаю, что :: 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 Просто сделайте это (и меня, если хотите), как вы делали раньше с другими PR с переименованием, и если кто-то возражает, мы можем справиться с этим в FCP. :)

Просто сделайте это (и меня, если хотите), как вы делали раньше с другими PR с переименованием, и если кто-то возражает, мы можем справиться с этим в FCP. :)

Что ж, я собираюсь немного подождать, потому что это не обязательно должно быть частью начальной стабилизации.

отказаться от нестабильного метода и вместо него ввести другой нестабильный метод

Ах, попался. Тогда продолжай.

Хорошо, переименовываем в https://github.com/rust-lang/rust/pull/59284 :

неинициализированный -> unit
into_initialized -> предположить_init
read_initialized -> читать
установить -> написать

Мне нравятся недавно предложенные имена. Меня немного беспокоит неправильное использование read , но это кажется гораздо менее вероятным, чем неправильное использование into_initialized , в первую очередь из-за связи с ptr::read . В целом, я считаю, что новое название вполне приемлемо для стабилизации.

Я готовлю PR для стабилизации MaybeUninit :: {new, uninit, zeroed, as_ptr, as_mut_ptr}.

Есть ли шанс, что это попадет в 1.35-бету (через ~ 2 недели)?

Я немного не согласен с этим, учитывая, насколько все еще витает в воздухе 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 .

Я хотел бы поделиться своим мнением о 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), что немного неудобно, но не является большой проблемой.

Мне также нужно добавить API-интерфейсы, которые я хотел бы при работе с унифицированными массивами, часто в сочетании с C.

  1. Было бы неплохо использовать метод перехода от &mut [MaybeUninit<T>] к &mut [T] , пользователь должен убедиться, что все значения в срезе правильно инициализированы.
  2. Функция или макрос инициализатора публичного массива, например uninitialized_array , также были бы действительно хорошим дополнением.

Я хотел оставить отзыв о MaybeUninit

Большое спасибо!

возврат & mut T в MaybeUninit :: set приводит к необходимости использовать let _ = ...

Почему так? Вы можете просто «выбросить» возвращаемые значения, на самом деле примеры в документации не делают let _ = ... . ( write / set пока не имеет примера ... но на самом деле это почти то же самое, что и read , возможно, нужно просто связать.)

foo.write(bar); отлично работает без let .

работа с унифицированными массивами

Да, это определенно область будущих интересов.

@RalfJung

возврат & 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 Хех, я не думаю, что видел ваш комментарий здесь, когда писал это в другом месте: https://github.com/rust-lang/rust/issues/54542#issuecomment -478261027

Удаление устаревших старых переименованных функций в https://github.com/rust-lang/rust/pull/59912.

После этого, я думаю, следующее, что нужно сделать, это предложить стабилизацию ...: tada:

Я немного не согласен с тем, чтобы продвигать это, учитывая, насколько все еще rust-lang / rfcs # 2582 . : / Без этого RFC постепенная инициализация структуры все еще невозможна, но люди все равно будут это делать.
OTOH, MaybeUninit ждал достаточно долго. И это не похоже на то, что код для постепенной инициализации, который люди пишут сейчас, лучше, чем тот, который они написали бы с помощью MaybeUninit .

После этого, я думаю, следующее, что нужно сделать, - это предложить стабилизацию ...

@RalfJung Как здесь состояние документации? Если мы сможем смягчить "люди все равно так поступят" с помощью некоторых четких документов, которые помогут мне лучше спать ... :)

При чтении документов MaybeUninit , в частности assume_init , в разделе «Безопасность» не ясно, что если вы вызываете mu.assume_init() а затем возвращаете этот результат в сейфе fn , тогда вы также должны соблюдать инварианты безопасности. Перед стабилизацией было бы хорошо улучшить эти документы и дать фрагменты с предоставленными библиотекой инвариантами безопасности, которые также должны поддерживаться при использовании MaybeUninit .

Как здесь состояние документации? Если мы сможем смягчить "люди все равно так поступят" с помощью некоторых четких документов, которые помогут мне лучше спать ... :)

Я, вероятно, добавлю раздел о постепенной инициализации структур, сказав, что в настоящее время это не поддерживается. Люди, читающие это, будут типа: «Чего, правда?».

TBH Я нахожу это довольно неприятным. :( Я думаю, что мы уже могли дать какой-то совет по этому поводу, и мне жаль, что мы не смогли это сделать.

в разделе «Безопасность» не ясно, что если вы вызываете mu.assume_init () и затем возвращаете этот результат в безопасный fn, вы также должны соблюдать инварианты безопасности. Перед стабилизацией было бы хорошо улучшить эти документы и предоставить фрагменты с предоставленными библиотекой инвариантами безопасности, которые также должны поддерживаться при использовании MaybeUninit.

По сути, вы предлагаете превратить это в документы, объясняющие всю идею инвариантов типов данных и то, как это реализуется в Rust. Я думаю, что MaybeUninit - неподходящее место для этого; из-за этого может показаться, что эта проблема специфична для MaybeUninit хотя на самом деле это не так. То, о чем вы спрашиваете, следует объяснять на более высоком уровне, например, в Номиконе. Я планирую сосредоточить документы MaybeUninit на основной проблеме этого типа. Не стесняйтесь расширять их, если считаете, что это полезно. :)

По сути, вы предлагаете превратить это в документы, объясняющие всю идею инвариантов типов данных и то, как это реализуется в Rust.

Это немного сильно ... Я всего лишь предлагаю "О, __ кстати__, помните, что инвариант безопасности тоже имеет значение" в каком-то стратегическом месте в документации MaybeUninit<T> . Я не предлагаю добавлять роман. ;) Этот роман может находиться в Номиконе, но есть вероятность, что большинство людей, использующих MaybeUninit<T> , в основном будут взаимодействовать со стандартной библиотечной документацией.

Хорошо, я попытался включить все это в PR стабилизации: 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 здесь, что такой вариант использования лучше обрабатывать вспомогательным модулем или ящиком.

Стабилизационный PR появился как раз к бета-версии. :) Прошло около 8 месяцев с тех пор, как я начал изучать ситуацию вокруг профсоюзов и неинициализированной памяти, и, наконец, у нас есть кое-что, что (скорее всего) будет отправлено через 6 недель. Какое путешествие! Большое спасибо всем, кто помогал с этим. : D

Конечно, мы далеки от завершения. Необходимо решить https://github.com/rust-lang/rfcs/pull/2582 . libstd по-прежнему довольно часто использует mem::uninitialized (в основном в коде для конкретной платформы), которые требуют переноса. Стабильный API, который у нас сейчас есть, очень минимален: нам нужно выяснить, что делать с read и write , и мы должны придумать API, которые помогают работать с массивами и блоками MaybeUninit . И нам нужно многое объяснить, чтобы постепенно отвести всю экосистему от mem::uninitialized .

Но мы доберемся до этого, и этот первый шаг был, вероятно, самым важным. :)

и мы должны придумать API, которые помогут работать с массивами и блоками MaybeUninit .

@RalfJung С этой целью; может быть, сейчас самое время начать работать над https://github.com/rust-lang/rust/issues/49147? = P

Кроме того, нам, вероятно, следует разделить и закрыть эту проблему отслеживания в пользу более мелких для оставшихся битов.

С этой целью; Может, пора приступить к работе над # 49147? = P

Вы только что стали волонтером? ;) (Боюсь, у меня не будет на это времени.)

нам, вероятно, следует разделить и закрыть эту проблему отслеживания в пользу более мелких для оставшихся битов.

Я оставлю это экспертам по процессу. Но я склонен согласиться.

Вы только что стали волонтером? ;) (Боюсь, у меня не будет на это времени.)

Что я сделал ... = 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 уже в этой теме : smile:

@Centril хочет обсудить подвопрос , который может быть полезен, если вы его разделите.

Да, я помню это обсуждение, но не помню конкретного api.

Вкратце, я хочу иметь что-то вроде

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 будут стабилизированы одновременно (все функции указывают на эту проблему). Есть ли причина не делать этого? Они хороши и являются единственным признаком того, что выполнение действия, которое они выполняют, разрешено (что, очевидно, должно быть правдой).

Это заблокировано при разрешении https://github.com/rust-lang/unsafe-code-guidelines/issues/77 : безопасно ли иметь &mut bool который указывает на неинициализированную память? Я думаю, что ответ должен быть «да», но люди не согласны.

Это заблокировано при разрешении rust-lang / unsafe-code -idance # 77

Я не думаю, что блокировка должна происходить. Вы можете стабилизировать его и сказать «это UB использовать это, если память не инициализирована», а затем смягчить требование, если мы определим, что это нормально. Это хороший метод для пост-инициализации.

а затем смягчить требование

Это означает, что если я кодирую в соответствии с документацией будущей версии, но кто-то компилирует мой код, используя (совместимую с API!) Старую версию компилятора, теперь есть UB?

@Gankro

Я не думаю, что блокировка должна происходить. Вы можете стабилизировать его и сказать «это UB использовать это, если память не инициализирована», а затем смягчить требование, если мы определим, что это нормально. Это хороший метод для пост-инициализации.

Мне это кажется очень пугающим. Почему бы просто не написать &mut *foo.as_mut_ptr() ? После того, как вы все инициализировали, почему это не сработает? IOW, теперь мне интересно, что вы говорите

единственное указание на то, что выполнение действия, которое они выполняют, разрешено

потому что почему бы и нет ? Если мы исчерпывающе перечислим все, что вы можете сделать после инициализации значения, это будет длинный список. ^^

@shepmaster

Это означает, что если я кодирую в соответствии с документацией будущей версии, но кто-то компилирует мой код, используя (совместимую с API!) Старую версию компилятора, теперь есть UB?

Это верно сегодня, если люди делают &mut *foo.as_mut_ptr() . Я не вижу способа избежать этого.

Кроме того, UB есть только в том случае, если нам действительно нужно что-то изменить при создании этой документации. В противном случае мы находимся в странной ситуации, когда UB был бы, если бы тот же код выполнялся с тем же компилятором до того, как мы сделали гарантию, но теперь, когда мы делаем гарантию, UB больше нет. UB - это свойство не только компилятора, но и спецификации, и спецификация может изменяться задним числом. ;)

Правильно, я предполагал, что процесс был

  • стабилизируйте его с помощью строгого, но бессмысленного требования сейчас
  • приступим к работе над моделью памяти и что у вас
  • как только модель будет готова

    • если нужен UB, круто, оставьте документацию без изменений, добавьте оптимизацию, если это полезно

    • если он не должен быть UB, круто, скиньте его из документации и закончите

@RalfJung

Есть ли здесь возврат?

Да, возврат значения или передача его куда угодно - это использование.

Это очень быстро становится очень тонким. Итак, ответ, который, я думаю, мы должны дать, заключается в том, что каждое присваивание (на самом деле каждая копия, как и каждое присваивание после понижения до MIR) является использованием, и это включает назначение в let x: bool = mem :: uninitialized ().

Выглядит корректно.

Так или иначе, это про произвольную вложенность MaybeUninit? Можно ли его безопасно преобразовать, не требуя от пользователя написания преобразования для каждого типа оболочки?

@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 не включают гарантию того, что деструкторы всегда будут работать.

Можно ли добавить метод MaybeUninit<T> -> NonNull<T> к MaybeUninit ? AFAICT указатель, возвращаемый MaybeUninit::as_mut_ptr() -> *mut T , никогда не равен нулю. Это уменьшит отток из-за необходимости взаимодействовать с API, которые используют 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 внутри, использование сторонних ящиков по-прежнему вызывает предупреждение об устаревании. Я заметил это сегодня, когда компилировал один из своих проектов с помощью nightly compiler. Несмотря на то, что код не содержит ни единого упоминания uninitialized , я получил предупреждение об устаревании, потому что он вызвал макрос implement_vertex glium.

Запуск cargo +nightly test на glium master дает мне более 1400 строк вывода, в основном состоящих из предупреждений об устаревании функции uninitialized (я считаю предупреждение 200 раз, но оно, вероятно, ограничено числом, которое rg "uninitialized" | wc -l output равно 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:

Как MaybeUninitявляется более безопасной альтернативой, начиная с Rust 1.38 функция mem :: uninitialized будет устаревшей.

Таким образом, я думаю, что нам следует избегать флип-флопперов на этом, поскольку это не дает хорошего сообщения и сбивает с толку. Более того, дата прекращения поддержки также должна быть широко известна, учитывая, что она упоминалась в сообщении в блоге.

Возможно, уже поздно возвращаться к прекращению поддержки uninitialized . Но, может быть, мы могли бы выбрать политику, которая будет выдавать предупреждения об устаревании в Nightly только после того, как замена уже некоторое время находится на стабильном канале?

Например, Firefox через две недели после выпуска потребовал новую версию Rust .

Мы уже говорили в сообщении блога о версии 1.36.0:

Я не согласен с тем, что упоминание свидания в сообщении в блоге - это такая железная степень. Он находится в репо, и мы можем отправить редактирование.

Таким образом, я думаю, что нам следует избегать флип-флопперов на этом, поскольку это не дает хорошего сообщения и сбивает с толку.

"шлепки" - это плохо, но менять свое мнение на основе данных и обратной связи - не то.

Меня не особо волнует фактическое решение, но я не верю, что люди будут сбиты с толку этим предложением. Те, кто видел сообщение в блоге или предупреждение об устаревании, могут перейти на новую вещь. Людям, у которых нет, просто наплевать на еще несколько релизов.

"шлепки" - это плохо, но менять свое мнение на основе данных и обратной связи - не то.

Полностью согласен. Я не вижу плохого сообщения, когда говорят: «Эй, наш график прекращения поддержки был слишком агрессивным, мы вернули все назад на выпуск». На самом деле, как раз наоборот.
На самом деле IIRC я ​​упоминал во время стабилизации PR, что прецедент состоит в том, чтобы исключить 3 релиза в будущем, а не 2, но по какой-то причине мы выбрали 2. Три релиза означают 1 полный релиз между стабильным-получает-выпущен-с- -deprecation-announcement и deprecated-on-nightly, это кажется подходящим временем для людей, отслеживающих каждую ночь. 6 недель - это эон, верно? ;)

Так что завтра я планирую отправить PR, который изменяет устаревшую версию на 1.39.0. Я также могу отправить PR для обновления этого сообщения в блоге, если люди сочтут это важным.

Так что завтра я планирую отправить PR, который изменяет устаревшую версию на 1.39.0. Я также могу отправить PR для обновления этого сообщения в блоге, если люди сочтут это важным.

Я соглашусь на 1.39, но не позже. Вам также необходимо будет обновить примечания к выпуску в дополнение к сообщению в блоге.

Отправленный PR для измененного графика прекращения поддержки: https://github.com/rust-lang/rust/pull/62599.

@SimonSapin

Какие еще проблемы вызывают блокирование стабилизации остальных методов? Выполнение всего с помощью * 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 * manual_drop.as_mut_ptr (), что именно тогда им нужно, чтобы что-то сделать.

Да, я знаю. Надежда состоит в том, чтобы подтолкнуть людей к тому, чтобы как можно больше откладывать часть &mut и работать с необработанными указателями. Конечно, без https://github.com/rust-lang/rfcs/pull/2582 это часто бывает сложно.

Документация на MaybeUninit кажется отличным местом, чтобы хотя бы обсудить, что это двусмысленность семантики языка и что пользователи должны консервативно предполагать, что это не нормально.

Правда, это был бы другой вариант.

Даже при консервативном предположении as_mut действительно после полной инициализации значения.

Один из способов быть консервативным с массивами - использовать MaybeUninit<[MaybeUninit<Foo>; N]> . Внешние оболочки позволяют создать массив с помощью одного вызова uninit() . (Я думаю, что для литерала [expr; N] требуется Copy ?) Внутренние оболочки делают безопасным даже в консервативном предположении использование удобства slice::IterMut для обхода массива и затем инициализируйте значения Foo одно за другим.

@SimonSapin видит нестабильный макрос uninitialized_array! в libcore .

@RalfJung, может быть, uninit_array! было бы лучшим именем.

@Stargateur Безусловно, это точно не будет стабилизировано с его нынешним названием. Мы надеемся, что стабилизации никогда не удастся, если https://github.com/rust-lang/rust/issues/49147 произойдет скоро (TM).

@RalfJung Ух , это моя вина, я без веской причины блокировал PR: https://github.com/rust-lang/rust/pull/61749#issuecomment -512867703

@eddyb, это работает для libcore, ура! Но почему-то, когда я пытаюсь использовать эту функцию в 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 Может быть, открыть PR, удалив использование макроса там, где это совершенно не нужно?

@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. Точно такой же API, как get_ref и get_mut , что может привести к утечке памяти при наличии клея;

    • (Вариант): тот же API, что и get_ref / get_mut , но с привязкой Copy ;
  2. API стиля закрытия, чтобы гарантировать падение:

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 , тогда этот API стал бы почти строго второстепенным. (Я говорю почти потому, что с предлагаемым API чтение по ссылке было бы нормальным, чего никогда не может быть в случае 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 основана на библиотеках со звуковыми API.

@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.

Закрытие этого вопроса в пользу мета-проблемы, которая отслеживает MaybeUninit<T> более общем плане: # 63566

Была ли эта страница полезной?
0 / 5 - 0 рейтинги