Rust: Объединения без тегов (проблема отслеживания для RFC 1444)

Созданный на 8 апр. 2016  ·  210Комментарии  ·  Источник: rust-lang/rust

Проблема с отслеживанием для rust-lang / rfcs # 1444.

Нерешенные вопросы:

  • [x] Присваивается ли прямое назначение полю объединения, вызывая отбрасывание предыдущего содержимого?
  • [x] При выходе из одного поля союза другие считаются недействительными? ( 1 , 2 , 3 , 4 )
  • [] При каких условиях вы можете реализовать Copy для объединения? Например, что, если некоторые варианты относятся к типу, отличному от копирования? Все варианты?
  • [] Какое взаимодействие существует между объединениями и оптимизацией компоновки перечисления? (https://github.com/rust-lang/rust/issues/36394)

Открытые вопросы высокой важности:

  • [x] https://github.com/rust-lang/rust/issues/47412 - Средство проверки безопасности на основе MIR иногда принимает небезопасный доступ к объединенным полям при наличии необитаемых полей.
B-RFC-approved B-unstable C-tracking-issue F-untagged_unions T-lang disposition-merge finished-final-comment-period

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

@nrc
Что ж, подмножество довольно очевидно - «союзы FFI», или «союзы C», или «союзы до C ++ 11» - даже если это не синтаксически. Моей первоначальной целью было как можно скорее стабилизировать это подмножество (в идеале - этот цикл), чтобы его можно было использовать в таких библиотеках, как winapi .
В остальном подмножестве и его реализации нет ничего особенно сомнительного, это просто не срочно и нужно ждать неясное количество времени, пока не завершится процесс для RFC «Unions 1.2». Я ожидал стабилизации оставшихся частей за 1, 2 или 3 цикла после стабилизации начального подмножества.

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

Возможно, я пропустил это при обсуждении RFC, но правильно ли я считаю, что деструкторы вариантов объединения никогда не запускаются? Будет ли в этом примере работать деструктор для Box::new(1) ?

union Foo {
    f: i32,
    g: Box<i32>,
}

let mut f = Foo { g: Box::new(1) };
f.g = Box::new(2);

@sfackler В настоящее время я понимаю, что f.g = Box::new(2) _ будет запускать деструктор, но f = Foo { g: Box::new(2) } _не_. То есть присвоение значения Box<i32> l приведет к падению, как всегда, а присвоение Foo lvalue - нет.

Итак, присвоение варианту похоже на утверждение, что поле ранее было «действительным»?

@sfackler Для Drop , да, это я понимаю. Если они ранее не были действительными, вам нужно использовать форму конструктора Foo или ptr::write . Однако, судя по быстрому grep, не похоже, что RFC явно описывает эту деталь. Я рассматриваю это как реализацию общего правила, согласно которому запись в значение Drop l вызывает вызов деструктора.

Должен ли союз & mut с вариантами Drop быть пухлым?

В пятницу, 8 апреля 2016 г., Скотт Олсон [email protected] написал:

@sfackler https://github.com/sfackler Для типов Drop, да, это мой
понимание. Если они ранее не были действительными, вам нужно использовать Foo
конструктор form или ptr :: write. Судя по быстрому grep, это не похоже
Однако в RFC эта деталь подробно описана.

-
Вы получаете это, потому что подписаны на эту ветку.
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -207634431

8 апреля 2016 г., 15:36:22 PDT, Скотт Олсон [email protected] написал:

@sfackler Для Drop , да, это я понимаю. Если они
ранее недействительны, вам нужно использовать форму конструктора Foo или
ptr::write . Судя по быстрому grep, не похоже, что RFC
однако подробно об этой детали.

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

Да, этот подход мне тоже кажется немного менее опасным.

Отсутствие сброса при назначении полю объединения приведет к тому, что f.g = Box::new(2) действовать иначе, чем let p = &mut f.g; *p = Box::new(2) , потому что вы не можете сделать последний случай _not_ drop. Я думаю, что мой подход менее удивителен.

Это тоже не новая проблема; Программистам unsafe уже приходится иметь дело с другими ситуациями, когда foo = bar - это UB, если foo не инициализирован, а Drop .

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

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

Я не собираюсь использовать изменяемые ссылки на союзы и, возможно,
просто "странно помеченные" с Into

В пятницу, 8 апреля 2016 г., Питер Аташян [email protected] написал:

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

-
Вы получаете это, потому что подписаны на эту ветку.
Ответьте на это письмо напрямую или просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -207653168

Похоже, это хороший вопрос, который стоит поднять как нерешенный. Я еще не уверен, какой подход предпочитаю.

@nikomatsakis Насколько мне неудобно назначать объединенному полю типа с Drop требовать предыдущую достоверность этого поля, упомянутый эталонный случай @tsion кажется почти неизбежным. Я думаю, что это может быть просто ошибка, связанная с кодом, который намеренно отключает линт для помещения типа с Drop в объединение. (И краткое объяснение этого должно быть в пояснительном тексте к этому линту.)

И я хотел бы повторить, что unsafe программисты уже должны знать, что a = b означает drop_in_place(&mut a); ptr::write(&mut a, b) для написания безопасного кода. Не отбрасывать поля объединения было бы еще одним исключением, а не меньше.

(Примечание: сброса не происходит, если a _статически_ известно, что он уже не инициализирован, например let a; a = b; .)

Но я поддерживаю наличие предупреждения по умолчанию против вариантов Drop в союзах, которые люди должны #[allow(..)] поскольку это довольно неочевидная деталь.

@tsion это неверно для a = b и, возможно, только иногда верно для a.x = b но это определенно верно для *a = b . Эта неопределенность и заставила меня колебаться. Например, это компилируется:

fn main() {
  let mut x: (i32, i32);
  x.0 = 2;
  x.1 = 3;
}

(хотя попытка напечатать x позже не удалась, но я считаю это ошибкой)

@nikomatsakis Этот пример для меня новый. Думаю, я бы счел это ошибкой, которую компилирует этот пример, учитывая мой предыдущий опыт.

Но я не уверен, что вижу уместность этого примера. Почему то, что я сказал, неверно для a = b и только иногда для a.x = b ?

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

fn main() {
    let mut x: (Box<i32>, i32);
    x.0 = Box::new(2); // x.0 statically know to be uninit, destructor not called
    x.0 = Box::new(3); // x.0 destructor is called before writing new value
}

Может быть, просто против такой записи?

Я хочу сказать, что = не _ всегда_ запускает деструктор; Это
использует некоторые сведения о том, известна ли цель
инициализирован.

Во вторник, 12 апреля 2016 г., в 16:10:39 -0700 Скотт Олсон написал:

@nikomatsakis Это новый для меня пример. Думаю, я бы счел это ошибкой, которую компилирует этот пример, учитывая мой предыдущий опыт.

Но я не уверен, что вижу уместность этого примера. Почему то, что я сказал, неверно для a = b и только иногда для 'ax = b'?

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

fn main() {
    let mut x: (Box<i32>, i32);
    x.0 = Box::new(2); // x.0 statically know to be uninit, destructor not called
    x.0 = Box::new(3); // x.0 destructor is called
}

@nikomatsakis

Он запускает деструктор, если установлен флаг отбрасывания.

Но я думаю, что такая запись все равно сбивает с толку, так почему бы просто не запретить ее? Вы всегда можете сделать *(&mut u.var) = val .

Я хочу сказать, что = не _ всегда_ запускает деструктор; он использует некоторые сведения о том, инициализирована ли цель.

@nikomatsakis Я уже упоминал, что:

(NB: сброса не происходит, если статически известно, что объект a уже не инициализирован, например let a; a = b ;.)

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

@tsion

Флаги сброса являются только полудинамическими - после сброса обнуления они являются частью кодогенерации. Я говорю, что мы запрещаем такую ​​запись, потому что она приносит больше путаницы, чем пользы.

Следует ли вообще разрешать типы Drop в объединениях? Если я правильно понимаю, основная причина создания объединений в Rust - это взаимодействие с кодом C, который имеет объединения, а в C даже нет деструкторов. Для всех остальных целей кажется, что лучше просто использовать enum в коде Rust.

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

А также вызов такого кода вручную через drop_in_place или аналогичный.

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

Можно ли запретить установщики полей, но потребовать полной замены объединения? В этом случае, если объединение реализует Drop, полное объединение drop будет вызываться для значения, замененного, как ожидалось.

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

Для обсуждения я намерен предоставить изменяемые ссылки на поля в объединениях _и_ поместить в них произвольные (возможно, Drop ) типы. В принципе, я хотел бы использовать союзы для написания специальных перечислений с эффективным использованием пространства. Например,

union SlotInner<V> {
    next_empty: usize, /* index of next empty slot */
    value: V,
}

struct Slot<V> {
    inner: SlotInner<V>,
    version: u64 /* even version -> is_empty */
}

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

Чтобы избежать излишне сложной семантики, присвоение полю объединения должно действовать как присвоение полю структуры, что означает удаление старого содержимого. Достаточно легко избежать этого, если вы знаете об этом, назначив вместо этого всему объединению. Это все еще немного удивительное поведение, но наличие поля union, которое вообще реализует Drop вызовет предупреждение, и в тексте этого предупреждения можно явно упомянуть это как предостережение.

Имеет ли смысл предоставить RFC pull request, изменяющий RFC1444, для документирования этого поведения?

@joshtriplett Поскольку @nikomatsakis уехал в отпуск, я отвечу: я думаю, что это отличная форма - подать RFC поправки для решения подобных вопросов. Мы часто ускоряем отслеживание таких RFC PR, когда это необходимо.

@aturon Спасибо. Я подал новый RFC PR https://github.com/rust-lang/rfcs/issues/1663 с этими пояснениями к RFC1444, чтобы решить эту проблему.

( @aturon, вы можете

У меня есть предварительная реализация в https://github.com/petrochenkov/rust/tree/union.

Статус: реализовано (по модулю ошибок), PR отправлен (https://github.com/rust-lang/rust/pull/36016).

@petrochenkov Замечательно ! Пока все выглядит отлично.

Я не совсем уверен, как обрабатывать объединения с полями, отличными от Copy в проверке перемещения.
Предположим, u - это инициализированное значение union U { a: A, b: B } и теперь мы выходим из одного из полей:

1) A: !Copy, B: !Copy, move_out_of(u.a)
Это просто, u.b также переводится в неинициализированное состояние.
Проверка работоспособности: union U { a: T, b: T } должен вести себя точно так же, как псевдоним поля struct S { a: T } +.

2) A: Copy, B: !Copy, move_out_of(u.a)
Предположительно, u.b все же следует инициализировать, потому что move_out_of(u.a) - это просто memcpy и никоим образом не меняет u.b .

2) A: !Copy, B: Copy, move_out_of(u.a)
Это самый странный случай; предположительно u.b также должен быть переведен в неинициализированное состояние, несмотря на то, что он Copy . Copy значения могут быть неинициализированными (например, let a: u8; ), но изменение их состояния с инициализированного на неинициализированное - это что-то новое, AFAIK.

@ retep998
Я знаю, что это совершенно не имеет отношения к потребностям FFI :)
Хорошая новость в том, что это не блокировщик, я собираюсь реализовать любое более простое поведение и отправить PR в эти выходные.

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

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

По этим правилам я ожидаю, что союзы будут реализовывать Copy если для них реализовано копирование. Однако, в отличие от структур / перечислений, здесь не будет внутренних проверок работоспособности: вы всегда можете реализовать копию для типа объединения, если хотите.

Позвольте мне прояснить несколько примеров:

union Foo { ... } // contents don't matter

Это объединение является аффинным, потому что Copy не реализовано.

union Bar { x: Rc<String> }
impl Copy for Bar { }
impl Clone for Bar { fn clone(&self) -> Self { *self } }

Этот тип объединения Bar является копией, поскольку реализован Copy .

Обратите внимание, что если бы Bar было структурой, реализация Copy была бы ошибкой из-за типа поля x .

Ха, я полагаю, что на самом деле я не отвечаю на ваш вопрос, теперь, когда я его перечитал. знак равно

Итак, я понимаю, что вообще не отвечал на ваш вопрос. Так что позвольте мне попробовать еще раз. Следуя принципу «битового ведра», я все еще ожидаю, что мы можем выйти из союза по своему желанию. Но, конечно, другим вариантом было бы относиться к нему так, как если бы мы относились к *mut T , и потребовать от вас использовать ptr::read для выхода.

РЕДАКТИРОВАТЬ: Я не совсем уверен, почему мы запрещаем такие действия. Возможно, это пришлось сделать с перемещением отбрасывания - или, может быть, просто потому, что легко сделать ошибку, и кажется, что лучше сделать "перемещение" более явным? У меня проблемы с запоминанием истории здесь.

@nikomatsakis

Мой инстинкт подсказывает, что профсоюзы - это, по сути, «мусорное ведро».

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

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

Все правила заимствования / перемещения в текущей реализации поддерживают эту гарантию, одновременно это наиболее консервативная интерпретация, которая позволяет нам идти либо «безопасным» путем (например, разрешая безопасный доступ к объединениям с одинаково типизированными полями, это может быть полезно ) или в будущем, когда будет накоплен больший опыт работы с союзами Rust.

На самом деле, я бы хотел сделать его еще более консервативным, как описано в https://github.com/rust-lang/rust/pull/36016#issuecomment -242810887

@petrochenkov

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

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

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

Поэтому я вижу любое использование some_union.field как в основном неявное (и небезопасное) утверждение, что набор допустимых вариантов в настоящее время включает field . Это похоже на то, как работает интеграция с проверкой заимствований; если вы заимствуете поле x а затем попытаетесь использовать y , вы получите сообщение об ошибке, потому что вы в основном говорите, что данные одновременно x и y (и заимствовано). (Напротив, с обычным перечислением невозможно заселить более одного варианта за раз, и вы можете увидеть это в том, как действуют правила заимствования ).

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

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

Мы должны обсудить вопрос о том, какие условия необходимы для реализации Copy в объединении - также мы должны убедиться, что у нас есть полный список этих серых областей, перечисленных выше, чтобы убедиться, что мы обращаемся и документируем до стабилизации!

По сути, объединение похоже на перечисление, но может быть одновременно в нескольких вариантах.

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

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

Я собираюсь внести поправки в RFC союза в не столь отдаленном будущем! Интерпретация "enum" имеет довольно забавные последствия.

преобразование во время компиляции обычно плохо (если мы не пытаемся превратить компилятор в какой-то частичный целевой эмулятор)

@petrochenkov Это одна из целей моего проекта Мири . Мири уже может делать трансмутации и различные махинации с необработанными указателями. Заставить Мири обрабатывать объединения потребовало бы небольшой работы (ничего нового в части обработки необработанной памяти).

И @eddyb настаивает на замене постоянной оценки rustc версией Miri.

@petrochenkov

Одним из аргументов против интерпретации "более чем одного варианта" является поведение союзов в постоянных выражениях ...

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

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

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

Я чувствую, что эта интерпретация странно взаимодействует с ходами в целом. Например, если данные «действительно» представляют собой X, и вы интерпретируете их как Y, но Y является аффинным, то остается ли это X?

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

Я собираюсь внести поправки в RFC союза в не столь отдаленном будущем! Интерпретация "enum" имеет довольно забавные последствия.

Такая уверенность! Ты собираешься попробовать;)

Не расскажете подробнее о том, какие конкретные изменения вы имеете в виду?

Не расскажете подробнее о том, какие конкретные изменения вы имеете в виду?

Более подробное описание реализации (то есть лучшая документация), некоторые небольшие расширения (например, пустые объединения и .. в шаблонах объединения), две основные (противоречащие) альтернативы эволюции объединения - более опасные и менее ограничивающие «рабочее пространство» интерпретация и более безопасная и более ограничительная интерпретация "перечисления с неизвестным дискриминантом" - и их последствия для средства проверки перемещения / инициализации, Copy impls, unsafe ty доступа к полю и т. д.

Также было бы полезно определить при доступе к неактивному полю объединения UB, например

union U { a: u8, b: () }
let u = U { b: () };
let a = u.a; // most probably an UB, equivalent to reading from `mem::uninitialized()`

но это бесконечно сложная область.

Похоже, кросс-полевая семантика - это в основном приведение указателя, верно?
_ (_ () как * u8)

В четверг, 1 сентября 2016 г., Вадим Петроченков [email protected]
написал:

Также было бы полезно определить при доступе к неактивному полю объединения
это UB, например

союз U {a: u8, b: ()}
пусть u = U {b: ()};
пусть a = ua; // скорее всего UB, что эквивалентно чтению из mem::uninitialized()

но это бесконечно сложная область.

-
Вы получаете это, потому что подписаны на эту ветку.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -244154751,
или отключить поток
https://github.com/notifications/unsubscribe-auth/ABxXhi68qRITTFW5iJn6omZQQBQgzweNks5qlw4qgaJpZM4IDXsj
.

Разве доступ к полю всегда небезопасен?

В четверг, 1 сентября 2016 г., Вадим Петроченков [email protected]
написал:

Не расскажете подробнее о том, какие конкретные изменения вы имеете в виду?

Более подробное описание реализации (т.е. лучше
документация), некоторые небольшие расширения (например, пустые объединения и .. в объединении
закономерности), две основные (противоречащие) альтернативы эволюции союза - подробнее
небезопасная и менее ограничительная интерпретация "места для царапин" и более безопасная
и более ограничительная интерпретация "перечисления с неизвестным дискриминантом" - и
их последствия для средства проверки перемещения / инициализации, копирования, небезопасности
полевого доступа и др.

-
Вы получаете это, потому что подписаны на эту ветку.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment -244151164,
или отключить поток
https://github.com/notifications/unsubscribe-auth/ABxXhuHStN8AFhR3KYDU27U29MiMpN5Bks5qlws9gaJpZM4IDXsj
.

Разве доступ к полю всегда небезопасен?

Иногда его можно сделать безопасным, например

  • присвоение тривиально разрушаемым полям объединения безопасно.
  • любой доступ к полям union U { f1: T, f2: T, ..., fN: T } (т.е. все поля имеют один и тот же тип) безопасен в интерпретации "перечисление с неизвестным дискриминантом".

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

В настоящее время тестируется поддержка союзов в последней версии rustc от git. Все, что я пробовал, работает отлично.

Наткнулся на интересный случай в мёртвом шашке. Попробуйте следующий код:

#![feature(untagged_unions)]

union U {
    i: i32,
    f: f32,
}

fn main() {
    println!("{}", std::mem::size_of::<U>());
    let u = U { f: 1.0 };
    println!("{:#x}", unsafe { u.i });
}

Вы получите эту ошибку:

warning: struct field is never used: `f`, #[warn(dead_code)] on by default

Похоже, что программа проверки dead_code не заметила инициализацию.

(Я уже подал PR № 36252 об использовании «поля структуры», заменив его просто «поле».)

В настоящее время объединения не могут содержать поля с динамическим размером, но RFC ни в коем случае не определяет это поведение:

#![feature(untagged_unions)]

union Foo<T: ?Sized> {
  value: T,
}

Вывод:

error[E0277]: the trait bound `T: std::marker::Sized` is not satisfied
 --> <anon>:4:5
  |
4 |     value: T,
  |     ^^^^^^^^ trait `T: std::marker::Sized` not satisfied
  |
  = help: consider adding a `where T: std::marker::Sized` bound
  = note: only the last field of a struct or enum variant may have a dynamically sized type

Контекстное ключевое слово не работает вне корневого контекста модуля / ящика:

fn main() {
    // all work
    struct Peach {}
    enum Pineapple {}
    trait Mango {}
    impl Mango for () {}
    type Strawberry = ();
    fn woah() {}
    mod even_modules {
        union WithUnions {}
    }
    use std;

    // does not work
    union Banana {}
}

Похоже на бородавку довольно неприятной консистенции.

@nagisa
Вы случайно используете старую версию rustc?
Я только что проверил ваш пример на манеже, и он работает (по модулю ошибок «пустое объединение»).
Для этой конкретной ситуации также существует тестовая проверка выполнения - https://github.com/rust-lang/rust/blob/master/src/test/run-pass/union/union-backcomp.rs.

@petrochenkov а, я использовал play.rlo, но похоже, что он вернулся к stable или что-то в этом роде. Тогда не обращайте на меня внимания.

Я думаю, что профсоюзы в конечном итоге должны будут поддерживать _безопасные поля_, злых близнецов небезопасных полей из этого предложения .
Копия https://github.com/rust-lang/rfcs/issues/381#issuecomment -246703410

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

Например, объединение, содержащее исключительно поля Copy non-Drop, все с одинаковым размером, кажется безопасным; независимо от того, как вы обращаетесь к полям, вы можете получить неожиданные данные, но вы не можете столкнуться с проблемой безопасности памяти или неопределенным поведением.

@joshtriplett Вам также нужно будет убедиться, что в типах нет "дыр", из которых вы можете читать неинициализированные данные. Как я люблю говорить: «Неинициализированные данные - это либо непредсказуемые случайные данные, либо ваш закрытый ключ SSH, в зависимости от того, что хуже».

Разве &T копирует и не выпадает? Поместите это в «безопасное» соединение с usize , и у вас будет генератор ложных ссылок. Так что правила должны быть немного более строгими.

Например, объединение, содержащее исключительно поля Copy non-Drop, все с одинаковым размером, кажется безопасным; независимо от того, как вы обращаетесь к полям, вы можете получить неожиданные данные, но вы не можете столкнуться с проблемой безопасности памяти или неопределенным поведением.

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

  • u8 и bool имеют одинаковый размер, но большинство значений u8 недопустимы для bool и игнорирование этого триггера вызывает UB
  • &T и &U имеют одинаковый размер и равны Copy + !Drop для всех T и U (при условии, что оба или ни один из них не являются Sized )
  • преобразование между uN / iN и fN в настоящее время возможно только в небезопасном коде. Я считаю, что такие трансмутации всегда безопасны, но это действительно расширяет безопасный язык, поэтому может быть спорным.
  • Нарушение конфиденциальности (например, каламбур между struct Foo(Bar); и Bar ) является большим запретом, поскольку конфиденциальность может использоваться для поддержания инвариантов, относящихся к безопасности.

@Amanieu Когда я писал это, я хотел добавить примечание об отсутствии внутреннего отступа и почему-то забыл это сделать. Спасибо, что уловили это.

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

@rkruppe

u8 и bool имеют одинаковый размер, но большинство значений u8 недопустимы для bool, и игнорирование этого триггера UB

Хорошая точка зрения; та же проблема относится и к перечислениям.

& T и & U имеют одинаковый размер и являются копией +! Drop для всех T и U (при условии, что оба или ни один из них не имеют размера)

Я забыл об этом.

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

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

Нарушение конфиденциальности (например, каламбур между struct Foo (Bar); и Bar) - это большой запрет, поскольку конфиденциальность может использоваться для поддержания инвариантов, относящихся к безопасности.

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

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

Числа с плавающей запятой имеют сигнальное значение NaN, которое является представлением прерывания, которое приводит к UB.

@ retep998 Работает ли Rust на любых платформах, которые не поддерживают отключение ловушек с плавающей запятой? (Это не меняет проблемы UB, но теоретически мы могли бы решить эту проблему.)

@petrochenkov

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

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

Вопрос о #[repr(C)] : как недавно указал мне @pnkfelix , текущая спецификация гласит, что если объединение не равно #[repr(C)] , незаконно хранить с полем x и читать с полем y . Предположительно это связано с тем, что нам не нужно начинать все поля с одного и того же смещения.

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

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

Мысли?

@nikomatsakis

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

Худшая часть - это фрагменты вариантов / полей, которые напрямую доступны для союзов.
Рассмотрим этот код:

union U {
    a: (u8, bool),
    b: (bool, u8),
}
fn main() {
    unsafe {
        let mut u = U { a: (2, false) };
        u.b.1 = 2; // turns union's memory into (2, 2)
    }
}

Все поля равны Copy , право собственности не задействовано, и проверка перемещения выполняется успешно, но частичное присвоение неактивному полю b превращает объединение в состояние с допустимыми вариантами 0 . Пока не думал, как с этим бороться. Сделать такие задания УБ? Изменить толкование? Что-то другое?

@petrochenkov

Сделать такие задания УБ?

Это было бы моим предположением, да. Когда вы назначили с помощью a , вариант b не был в наборе допустимых вариантов, и, следовательно, последующее использование u.b.1 (для чтения или назначения) недопустимо.

Вопрос о # [repr (C)]: как недавно указал мне @pnkfelix , текущая спецификация гласит, что если объединение не равно # [repr (C)], запрещено хранить с полем x и читать с полем y . Предположительно это связано с тем, что нам не нужно начинать все поля с одного и того же смещения.

Я думаю, что подходящая формулировка здесь такова: 1) Чтение из полей, которые не "совместимы с макетом" (это расплывчато) с ранее написанными полями / фрагментами полей - это UB 2) Для #[repr(C)] объединений пользователи знают, какие макеты (из документов ABI), чтобы они могли различать UB и не-UB 3) Для #[repr(Rust)] макеты #[repr(Rust)] не-UB способом.

4) После того, как вопросы о размере / шаге и изменении порядка полей будут решены, я ожидаю, что макеты структуры и объединения будут высечены и указаны, поэтому пользователи также будут знать макеты и смогут использовать объединения #[repr(Rust)] так же свободно, как #[repr(C)] и проблема исчезнет.

@nikomatsakis В обсуждении RFC объединения люди упомянули о желании иметь собственный код Rust, который использует объединения для построения компактных структур данных.

Что мешает людям, использующим #[repr(C)] ? Если нет, то я не вижу необходимости предоставлять какие-либо гарантии для #[repr(Rust)] , просто оставьте это как «здесь будут драконы». Вероятно, лучше всего будет иметь линт, который по умолчанию предупреждается для профсоюзов, не являющихся #[repr(C)] .

@ retep998 Мне кажется разумным, что repr(Rust) не гарантирует какой-либо конкретный макет или перекрытие. Я бы просто предположил, что repr(Rust) на практике не должно нарушать предположения людей об использовании памяти объединением («не больше, чем самый большой член»).

Работает ли Rust на каких-либо платформах, которые не поддерживают отключение ловушек с плавающей запятой?

На самом деле это не правильный вопрос. Во-первых, сам оптимизатор может полагаться на универсальность представлений ловушек и неожиданно переписывать программу. Более того, Rust также не поддерживает изменение среды FP.

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

Мысли?

В этом поможет добавление линта или чего-то подобного, которое проверяет ход программы и бросает жалобу пользователю, если чтение выполняется из поля, когда перечисление было доказуемо записано в каком-то другом поле Линт на основе МИР быстро с этим справится. Если CFG не позволяет сделать какие-либо выводы о законности загрузки поля union и пользователь делает ошибку, поведение undefined - лучшее, что мы можем указать без указания самого IMO repr Rust.

¹: Особенно эффективно, если люди по какой-то причине начинают использовать союз как трансмутацию бедняка.

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

Я не согласен. Например, на некоторых архитектурах имеет смысл расширить материал repr(Rust) до размера машинного слова.

Одна из проблем, которую можно рассмотреть до стабилизации, - это https://github.com/rust-lang/rust/issues/37479. Похоже, что с самой последней версией LLDB отладочные соединения могут не работать :(

@alexcrichton Это работает с GDB?

Насколько я могу судить, да. Боты Linux, похоже, отлично справляются с тестом.

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

Было бы здорово посмотреть, сможем ли мы включить эту функцию в FCP для цикла 1.17 (это бета-версия от 16 марта). Может ли кто-нибудь кратко изложить нерешенные вопросы и текущую ситуацию с функцией, чтобы мы могли увидеть, сможем ли мы достичь консенсуса и все решить?

@withoutboats
Мои планы

  • Ждите грядущего релиза (3 февраля).
  • Предложите стабилизацию объединений с полями Copy . Это покроет все потребности FFI - библиотеки FFI смогут использовать союзы на стабильной основе. Объединения "POD" десятилетиями используются в C / C ++ и хорошо изучены (алиасинг на основе модульного типа, но в Rust его нет), также нет известных блокировщиков.
  • Напишите RFC "Unions 1.2" до 3 февраля. В нем будет описана текущая реализация объединений и намечены направления на будущее. Будущее объединений с полями, отличными от Copy будет решено в процессе обсуждения этого RFC.

Обратите внимание, что отображение чего-то вроде ManuallyDrop или NoDrop из стандартной библиотеки не требует стабилизирующих объединений.

ОБНОВЛЕНИЕ СТАТУСА (4 февраля): я пишу RFC, но у меня, как обычно, возникает писательский блок после каждого предложения, поэтому есть шанс, что я закончу его в следующие выходные (11-12 февраля), а не в эти выходные (4-5 февраля).
ОБНОВЛЕНИЕ СТАТУСА (11 февраля): текст готов на 95%, отправлю завтра.

@petrochenkov, что кажется вполне разумным.

@petrochenkov Мне это кажется разумным. Я также рассмотрел ваше предложение профсоюзов 1.2 и дал несколько комментариев; в целом, мне это нравится.

@joshtriplett Я подумал, что, хотя на встрече @ rust-lang / lang мы говорили о поддержании актуальности контрольных списков, я действительно хотел бы, чтобы по каждому из этих пунктов мы принимали утвердительное решение (т.е. в идеале с помощью @rfcbot). Это, вероятно, предполагает отдельную проблему (или даже поправку RFC). Мы могли бы сделать это со временем, но до тех пор я не чувствую, что мы окончательно «уладили» ответы на открытые вопросы. Таким образом, извлечение и обобщение соответствующего разговора в поправке RFC или даже просто в вопросе, на который мы можем ссылаться здесь, кажется отличным шагом для обеспечения того, чтобы все были на одной странице - и что-то, что может сделать любой, кто заинтересован. конечно, не только члены @ rust-lang / lang или пастыри.

Итак, я отправил RFC "Unions 1.2" - https://github.com/rust-lang/rfcs/pull/1897.

Теперь я хотел бы предложить стабилизацию консервативного подмножества объединения - все поля объединения должны быть Copy , количество полей должно быть ненулевым, и объединение не должно реализовывать Drop .
(Я не уверен, что последнее требование жизнеспособно, потому что его можно легко обойти, заключив объединение в структуру и реализовав Drop для этой структуры.)
Такие объединения покрывают все потребности библиотек FFI, которые должны быть основными потребителями этой языковой функции.

Текст RFC "Unions 1.2" на самом деле не говорит ничего нового о союзах в стиле FFI, за исключением того, что он явно подтверждает, что использование каламбура разрешено.
РЕДАКТИРОВАТЬ : RFC "Unions 1.2" также собирается сделать безопасными назначения тривиально разрушаемых полей Copy (см. Https://github.com/rust-lang/rust/issues/32836#issuecomment-281296416, https : //github.com/rust-lang/rust/issues/32836#issuecomment-281748451), это также влияет на союзы в стиле FFI.

Этот текст также предоставляет документацию, необходимую для стабилизации.
Раздел «Обзор» можно скопировать в книгу, а раздел «Детальный дизайн» - в ссылку.

пинг @nikomatsakis

Неужели что-то подобное нужно добавить как часть языка? Мне потребовалось около 20 минут, чтобы создать реализацию объединения, используя маленькие unsafe и ptr::write() .

use std::mem;
use std::ptr;


/// A union of `f64`, `bool`, and `i32`.
#[derive(Default, Clone, PartialEq, Debug)]
struct Union {
    data: [u8; 8],
}

impl Union {
    pub unsafe fn get<T>(&self) -> &T {
        &*(&self.data as *const _ as *const T)
    }

    pub unsafe fn set<T>(&mut self, value: T) {
        // "transmute" our pointer to self.data into a &mut T so we can 
        // use ptr::write()
        let data_ptr: &mut T = &mut *(&mut self.data as *mut _ as *mut T);
        ptr::write(data_ptr, value);
    }
}


fn main() {
    let mut u = Union::default();
    println!("data: {0:?} ({0:#p})", &u.data);
    {
        let as_i32: &i32 = unsafe { u.get() };
        println!("as i32: {0:?} ({0:#p})", as_i32);
    }

    unsafe {
        u.set::<f64>(3.14);
    }

    println!("As an f64: {:?}", unsafe { u.get::<f64>() });
}

Я чувствую, что кому-то не составит труда написать макрос, который может генерировать что-то подобное, за исключением того, что внутренний массив должен иметь размер самого большого типа. Затем вместо моего полностью общего (и ужасно опасного) get::<T>() они могли бы добавить черту, ограничивающую типы, которые вы можете получить и установить. Вы даже можете добавить определенные методы получения и установки, если хотите именованные поля.

Думаю, они могли бы написать что-то вроде этого:

union! { Foo(u64, Vec<u8>, String) };

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

@ Michael-F-Bryan У нас пока нет постоянного size_of .

@ Michael-F-Bryan Недостаточно просто иметь массив [u8] , вам также нужно добиться правильного выравнивания . Фактически я уже использую макросы для обработки объединений, но из-за отсутствия констант size_of и align_of мне приходится вручную выделять правильное пространство, плюс потому, что в декларативных макросах нет полезной конкатенации идентификаторов. необходимо вручную указать имена как для геттеров, так и для сеттеров. На данный момент сложно даже просто инициализировать объединение, потому что я должен сначала инициализировать его некоторым значением по умолчанию, а затем установить значение для варианта, который я хочу (или добавить другой набор методов для создания объединения, что является еще более подробным в определении союза). В целом это намного больше работы, подвержено ошибкам и уродливее, чем встроенная поддержка профсоюзов. Возможно, вам стоит прочитать RFC и сопутствующее обсуждение, чтобы понять, почему эта функция так важна.

То же самое и с выравниванием.

Я полагаю, что объединение идентификаторов не должно быть слишком сложным, теперь существует syn . Это позволяет вам выполнять операции с переданным AST, поэтому вы можете взять два идентификатора , извлечь их строковое представление ( Ident реализует AsRef<str> ), а затем создать новый Ident который является конкатенацией двух с использованием Ident::From<String>() .

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

Например, у вас может быть MyUnion::default() который просто обнуляет внутренний буфер объединения, затем fn MyUnion::new<T>(value:T) -> MyUnion , где T имеет границу, гарантирующую, что вы можете инициализировать только правильные типы .

С точки зрения выравнивания и размера, можете ли вы использовать модуль mem из стандартной библиотеки (т.е. std :: mem :: align_of () и других)? Я предполагаю, что все, что я предлагаю, будет зависеть от возможности использовать их во время расширения макроса, чтобы определить требуемый размер и выравнивание. В 99,9% случаев объединения используются в любом случае с примитивными типами, поэтому я чувствую, что вы могли бы написать вспомогательную функцию, которая принимает имя типа и возвращает его выравнивание или размер (возможно, запрашивая компилятор, хотя это больше деталь реализации).

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

Что касается выравнивания и размера, можете ли вы использовать модуль mem из стандартной библиотеки (т.е. std :: mem :: align_of () и других)?

Это не будет работать ни в каком контексте кросс-компиляции.

@ Michael-F-Bryan Все эти и многие другие обсуждения происходили в истории https://github.com/rust-lang/rfcs/pull/1444 . Подводя итог ответам на ваши конкретные проблемы в дополнение к уже упомянутым: вам придется повторно реализовать правила заполнения и выравнивания каждой целевой платформы / компилятора и использовать неудобный синтаксис во всем коде FFI (что на самом деле сделал @ retep998 широко для привязок Windows и может ручаться за неудобства). Кроме того, в настоящее время макросы proc работают только для наследования; вы не можете расширить синтаксис в другом месте.

Также:

В 99,9% случаев объединения используются в любом случае с примитивными типами.

Совсем неправда. В коде C широко используется шаблон «структура объединений структур», в котором большинство полей объединения состоят из различных типов структур.

@rfcbot ФКП слияния в @petrochenkov «s комментарий https://github.com/rust-lang/rust/issues/32836#issuecomment -279256434

Мне нечего добавить, просто запускаю бота

Член команды @withoutboats предложил объединить это. Следующий шаг - проверка остальными отмеченными командами:

  • [x] @aturon
  • [x] @eddyb
  • [x] @nikomatsakis
  • [x] @nrc
  • [x] @pnkfelix
  • [x] @withoutboats

В настоящее время не перечислено никаких проблем.

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

См. Этот документ для получения информации о том, какие команды могут дать мне члены команды с тегами.

PSA: я собираюсь обновить RFC "Unions 1.2" еще одним изменением, затрагивающим объединения в стиле FFI - я перенесу безопасные назначения в тривиально разрушаемые поля объединений из "Future direction" в RFC собственно.

union.trivially_destructible_field = 10; // safe

Почему:

  • Присваивания тривиально разрушаемым полям объединения безусловно безопасны, независимо от интерпретации объединений.
  • Он удалит примерно половину блоков unsafe связанных с объединением.
  • Позже это будет сложнее сделать из-за потенциально большого количества предупреждений / ошибок unused_unsafe в стабильном коде.

@petrochenkov Вы имеете в виду «тривиально разрушаемые поля объединения» или «объединения с полностью тривиально разрушаемыми полями»?

Вы предлагаете, чтобы все небезопасное поведение происходило при чтении, где вы выбираете интерпретацию? Например, если объединение содержит перечисление и другие поля, где значение объединения содержит недопустимый дискриминант?

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

Я нормально отношусь к стабилизации этого подмножества. Я пока не знаю своего мнения об этом RFC для Unions 1.2, так как у меня не было времени его прочитать! Я не уверен, что я думаю о безопасном доступе к полям в некоторых случаях. Оглядываясь назад, я чувствую, что наши попытки составить «минимальное» представление о том, что небезопасно (просто разыменование указателей), были ошибкой, и мы должны были объявить небезопасным более широкий круг вещей (например, множество приведений), поскольку они сложным образом взаимодействуют с LLVM. Я чувствую, что это может иметь место и здесь. Другими словами, я мог бы скорее отказаться от правил о unsafe вместе с большим прогрессом в рекомендациях по небезопасному коду.

@joshtriplett
"тривиально разрушаемые поля", я изменил формулировку.

Вы предлагаете, чтобы все небезопасное поведение происходило при чтении, где вы выбираете интерпретацию?

Да. Сама по себе запись не может вызвать ничего опасного без последующего чтения.

РЕДАКТИРОВАТЬ:

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

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

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

@nrc
Что ж, подмножество довольно очевидно - «союзы FFI», или «союзы C», или «союзы до C ++ 11» - даже если это не синтаксически. Моей первоначальной целью было как можно скорее стабилизировать это подмножество (в идеале - этот цикл), чтобы его можно было использовать в таких библиотеках, как winapi .
В остальном подмножестве и его реализации нет ничего особенно сомнительного, это просто не срочно и нужно ждать неясное количество времени, пока не завершится процесс для RFC «Unions 1.2». Я ожидал стабилизации оставшихся частей за 1, 2 или 3 цикла после стабилизации начального подмножества.

Думаю, у меня есть веский аргумент в пользу безопасных полевых работ.
Небезопасное присвоение поля

unsafe {
    u.trivially_destructible_field = value;
}

эквивалентно назначению безопасного полного объединения

u = U { trivially_destructible_field: value };

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

@petrochenkov Крайний момент - size_of_val(&value) == 0 , верно?

Эквивалентность двух фрагментов верна, только если рассматриваемое поле
"тривиально разрушаемо", не так ли?

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

22.02.2017 14:50, "Вадим Петроченков" [email protected]
написал:

Думаю, у меня есть веский аргумент в пользу безопасных полевых работ.
Небезопасное присвоение поля

unsafe {
u.trivially_destructible_field = значение;
}

эквивалентно назначению безопасного полного объединения

u = U {trivially_destructible_field: value};

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

-
Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment-281660298 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/AApc0lUOXLU5xNTfM5PEfEz9nutMZhXUks5rfC8UgaJpZM4IDXsj
.

@eddyb

Крайний момент - size_of_val (& value) == 0, верно?

Ага.

@nagisa
Я не понимаю, почему одно дополнительное простое правило, устраняющее большую часть ложных срабатываний, крайне непоследовательно. "только некоторые случаи" охватывают, в частности, все союзы FFI.
Я считаю, что «крайне непоследовательно» - это большое завышение. Не такой большой, как «можно присвоить только mut переменные? Ужасная несогласованность!», Но все же идет в этом направлении.

@petrochenkov, пожалуйста, рассмотрите такой случай:

// Somebody Somewhere in some crate (v 1.0.0)
struct Peach; // trivially destructible
union Banana { pub actually: Peach }

// Somebody Else in their dependent crate
extern some crate;
fn somefn(banana: &mut Banana) {
    banana.actually = Peach;
}

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

impl Drop for Peach { fn drop(&mut self) { println!("Moi Peach!") }

и выпустить версию ящика 1.1.0 (semver, совместимую с 1.0.0 AFAIK).

Внезапно ящик мистера Чужого больше не компилируется:

fn somefn(banana: &mut Banana) {
    banana.actually = Peach; // ERROR: Something something… unsafe assingment… somewhat somewhat trivially indestructible… 
}

И поэтому иногда разрешение безопасных присваиваний полям объединения не так тривиально, как только mut locals, способные к изменению.


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

@nagisa
Это хороший аргумент, о совместимости не думал.

Однако проблема кажется разрешимой. Чтобы избежать проблем с совместимостью, делайте то же, что и согласованность - избегайте негативных рассуждений. Т.е. замените "trivially-destructible" == "никакие компоненты не реализуют Drop " ближайшим положительным приближением - "реализует Copy ".
Copy Тип не может не-реализации Copy обратной совместимости, и Copy типа до сих пор представляют большинство "тривиальным-разрушаемых" типов, особенно в контексте FFI союзов.

Реализация Drop уже не имеет обратной совместимости, и это не имеет ничего общего с функцией объединения:

// Somebody Somewhere in some crate (v 1.0.0)
struct Apple; // trivially destructible
struct Pineapple { pub actually: Apple }

// Somebody Else in their dependent crate
extern some crate;
fn pineapple_to_apple(pineapple: Pineapple) -> Apple {
    pineapple.actually
}
// some crate v 1.1.0
impl Drop for Pineapple { fn drop(&mut self) { println!("Moi Pineapple!") }
fn pineapple_to_apple(pineapple: Pineapple) -> Apple {
    pineapple.actually // ERROR: can't move out of Pineapple
}

Что, в свою очередь, похоже на реализацию Drop drop неявного копирования. И скопировать
на него можно положиться.

В среду, 22 февраля 2017 г., в 10:11, jethrogb [email protected] написал:

Реализация Drop уже не имеет обратной совместимости, и это
ничего общего с функцией объединения:

// Кто-то Где-то в каком-то ящике (v 1.0.0)
struct Apple; // тривиально разрушаемый
struct Pineapple {собственно паб: Apple}

// Кто-то еще в своем зависимом ящике
внешний ящик;
fn pineapple_to_apple (ананас: ананас) -> Apple {
ананас. на самом деле
}

// какой-то ящик v 1.1.0
impl Drop for Pineapple {fn drop (& mut self) {println! ("Мой ананас!")}

fn pineapple_to_apple (ананас: ананас) -> Apple {
банан. на самом деле // ОШИБКА: невозможно выйти из ананаса
}

-
Вы получаете это, потому что подписаны на эту ветку.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/rust-lang/rust/issues/32836#issuecomment-281752949 ,
или отключить поток
https://github.com/notifications/unsubscribe-auth/ABxXhgbFgRNzYOsU4c6Gu1KFfwdjDHn3ks5rfHpYgaJpZM4IDXsj
.

@jethrogb
Я хотел упомянуть об этой проблеме, но не стал, потому что у нее есть несколько особых предварительных условий - структура, реализующая Drop должна иметь общедоступное поле, и это поле не должно быть Copy . Случай объединения безоговорочно влияет на все структуры.

@petrochenkov Считаю, что может быть аргумент о том, что создание союза небезопасно :)

@petrochenkov каковы пути развития и ночного взаимодействия с пользователем для стабилизации подмножества? Добавим ли мы сначала новые функции для подмножества, чтобы люди могли получить конкретный опыт использования подмножества, прежде чем оно стабилизируется?

@pnkfelix
Я решил просто перестать требовать #[feature(untagged_unions)] для этого подмножества, никаких новых функций или другой бюрократии.
Предполагается, что союзы в стиле FFI являются наиболее часто используемым видом союзов, поэтому новая функция будет означать гарантированный выход из строя непосредственно перед стабилизацией, что, как я полагаю, будет раздражать.

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

@ retep998
Упаковка? Если вы имеете в виду #[repr(packed)] тогда он поддерживается в союзах прямо сейчас (в отличие от атрибутов align(>1) ).

@petrochenkov #[repr(packed(N))] . В winapi требуется немного упаковки кроме 1. Дело не в том, что мне нужны эти вещи, специально поддерживаемые в объединениях, я просто не хочу переходить на новую основную версию, чтобы увеличить мои минимальные требования Rust, если я не смогу получить все это одновременно.

Чтобы немного прояснить состояние игры:

Текущее предложение FCP предназначено только для чистых союзов Copy . Насколько мне известно, в основном нет нерешенных вопросов, кроме «Должны ли мы стабилизироваться?» С момента подачи предложения в FCP все @petrochenkov .

@nrc и @nikomatsakis , из обсуждения IRC, я подозреваю, что вы оба готовы поставить галочки, но я оставлю это вам ;-)

: bell: Сейчас наступает последний период для комментариев , как указано в обзоре выше . : колокол:

@petrochenkov
Мне кажется, что мне поможет идея, которую я недавно высказал. (https://internals.rust-lang.org/t/automatic-marker-trait-for-unconditional-valid-repr-c-types/5054)

Хотя это не было предложено с учетом объединений, черта Plain как объяснялось (с учетом байкешеддинга), позволит использовать любое объединение, состоящее только из типов Plain без какой-либо опасности. Он кодифицирует свойство, что _ любой_ битовый шаблон в памяти одинаково допустим, поэтому вы можете решить проблемы инициализации, потребовав обнуления памяти, и гарантирует, что чтение не может вызывать UB.

В контексте FFI наличие Plain в соответствии с определением также является фактическим требованием для того, чтобы такой код был надежным во многих случаях, в то время как случаи, когда полезны типы, отличные от Plain , редки и сложны в настройке. безопасно.

Помимо фактического существования названной черты, было бы разумно разделить ее на две части. С неквалифицированным union обеспечивающим соблюдение требований и разрешающим использование без небезопасности, и unsafe union с ослабленными требованиями к контенту и дополнительной головной болью для пользователей. Это позволило бы стабилизировать одну из них, не преграждая путь к добавлению другой в будущем.

@ le-jzr
Это кажется достаточно ортогональным для союзов.
Я бы оценил принятие Plain в Rust в ближайшем будущем как маловероятный + создание большего количества безопасных обращений к полям union более или менее обратно совместимо (не полностью из-за линтов), поэтому я бы не стал откладывать к нему.

@petrochenkov Я не предлагаю откладывать профсоюзы в ожидании какого-либо закрытия по моему предложению, а скорее хочу рассмотреть _существование_ таких возможных ограничений само по себе. Обеспечение безопасного доступа к полям объединения в будущем может столкнуться с препятствиями, поскольку это создает большую несогласованность в языке. В частности, сделать так, чтобы безопасность доступа к полю варьировалась от поля к полю, кажется уродливым.

Поэтому я предлагаю сделать объявление unsafe union , чтобы безоговорочно безопасная для использования версия могла быть представлена ​​позже без добавления новых ключевых слов. Стоит отметить, что полностью безопасной для использования версии достаточно для большинства случаев использования.

Изменить: попытался прояснить, что я хочу сказать.

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

Последний период комментариев подошел к концу.

Теперь, когда объединение FCP завершено, что делать дальше? Было бы неплохо стабилизировать это для версии 1.19.

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

Путь к переходу в 1.19 ясен. Кто-нибудь на крючке? cc @joshtriplett

Гарантировано ли применение оптимизации макета перечисления NonZero через union ? Например, Option<ManuallyDrop<&u32>> не должно представлять None как нулевой указатель. Some(ManuallyDrop::new(uninitialized::<[Vec<Foo>; 10]>())).is_some() не должен читать неинициализированную память.

На https://crates.io/crates/nodrop (используется в https://crates.io/crates/arrayvec) есть хаки, чтобы справиться с этим.

@SimonSapin
В настоящее время этот вопрос отмечен в RFC как нерешенный.
В текущей реализации эта программа

#![feature(untagged_unions)]

struct S {
    _a: &'static u8
}
union U {
    _a: &'static u8
}

fn main() {
    use std::mem::size_of;
    println!("struct {}", size_of::<S>());
    println!("optional struct {}", size_of::<Option<S>>());
    println!("union {}", size_of::<U>());
    println!("optional union {}", size_of::<Option<U>>());
}

печатает

struct 8
optional struct 8
union 8
optional union 16

, т.е. оптимизация не производится.
копия https://github.com/rust-lang/rust/issues/36394

Вряд ли это будет 1.19.

@brson

Вряд ли это будет 1.19.

PR стабилизации объединен.

Поскольку немаркированные союзы теперь поставляются в версии 1.19 (частично с https://github.com/rust-lang/rust/pull/42068) - есть ли что-нибудь еще по этой проблеме, или мы должны закрыть?

@jonathandturner
По-прежнему существует целый мир объединений с полями, отличными от Copy !
Прогресс в основном заблокирован в пояснении / документации RFC (https://github.com/rust-lang/rfcs/pull/1897).

Был ли прогресс в объединениях с полями, отличными от Copy с августа? RFC для Unions 1.2 кажется застрявшим (я предполагаю, из-за периода имплантации?)

Разрешение типов ?Sized в объединениях - хотя бы для объединений одного типа - упростило бы реализацию https://github.com/rust-lang/rust/issues/47034 :

ржавчина
union ManuallyDrop{
значение: T
}

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

Я смотрю на код Rust, использующий union s, и понятия не имею, вызывает ли он неопределенное поведение или нет.

В ссылке [items :: unions] упоминается только:

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

Но я не могу найти определение «совместимый макет» ни в [items :: unions], ни в [type_system :: type_layout] .

Просматривая RFC, я тоже не смог найти определение «совместимого с макетом», только помаханные рукой примеры того, что должно и не должно работать в RFC 1897: Unions 1.2 (не объединено).

RFC1444: unions, по- видимому, позволяет преобразовать объединение в его варианты только при условии, что он не вызывает неопределенное поведение, но я не могу найти нигде в RFC, когда это / не так.

Являются ли правила _precise_, которые говорят мне, есть ли где-то записанное поведение для части кода, использующей объединения (и каково это определенное поведение)?

@gnzlbg В первом приближении: вы не можете получить доступ к заполнению, не можете получить доступ к перечислению, которое содержит недопустимый дискриминант, может не получить доступ к bool, который содержит значение, отличное от true или false, может не получить доступ к недопустимому или сигнализирующему значению с плавающей запятой , и несколько других подобных вещей.

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

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

Фактически, последний консенсус таков, что чтение произвольных чисел с плавающей запятой - это нормально (# 46012).

Я бы добавил еще одно требование: как исходный, так и целевой варианты объединения равны #[repr(C)] и все их поля (и рекурсивно), если они являются структурами.

@Amanieu Я поправляюсь, спасибо.

Так я предполагаю, что правила нигде не написаны?

Я смотрю, как использовать stdsimd с новым интерфейсом. Если мы не стабилизируем с его помощью некоторые дополнения, вам нужно будет использовать объединения, чтобы выполнять каламбур с некоторыми типами simd, например:

https://github.com/rust-lang-nursery/stdsimd/blob/03cb92ddce074a5170ed5e5c5c20e5fa4e4846c3/coresimd/src/x86/test.rs#L17

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

В этом отношении функция, которую вы связали, может просто использовать transmute::<__m128d, [f64; 2]> . Хотя версия объединения, возможно, лучше, по крайней мере, когда существующая трансмутация удаляется: это может быть просто A { a }.b[idx] .

@rkruppe Я заполнил короткую проблему, чтобы добавить этот ворс: https://github.com/rust-lang-nursery/rust-clippy/issues/2361

связанная вами функция может просто использовать transmute :: <__ m128d i = "8">

Я предполагаю, что правила, которые я ищу, - это когда трансмутация вызывает неопределенное поведение (так что я пойду поищу их).

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

Чтобы было ясно, transmute [_copy] не «примитивнее», чем союзы. Фактически transmute_copy - это буквально просто указатель as cast плюс ptr::read . transmute дополнительно требует mem::uninitialized (не рекомендуется) или MaybeUninitialized (объединение) или что-то в этом роде, и для повышения эффективности реализовано как внутреннее, но это тоже сводится к типу - прокалывание memcpy. Основная причина, по которой я обратил внимание на трансмутацию, заключается в том, что она более древняя и исторически чрезмерно подчеркнута, и поэтому в настоящее время у нас есть больше рецензий и фольклорных знаний, в которых особое внимание уделяется трансмутации. Реальная основополагающая концепция, которая определяет, что действительно, а что нет (и что будет описывать спецификация), заключается в том, как значения хранятся в памяти в виде байтов и какие последовательности байтов являются UB для чтения как типы.

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

fn transmute<T, U>(x: T) -> U {
    assert!(size_of::<T>() == size_of::<U>());
    let mut bytes = [0u8; size_of::<U>()];
    ptr::write(bytes.as_mut_ptr() as *mut T, x);
    mem::forget(x);
    ptr::read(bytes.as_ptr() as *const U)
}

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

Ссылка and и Unions 1.2 RFC намеренно расплывчаты по этому поводу, потому что правила преобразования в целом не установлены.
Намерение было «для repr(C) объединений см. Сторонние спецификации ABI, для repr(Rust) совместимость макетов объединений в основном не указана (если это не так)».

Не слишком ли поздно пересматривать семантику проверки отбрасывания объединений с полями отбрасывания?

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

Урезанный пример находится на https://play.rust-lang.org/?gist=607e2dfbd51f4062b9dc93d149815695&version=nightly. Идея состоит в том, что существует тип Pin<'a, T> с методом pin(&'a self) -> &'a T , безопасность которого зависит от инварианта "после вызова pin.pin() , если память, поддерживающая контакт, когда-либо будет восстановлена, тогда деструктор булавки должен быть запущен ".

Этот инвариант поддерживался Rust до добавления #[allow(unions_with_drop_fields)] и использовался ManuallyDrop https://doc.rust-lang.org/src/core/mem.rs.html#949.

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

IRC-разговор: https://botbot.me/mozilla/rust-lang/2018-02-01/?msg=96386869&page=3

Проблема с Жозефиной: https://github.com/asajeffrey/josephine/issues/52

Копия: @nox @eddyb @pnkfelix

Первоначальная проблема заключается в том, что добавление ManuallyDrop привело к тому, что Жозефина потеряла работоспособность, потому что она (довольно озорно) полагалась на постоянные заимствованные значения, не восстанавливая их резервное хранилище без предварительного запуска их деструктора.

Работа деструкторов не гарантируется. Rust не гарантирует этого. Он пытается, но, например, std::mem::forget было преобразовано в безопасную функцию.

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

Профсоюзы небезопасны в основном потому, что вы не можете знать, какая область союза действительна. У объединения не может быть автоматического Drop impl; если вам нужен такой impl, вам нужно будет написать его вручную с учетом любых средств, которые вы должны знать, допустимо ли поле union с Drop impl.

Одно уточнение: я не верю, что мы должны когда-либо разрешать объединения с полями Drop по умолчанию без, по крайней мере, линта с предупреждением по умолчанию, если не с ошибкой по умолчанию. unions_with_drop_fields не должен исчезнуть в процессе стабилизации.

РЕДАКТИРОВАТЬ: упс, не имел в виду нажать «закрыть и прокомментировать».

@joshtriplett да, Rust не гарантирует, что деструкторы будут запущены, но это случилось (до 1.19), чтобы поддерживать инвариант, что заимствованные значения будут освобождены, только если деструктор будет запущен. Это верно даже при наличии mem::forget , так как вы не можете вызвать его по постоянному заимствованному значению.

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

Было бы хорошо, если бы allow(unions_with_drop_fields) считались небезопасной аннотацией, это не было бы радикальным изменением, AFAICT, просто потребовалось бы deny(unsafe_code) для проверки allow(unions_with_drop_fields) .

@asajeffrey Я все еще пытаюсь понять Pin ... так что, если я правильно следую примеру, причина "работает" в том, что fn pin(&'a Pin<'a, T>) -> &'a T заставляет заимствование длиться как пока время жизни 'a аннотировано в типе, и это время жизни, кроме того, инвариантно.

Интересное наблюдение! Я не знал об этой уловке. Мне кажется, что это работает «случайно», то есть безопасный Rust не дает возможности предотвратить запуск деструктора, но это не делает эту часть «контракта». Примечательно, что https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html не перечисляет утечки.

ИМО, неважно, сработает он случайно или намеренно. Не было способа избежать запуска Drop с этим трюком до того, как ManuallyDrop существовал (что требует реализации небезопасного кода), и теперь мы не можем больше на это полагаться.

Добавление ManuallyDrop основном убило это аккуратное поведение Rust, и утверждение, что на него вообще не следовало полагаться, звучит для меня как циркулярная аргументация. Если бы ManuallyDrop не позволял позвонить Pin::pin , был бы другой способ сделать вызов Pin::pin необоснованным? Я так не думаю.

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

Кроме того, это палка о двух концах: каждая дополнительная гарантия, которую мы решаем предоставить, - это то, о чем должен позаботиться небезопасный код. Таким образом, обнаружение новой гарантии может с таким же успехом сломать существующий небезопасный код (сидящий молча где-то на crates.io, не зная об этом), как и включить новый небезопасный код (последнее имело место здесь).

Например, вполне возможно, что лексическое время жизни допускает небезопасный код, который нарушается нелексическим временем жизни. В настоящее время все жизненные циклы хорошо вложены, может быть, есть способ использовать это небезопасным кодом? Только нелексические жизни могут накладываться друг на друга, но ни одна из них не включается в другую. Делает ли это NLL решающим изменением? Надеюсь нет!

Если бы ManuallyDrop не позволял вызывать Pin :: pin, был бы другой способ сделать вызов Pin :: pin несостоятельным? Я так не думаю.

С кодом unsafe будет. Таким образом, объявляя этот трюк-звук Pin , вы объявляете небезопасный код unsound , который будет звучать, если мы решим, что ManuallyDrop в порядке.

Мы описали очень эргономичный способ интеграции Rust с GC. Я пытаюсь сказать, что мне кажется неправильным говорить нам, что это была просто случайность, что это сработало и что мы должны забыть об этом, когда я не могу найти ни одного Drop полей, как описано здесь @asajeffrey , и когда это действительно единственная бородавка, ломающая Жозефину.

Я буду счастлив забыть об этом, если кто-то сможет доказать, что он был ненадежным даже без ManuallyDrop .

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

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

и что мы должны забыть об этом

Я должен был прояснить, что эта часть - всего лишь мое личное чутье. Я думаю, что было бы разумно объявить это «счастливым несчастным случаем» и фактически сделать это гарантией - если мы достаточно уверены, что действительно весь другой код unsafe соблюдает эту гарантию, и что предоставление этой гарантии более важно, чем вариант использования ManuallyDrop . Это компромисс, похожий на утечку покалипсиса, когда мы не можем съесть наш пирог и получить его (у нас не может быть одновременно Rc с его текущим API и drop - на основе потоков с ограниченной областью действия; у нас не может быть одновременно ManuallyDrop и Pin ), поэтому мы должны принять решение в любом случае.

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

если мы достаточно уверены, что действительно весь другой код unsafe соблюдает эту гарантию, и что предоставление этой гарантии более важно, чем вариант использования ManuallyDrop . Это компромисс, похожий на утечку покалипсиса, когда мы не можем съесть наш пирог и получить его (у нас не может быть одновременно Rc с его текущим API и drop - на основе потоков с ограниченной областью действия; у нас не может быть одновременно ManuallyDrop и Pin ), поэтому мы должны принять решение в любом случае.

Честно говоря, я полностью согласен с этим. Обратите внимание, что если мы рассмотрим то, что @asajeffrey в конце drop .

Насколько я понимаю, предложение Алана состоит не в том, чтобы удалять ManuallyDrop , а только для того, чтобы dropck предполагал, что у него (и других объединений с полями Drop ) есть деструктор. (Эти деструкторы ничего не делают, но их существование влияет на то, какие программы dropck принимает или отклоняет.)

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

Не уверен, что это подходит, но вот моя первая попытка: глупая реализация чего-то вроде ManuallyDrop которая работает в pre- union Rust.

pub mod manually_drop {
    use std::mem;
    use std::ptr;
    use std::marker::PhantomData;

    pub struct ManuallyDrop<T> {
        data: [u8; 32],
        phantom: PhantomData<T>,
    }

    impl<T> ManuallyDrop<T> {
        pub fn new(x: T) -> ManuallyDrop<T> {
            assert!(mem::size_of::<T>() <= 32);
            let mut data = [0u8; 32];
            unsafe {
                ptr::copy(&x as *const _ as *const u8, &mut data[0] as *mut _, mem::size_of::<T>());
            }
            mem::forget(x);
            ManuallyDrop { data, phantom: PhantomData }
        }

        pub fn deref(&self) -> &T {
            unsafe {
                &*(&self.data as *const _ as *const T)
            }
        }
    }
}

(Да, мне, вероятно, придется еще немного поработать, чтобы добиться правильного выравнивания, но это также можно сделать, пожертвовав некоторыми байтами.)
Площадка, на которой показаны эти разрывы Pin : https://play.rust-lang.org/?gist=fe1d841cedb13d45add032b4aae6321e&version=nightly

Это то, что я имел в виду под обоюдоострым мечом выше - насколько я могу судить, мой ManuallyDrop соблюдает все установленные нами правила. Итак, у нас есть две части несовместимого небезопасного кода - ManuallyDrop и Pin . Кто «прав»? Я бы сказал, что Pin полагается на гарантии, которых мы никогда не давали, и, следовательно, здесь "неправильно", но это суждение, а не доказательство.

Вот это интересно. В некоторых версиях нашего закрепления Pin::pin принимает &'this mut Pin<'this, T> , но для вашего ManuallyDrop было бы разумно иметь DerefMut impl, верно ?

Вот игровая площадка, которая показывает, что @RalfJung (неудивительно) все еще ломает Pin с помощью &mut метода pin .

https://play.rust-lang.org/?gist=5057570b54952e245fa463f8d7719663&version=nightly

для вашего ManuallyDrop было бы разумно иметь имплз DerefMut, верно?

Да, я просто добавил API, который мне нужен для этого примера. Очевидное deref_mut должно работать нормально.

Насколько я понимаю, предложение Алана состоит не в том, чтобы удалять ManuallyDrop, а только для того, чтобы dropck предполагал, что он (и другие объединения с полями Drop) имеет деструктор. (Эти деструкторы ничего не делают, но их существование влияет на то, какие программы dropck принимает или отклоняет.)

Ах, я пропустил это; прости за это. Добавление следующего в мой пример продолжает работать:

    unsafe impl<#[may_dangle] T> Drop for ManuallyDrop<T> {
        fn drop(&mut self) {}
    }

Только если я удалю #[may_dangle] Rust отклонит его. Так что, по крайней мере, мы должны были бы придумать какое-то правило, которое нарушает приведенный выше код - просто сказать, что «существует некоторый код, который мы хотим, чтобы он был несовместим», - плохой вызов, потому что он заставляет практически невозможно взглянуть на какой-то код и проверить, работает ли он.


Я думаю, что меня больше всего беспокоит эта «случайная гарантия», так это то, что я не вижу ни одной веской причины, по которой это работает. То, как все устроено в Rust, удерживает это вместе, но dropck был добавлен не для предотвращения утечек, а для избежания необоснованных ссылок на мертвые данные (распространенная проблема в деструкторах). Обоснование того, что Pin работает, основано не на том, что «вот какой-то механизм в компиляторе Rust или какая-то гарантия системы типов, которая довольно четко говорит о невозможности утечки заимствованных данных» - это скорее основано на «мы очень старались, и у нас не было возможности утечки постоянно заимствованных данных, поэтому мы думаем, что это нормально». Полагаться на это для здравого смысла заставляет меня очень нервничать. EDIT: тот факт, что задействован dropck, заставляет меня еще больше нервничать, потому что в этой части компилятора есть история неприятных ошибок надежности. Причина, по которой это работает, похоже, заключается в том, что постоянные займы расходятся с безопасными drop . Это действительно похоже на «рассуждение, основанное на исчерпывающем анализе случаев того, что можно делать с постоянно заимствованными данными».

Честно говоря, можно сказать то же самое о внутренней изменчивости - бывает, что разрешение модификаций через общие ссылки действительно работает безопасно в некоторых случаях, если мы выбираем правильный API. Тем не менее, делая эту работу на самом деле требуется явная поддержка в компиляторе ( UnsafeCell ) , потому что он конфликтует с оптимизацией, и там небезопасно код , который будет звук без внутренней изменчивостью , но не звук с внутренней изменчивостью. Другое отличие состоит в том, что внутренняя изменчивость была целью дизайна с самого начала (или с самого начала - это было задолго до моего пребывания в сообществе Rust), что не относится к «постоянному заимствованию, не просачивается». И, наконец, что касается внутренней изменчивости, я думаю, что есть довольно хорошая история о том, что «совместное использование делает мутации опасными , но не невозможными , а API общих ссылок просто говорит, что вы не получаете изменчивость в целом, но не исключает разрешения дополнительных операций для конкретных виды », в результате чего получается целостная общая картина. Конечно, я потратил много времени на размышления об общих ссылках, так что, возможно, есть столь же связная картина по рассматриваемой проблеме, о которой я просто не знаю.

Часовые пояса - это весело, я только встал! Кажется, здесь есть две проблемы (инварианты в целом и dropck в частности), поэтому я помещу их в отдельные комментарии ...

@RalfJung : да, это проблема инвариантов, поддерживаемых небезопасным Rust. Для любой версии Rust + std существует более одного варианта инварианта I который поддерживается с использованием аргументации rely-Guarantee. И действительно, могут быть две библиотеки L1 и L2 , которые выбрали несовместимые I1 и I2 , так что Rust + L1 безопасен и Rust + L2 безопасен, но Rust + L1 + L2 небезопасен.

В этом случае L1 - это ManuallyDrop а L2 - Josephine , и совершенно очевидно, что ManuallyDrop выиграет, так как это сейчас в std , который имеет гораздо более сильные ограничения обратной совместимости, чем у Жозефины.

Интересно, что рекомендации на https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html написаны следующим образом: «При написании небезопасного кода ответственность за то, чтобы безопасный код был недоступен, возлагается на программиста. демонстрируют следующее поведение: ... "то есть это контекстное свойство (для всех безопасных контекстов C, C [P] не может пойти не так) и поэтому зависит от версии (поскольку v1.20 Rust + std имеет более безопасный контексты, чем v1.18). В частности, я бы сказал, что закрепление действительно удовлетворяло этому ограничению для Rust до 1.20, поскольку не было безопасного контекста C st C [закрепление] идет не так.

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

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

Конкретная вещь, которую делают немаркированные союзы (и, следовательно, ManuallyDrop ), заключалась во взаимодействии с функцией проверки перетаскивания, в частности ManualDrop действует так, как его defn:

unsafe impl<#[may_dangle] T> Drop for ManuallyDrop<T> { ... }

а затем вы можете поговорить о том, разрешено ли это или нет :) Действительно, этот разговор происходит в потоке may_dangle начиная с https://github.com/rust-lang/rust/issues/ 34761 # issuecomment -362375924

@RalfJung ваш код показывает интересный угловой случай, когда тип времени выполнения для data равен T , но его тип времени компиляции - [u8; N] . Какой тип считается may_dangle ?

Интересно, что рекомендации на https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html написаны следующим образом: «При написании небезопасного кода ответственность за то, чтобы безопасный код был недоступен, возлагается на программиста. демонстрируют следующее поведение: ... "то есть это контекстное свойство

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

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

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

Например, пределы неопределенного поведения и того, что может делать небезопасный код, не совпадают. См. Https://github.com/nikomatsakis/rust-memory-model/issues/44 для недавнего обсуждения этой темы: Дублирование &mut T для mem::size_of::<T>() == 0 не приводит к неопределенному поведению прямо, но явно считается незаконным для небезопасного кода. Причина в том, что другой небезопасный код может полагаться на соблюдение своей дисциплины владения, а дублирование вещей нарушает эту дисциплину.

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

О, конечно. И мне интересно, что мы можем сделать, чтобы этого избежать в будущем? Может быть, поместите какое-нибудь большое предупреждение на https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html, сказав: «То, что инвариант сохраняется в rustc + libstd, не означает, что небезопасный код может полагаться на него; вместо этого вот некоторые инварианты, на которые вы можете положиться "?

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

Единственное, о чем я прошу, это, пожалуйста, не могли бы мы дать себе немного места для маневра и определить два инварианта для обоснования гарантий (код может полагаться на R и должен гарантировать G, где G подразумевает R). Таким образом, есть место для усиления R и ослабления G. Если у нас есть только один инвариант (например, R = G), мы никогда не сможем их изменить!

В настоящее время постоянная проверка не требует специальных полей объединения: (cc @solson @ oli-obk)

union Transmute<T, U> { from: T, to: U }

const SILLY: () = unsafe {
    (Transmute::<usize, Box<String>> { from: 1 }.to, ()).1
};

fn main() {
    SILLY
}

Приведенный выше код вызывает ошибку оценки miri "вызов неконстантной fn std::ptr::drop_in_place::<(std::boxed::Box<std::string::String>, ())> - shim(Some((std::boxed::Box<std::string::String>, ()))) ".

Измените его так, чтобы тип .to наблюдался константной проверкой:

const fn id<T>(x: T) -> T { x }

const SILLY: () = unsafe {
    (id(Transmute::<usize, Box<String>> { from: 1 }.to), ()).1
};

приводит к выражению «деструкторы не могут быть оценены во время компиляции».

Соответствующий код реализации находится здесь (в частности, вызов restrict ):
https://github.com/rust-lang/rust/blob/5e4603f99066eaf2c1cf19ac3afbac9057b1e177/src/librustc_mir/transform/qualify_consts.rs#L557

Более точный анализ # 41073 показал, что семантика, когда деструкторы запускаются при назначении подполям объединений, недостаточно готовы для стабилизации. См. Подробности в этой проблеме.

Реально ли полностью исключить типы Drop в объединениях и реализовать ManuallyDrop отдельно (как lang-item)? Насколько я могу судить, ManuallyDrop кажется самой большой мотивацией для Drop в профсоюзах, но это очень особый случай.

При отсутствии положительного признака «без выпадения» мы могли бы сказать, что объединение правильно сформировано, если каждое поле имеет либо Copy либо форму ManuallyDrop<T> . Это было бы полностью боковой шаг всех осложнений вокруг сбросив при назначении союзных полей (где это кажется каждый из возможных решений будут полны удивительных footguns), и ManuallyDrop явного маркер программистов , что они должны обрабатывать Drop сами здесь. (Проверка могла бы быть более умной, например, она могла бы проходить через типы продуктов и номинальные типы, объявленные в одном ящике. Конечно, положительный способ сказать «этот тип никогда не будет реализовывать Drop » будет лучше.)


В контрольном списке в первом посте не упоминаются безразмерные союзы, как и в RFC, но все же у нас есть реализация , ограниченная одновариантными союзами. Это тесно связано с взаимодействием с оптимизацией макета, потому что оно предполагает (как только DST с тонким указателем входят в изображение), что одновариантное объединение должно быть в некотором смысле «действительным» (его можно отбросить, но нельзя любой нечетный битовый шаблон).

Это противоречит тому, как объединения иногда используются в C, который является «точкой расширения» (IIRC @joshtriplett был единственным, кто упомянул об этом всеми руками): файл заголовка может объявлять 3 варианта объединения, но это считается прямой совместимой с добавлением дополнительных вариантов позже (если это не увеличивает размер объединения). Пользователь библиотеки обещает не трогать данные объединения, если тег (расположенный где-то еще) указывает, что он не знает текущий вариант. Кардинально, если вы знаете только один вариант, это не означает , что существует только один вариант!

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

Этот предикат уже существует, но он консервативен в обобщениях из-за отсутствия признака, который можно было бы привязать.
Вы можете получить к нему доступ через std::mem::needs_drop (который использует встроенную функцию, которую реализует rustc ).

@eddyb будет needs_drop принимать во внимание прямую совместимость или с радостью Drop ? Цель здесь состоит в том, чтобы иметь проверку, которая никогда не прервется из-за изменений, совместимых с semver, например, добавление impl Drop в структуру без параметров типа или времени жизни, и только частные поля совместимы с semver.

@RalfJung

Это противоречит тому, как объединения иногда используются в C, который является «точкой расширения» (IIRC @joshtriplett был единственным, кто упомянул об этом всеми руками): файл заголовка может объявлять 3 варианта объединения, но это считается прямой совместимой с добавлением дополнительных вариантов позже (если это не увеличивает размер объединения). Пользователь библиотеки обещает не трогать данные объединения, если тег (расположенный где-то еще) указывает, что он не знает текущий вариант. Важно отметить, что если вы знаете только один вариант, это не значит, что существует только один вариант!

Это очень специфический случай.
Это влияет только на объединения в стиле C (так что нет деструкторов, и все - Copy , в точности то подмножество, которое доступно в стабильной версии), созданное из заголовков C.
Мы можем легко добавить к таким объединениям поля _dummy: () или _future: () и по умолчанию продолжать пользоваться более безопасной моделью "enum". Профсоюз FFI, являющийся «точкой расширения», в любом случае должен быть хорошо задокументирован.

17 апреля 2018 г., 10:08:54 PDT, Вадим Петроченков [email protected] написал:

Мы можем легко добавить к таким объединениям поле _dummy: () или _future: ()
и по-прежнему пользуйтесь преимуществами нашей более безопасной модели "enum".

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

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

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

Мы обязательно должны задокументировать семантику как можно точнее.

@RalfJung Нет, он ведет себя как auto trait s, показывая все внутренние детали.

В настоящее время ведется обсуждение «активных полей» и объединения союзов на https://github.com/rust-lang/rust/issues/41073#issuecomment -380291471

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

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

Для меня, если вы хотите отбросить содержимое объединения, вам нужно ~ нужно преобразовать / преобразовать (возможно, его нельзя преобразовать, потому что он может быть больше с некоторыми неиспользованными битами в конце для другого варианта) его к типу вы хотите отбросить ~ взять указатели на поля, которые нужно удалить, и использовать std::ptr::drop_in_place или использовать синтаксис поля для извлечения значения.

Если бы я ничего не знал о союзах, я бы ожидал, что они будут работать:

Пример - представление mem::uninitialized в виде объединения

pub union MaybeValid<T> {
    valid: T,
    invalid: ()
}

impl<T> MaybeValid<T> {
    #[inline] // this should optimize to a no-op
    pub fn from_valid(valid: T) -> MaybeValid<T> {
        MaybeValid { valid }
    }

    pub fn invalid() -> MaybeValid<T> {
        MaybeValid { invalid: () }
    }

   pub fn zeroed() -> MaybeValid<T> {
        // do whatever is necessary here...
        unimplemented!()
    }
}

fn example() {
    let valid_data = MaybeValid::from_valid(1_u8);
    // Destructor of a union always does nothing, but that's OK since our 
    // data type owns nothing.
    drop(valid_data);
    let invalid_data = MaybeValid::invalid();
    // Destructor of a union again does nothing, which means it needs to know 
    // nothing about its surroundings, and can't accidentally try to free unused memory.
    drop(invalid_data);
    let valid_data = MaybeValid::from_valid(String::from("test string"));
    // Now if we dropped `valid_data` we would leak memory, since the string 
    // would never get freed. This is already possible in safe rust using e.g. `Rc`. 
    // `union` is a similarly advanced feature to `Rc` and so new users are 
    // protected by the order in which concepts are introduced to them. This is 
    // still "safe" even though it leaks because it cannot trigger UB.
    //drop(valid_data)
    // Since we know that our union is of a particular form, we can safely 
    // move the value out, in order to run the destructor. I would expect this 
    // to fail if the drop method had run, even though the drop method does 
    // nothing, because that's the way stuff works in rust - once it's dropped
    // you can't use it.
    let _string_to_drop = unsafe { valid_data.valid };
    // No memory leak and all unsafety is encapsulated.
}

Я собираюсь опубликовать это, а затем отредактировать, чтобы не потерять работу.
ИЗМЕНИТЬ @SimonSapin способ удаления полей.

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

(Если нужно только позволить ему упасть, нет необходимости извлекать значение в смысле его перемещения, вы можете взять указатель на одно из полей и использовать std::ptr::drop_in_place .)

Связано: для констант я сейчас утверждаю, что по крайней мере одно поле объединения внутри константы должно быть правильным: https://github.com/rust-lang/rust/pull/51361 (если у вас есть поле ZST, которое всегда правда)

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

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

@derekdreery (и все остальные) Мне были бы интересны ваши отзывы о https://internals.rust-lang.org/t/pre-rfc-unions-drop-types-and-manuallydrop/8025

Связано: для констант я сейчас утверждаю, что по крайней мере одно поле объединения внутри константы должно быть правильным: # 51361

Я видел реализацию, но не видел аргумента. ;)

Что ж ... аргумент «совсем не проверять казался странным».

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

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

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

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

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

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

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

Кроме того , это делает union простой. Я думаю, что простота важна и даже важнее, когда речь идет о небезопасном коде (что всегда будет иметь место с союзами). Здесь мы должны мириться с дополнительными сложностями, только если они принесут ощутимые выгоды.

Мы не думаем, что должны платить эту стоимость за союзы, поскольку модель мешка битов не дает никаких новых возможностей по сравнению с моделью enum-with-unknown-option.

Собственность, о которой идет речь здесь, является как минимум бременем для небезопасного кода, так и защитой. Не существует статического анализа, который может предотвратить все ошибки, которые могут нарушить это свойство, поскольку мы действительно хотим использовать объединения для небезопасного типа punning 1 , поэтому «перечисление с неизвестным вариантом» действительно означает, что объединения, обрабатывающие код, должны быть очень осторожны с тем, как он записывает объединение или риск мгновенного UB, без реального снижения небезопасности, связанной с чтением из объединения, поскольку чтение уже требует знания (через каналы, которые компилятор не понимает), что биты действительны для варианта, который вы читаете. На самом деле мы можем только предупредить пользователей о том, что объединение недействительно для любого из его вариантов, только при работе под miri, а не в подавляющем большинстве случаев, когда это происходит во время выполнения.

1 Например, если для простоты кортежи представляют собой repr (C), union Foo { a: (bool, u8), b: (u8, bool) } позволяет создать что-то недействительное только путем присвоения полей.

@rkruppe

объединение Foo {a: (bool, u8), b: (u8, bool)}

Эй, это мой пример :)
И это действительно в рамках модели RFC 1897 (хотя бы один из "листовых" фрагментов bool -1, u8 -1, u8 -2, bool -2 действительно после любых частичных присвоений).

Объединения обработки кода должны быть очень осторожны с тем, как он пишет в объединение, иначе рисковать мгновенным UB

В этом суть модели RFC 1897: статическая проверка гарантирует, что никакая безопасная операция (например, присваивание или частичное присваивание) не может превратить объединение в недопустимое состояние, поэтому вам не нужно быть очень осторожным все время и не получить мгновенный UB .
Только небезопасные операции, не связанные с объединением, такие как запись через дикие указатели, могут сделать объединение недействительным.

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

let u: Union;
let x = u.field; // UB

В этом суть модели RFC 1897, статическая проверка гарантирует, что никакая безопасная операция (например, присваивание или частичное присваивание) не может превратить объединение в недопустимое состояние, поэтому вам не нужно быть очень осторожным все время и не получить мгновенный UB .
Только небезопасные операции, не связанные с объединением, такие как запись через дикие указатели, могут сделать объединение недействительным.

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

Как более изящная проверка типов повлияет на отбрасывание? Если вы создаете объединение, а затем передаете его C, который становится владельцем, будет ли Rust пытаться освободить данные, возможно, вызывая двойное освобождение? Или вы всегда будете реализовывать Drop самостоятельно?

отредактировать было бы здорово, если бы объединения были похожи на «перечисления, в которых вариант проверяется статически во время компиляции», если я понял предложение

edit 2 могут ли союзы начинаться как набор битов, а затем обеспечивать безопасный доступ, сохраняя при этом обратную совместимость?

И это действительно в соответствии с моделью RFC 1897 (по крайней мере, один из "листовых" фрагментов bool-1, u8-1, u8-2, bool-2 действителен после любых частичных присваиваний).

Если мы решим, что мы хотим, чтобы это было действительным, я думаю, что @ oli-obk должен обновить проверки miri, чтобы отразить это - при объединении https://github.com/rust-lang/rust/pull/51361 он будет отклонен мири.

@petrochenkov Я не понимаю, что это нам дает. Мы получаем дополнительную сложность с точки зрения реализации (статический анализ) и использования (пользователь по-прежнему должен знать точные правила). Эта дополнительная сложность приводит к тому, что когда используются объединения, мы уже находимся в небезопасном контексте, поэтому вещи, естественно, становятся более сложными. Я думаю, у нас должна быть четкая мотивация того, почему эта дополнительная сложность того стоит. Я не считаю "это несколько нарушает дух языка" явной мотивацией.

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

Думаю, мне здесь не хватает чего-то принципиального. Я согласен с @rkruppe, что
сложная проблема профсоюзов - убедиться, что профсоюз в настоящее время хранит
данные, которые программа хочет прочитать.

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

Итак ... есть ли решение этой проблемы? Или что значит
предлагаемые точные решения действительно нас купят? Скажем, я получаю союз от C,
без анализа всей программы на Rust и C, что может предложить
статический анализ на самом деле гарантия для читателей?

@gnzlbg Я думаю, что единственная гарантия, которую мы получим, - это то, что @petrochenkov написал выше

статическая проверка гарантирует, что никакая безопасная операция (например, присвоение или частичное присвоение) не может превратить объединение в недопустимое состояние

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

Ваше предложение также не защищает от плохого чтения, я не думаю, что это возможно.

Кроме того, я представил себе простейшее «инициализированное» отслеживание типа «запись в любое поле инициализирует объединение». Нам все равно что-то понадобится, когда разрешено impl Drop for MyUnion . Хорошо это или плохо, но мы должны решить, когда и где вставлять автоматические отбрасывающие вызовы для объединения. Эти правила должны быть максимально простыми, поскольку это дополнительный код, который мы вставляем в существующий тонкий небезопасный код. Для союзов, которые реализуют Drop , я также представил ограничение, подобное struct которое не разрешает запись в поле, если структура данных уже не инициализирована.

@derekchiang

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

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

Например, один из вариантов использования для этого: рассмотрим объединение с тегами в стиле C, которое в будущем может быть расширено с помощью дополнительных тегов. Код C и Rust, читающий это объединение, не должен предполагать, что он знает все возможные типы полей.

@RalfJung

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

Должен ли этот код работать 1) для союзов 2) для не-союзов?

let x: T;
let y = x.field;

Для меня ответ очевиден «нет» в обоих случаях, потому что это целый класс ошибок, которые Rust может и хочет предотвратить, независимо от «объединяемости» T .

Это означает, что программа проверки ходов должна иметь некую схему, в соответствии с которой она реализует эту поддержку. Учитывая, что средство проверки перемещения (и средство проверки заимствований) обычно работают для каждого поля, простейшей схемой для объединений были бы «те же правила, что и для структур + (де) инициализация / заимствование поля также (де) инициализирует / заимствует его родственные поля ".
Это простое правило охватывает все статические проверки.

Тогда модель enum - это просто следствие описанной выше статической проверки + еще одно условие.
Если 1) проверка инициализации включена и 2) небезопасный код не записывает произвольные недопустимые байты в область, принадлежащую объединению, то одно из полей "листа" объединения автоматически становится действительным. Это динамическая непроверяемая гарантия (по крайней мере, для объединений с> 1 полями и вне констант-оценщика), но она нацелена в первую очередь на людей, читающих код.

Этот кейс от @joshtriplett , например

Например, один из вариантов использования для этого: рассмотрим объединение с тегами в стиле C, которое в будущем может быть расширено с помощью дополнительных тегов. Код C и Rust, читающий это объединение, не должен предполагать, что он знает все возможные типы полей.

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

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

@petrochenkov

Должен ли этот код работать 1) для союзов 2) для не-союзов?

let x: T;
let y = x.field;

Для меня ответ очевиден «нет» в обоих случаях, потому что это целый класс ошибок, которые Rust может и хочет предотвратить, независимо от «объединяемости» T .

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

Это означает, что программа проверки ходов должна иметь некую схему, в соответствии с которой она реализует эту поддержку. Учитывая, что средство проверки перемещения (и средство проверки заимствований) обычно работают для каждого поля, простейшей схемой для объединений были бы «те же правила, что и для структур + (де) инициализация / заимствование поля также (де) инициализирует / заимствует его родственные поля ".
Это простое правило охватывает все статические проверки.

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

Тогда модель enum - это просто следствие описанной выше статической проверки + еще одно условие.
Если 1) проверка инициализации включена и 2) небезопасный код не записывает произвольные недопустимые байты в область, принадлежащую объединению, то одно из полей "листа" объединения автоматически становится действительным. Это динамическая непроверяемая гарантия (по крайней мере, для объединений с> 1 полями и вне констант-оценщика), но она нацелена в первую очередь на людей, читающих код.

Это дополнительное условие не действует для профсоюзов.

Этот кейс от @joshtriplett , например

Например, один из вариантов использования для этого: рассмотрим объединение с тегами в стиле C, которое в будущем может быть расширено с помощью дополнительных тегов. Код C и Rust, читающий это объединение, не должен предполагать, что он знает все возможные типы полей.

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

Профсоюзы C работают не так и не так работают профсоюзы Rust. (И я бы спросил, будет ли это яснее или просто соответствует другому набору ожиданий.) Если это изменить, союзы Rust больше не будут подходить для некоторых целей, для которых они были разработаны и предложены.

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

Эти «небезопасные третьи стороны» означают «получение союза от FFI», что является вполне допустимым вариантом использования.

Вот конкретный пример:

union Event {
    event_id: u32,
    event1: Event1,
    event2: Event2,
    event3: Event3,
}

struct Event1 {
    event_id: u32, // always EVENT1
    // ... more fields ...
}
// ... more event structs ...

match u.event_id {
    EVENT1 => { /* ... */ }
    EVENT2 => { /* ... */ }
    EVENT3 => { /* ... */ }
    _ => { /* unknown event */ }
}

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

@petrochenkov

Должен ли этот код работать 1) для союзов 2) для не-союзов?
Для меня ответ очевиден «нет» в обоих случаях, потому что это целый класс ошибок, которые Rust может и хочет предотвратить, независимо от «объединяемости» T.

Хорошо для меня.

простейшей схемой для объединений будет «те же правила, что и для структур + (де) инициализация / заимствование поля также (де) инициализация / заимствование его родственных полей».

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

Если 1) проверка инициализации включена и 2) небезопасный код не записывает произвольные недопустимые байты в область, принадлежащую объединению, то одно из полей "листа" объединения автоматически становится действительным. Это динамическая непроверяемая гарантия (по крайней мере, для объединений с> 1 полями и вне констант-оценщика), но она нацелена в первую очередь на людей, читающих код.

Я думаю, что крайне желательно, чтобы базовые допущения валидности были динамически проверяемыми (с учетом информации о типе). Затем мы можем проверять их во время CTFE в miri, мы можем даже проверять их во время «полных» запусков miri (например, набора тестов), в конечном итоге мы можем получить какой-то дезинфицирующий агент или, возможно, режим, в котором Rust выдает debug_assert! в критических местах для проверки инвариантов действительности.
Я думаю, что опыт использования непроверяемых правил C дает достаточно доказательств того, что они проблематичны. Обычно первый шаг к тому, чтобы действительно понять и прояснить, что такое правила, - это найти динамически проверяемый способ их выражения. Даже для моделей параллельной памяти появляются «динамически проверяемые» варианты (операционная семантика, объясняющая все с точки зрения пошагового выполнения виртуальной машины), которые кажутся единственным способом решения давних открытых проблем аксиоматики. модели, которые использовались ранее (ключевое слово здесь - «проблема разреженного воздуха»).

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

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

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

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

@joshtriplett

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

Модель, предложенная @petrochenkov, допускает эти варианты использования, добавляя к объединению поле __non_exhaustive: () . Однако я не думаю, что это должно быть необходимо. Возможно, генераторы привязки могли бы добавить такое поле.

@RalfJung

Это динамическая непроверяемая (по крайней мере, для объединений с> 1 полями и вне констант-оценщика) гарантия

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

Уточнение: я имел в виду не отмеченный в «по умолчанию» / «в режиме выпуска», конечно, это можно проверить в «медленном режиме» с некоторыми дополнительными инструментами, но вы уже писали об этом лучше, чем я.

@RalfJung

Модель, предложенная @petrochenkov, допускает эти варианты использования, добавляя в объединение поле __non_exhaustive: ().

Да, я понял, что это было предложение.

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

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

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

@joshtriplett

основные варианты использования союзов

Для меня совсем не очевидно, почему это основной вариант использования.
Это может быть верно для объединений repr(C) если вы предположите, что все виды использования объединений для тегированных объединений / "эмуляции перечисления Rust" в FFI предполагают расширяемость (что неверно), но из того, что я видел, использование repr(Rust) союзы (управление отбрасыванием, управление инициализацией, преобразование) не ожидают внезапного появления в них "неожиданных вариантов".

@petrochenkov я не сказал «сломать основной случай использования», я сказал «сломать случаи первичного использования». FFI - один из основных вариантов использования союзов.

и возьмите объединение (хе;)) всех этих наборов

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

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

union F {
  x: (u8, bool),
  y: (bool, u8),
}
fn foo() -> F {
  let mut f = F { x: (5, false) };
  unsafe { f.y.1 = 17; }
  f
}

На самом деле я думаю, что это ошибка, что для этого даже требуется unsafe .

Итак, объединение нужно брать хотя бы побайтно.
Кроме того, я не думаю, что «привлекательная очевидность» сама по себе является достаточно веской причиной. Любой инвариант, который мы выбираем, является серьезным бременем для авторов небезопасного кода, мы должны иметь конкретные преимущества, которые мы получаем в свою очередь.

@RalfJung

На самом деле я думаю, что это ошибка, которая требует даже небезопасности.

Я не знаю о новой реализации средства проверки безопасности на основе MIR, но в старом варианте на основе HIR это определенно было ограничение / упрощение средства проверки - только выражения вида expr1.field = expr2 были проанализированы на предмет возможного поля " присвоение «небезопасный отказ», все остальное консервативно рассматривалось как общий «полевой доступ», небезопасный для профсоюзов.

Отвечая на комментарий в https://github.com/rust-lang/rust/issues/52786#issuecomment -408645420:

Идея состоит в том, что компилятор по-прежнему ничего не знает о Wrap<T> и, например, не может выполнять оптимизацию макета. Хорошо, эта позиция понятна.
Это означает, что внутри модуля Wrap реализация модуля Wrap<T> может, например, временно записывать в него «неожиданные значения», если они не передаются пользователям, и компилятор с ними согласится.

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

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

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

Это означает, что если объединение имеет одно частное поле, то его разработчик (но не компилятор) может предположить, что никакая третья сторона не запишет в это объединение неожиданное значение.
Это в некотором смысле «пункт документации объединения по умолчанию» для пользователя:
- (По умолчанию) Если у объединения есть личное поле, вы не можете записывать в него мусор.
- В противном случае вы можете записывать мусор в объединение, если это явно не запрещено документами.

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

@RalfJung
Это точно описывает вашу позицию?

Как обрабатываются подобные сценарии?

mod m {
    union MyPrivateUnion { /* private fields */ }
    extern {
        fn my_private_ffi_function() -> MyPrivateUnion; // Can return garbage (?)
    }
}

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

Нет, я не это имел в виду.

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

  • «Инвариант уровня макета» (или «синтаксический инвариант») типа полностью определяется синтаксической формой типа. Это такие вещи, как « &mut T не равно NULL и выровнено», « bool is 0 или 1 », « ! не может существовать". На этом уровне *mut T совпадает с usize - оба допускают любое значение (или, может быть, любое инициализированное значение, но это различие для другого обсуждения). В конечном итоге у нас будет документ, описывающий эти инварианты для всех типов с помощью структурной рекурсии: инвариант структуры на уровне макета состоит в том, что все ее поля имеют свой инвариант и т. Д. Видимость здесь не играет роли.
Violating the layout-level invariant is instantaneous UB. This is a statement we can make because we have defined this invariant in very simple terms, and we make it part of the definition of the language itself. We can then exploit this UB (and we already do), e.g. to perform enum layout optimizations.
  • «Пользовательский инвариант уровня типа» (или «семантический инвариант») типа выбирается тем, кто реализует этот тип. Компилятор не может знать этот инвариант, поскольку у нас нет языка для его выражения, и то же самое касается определения языка. Мы не можем добиться нарушения этого инварианта UB, поскольку мы даже не можем сказать, что это за инвариант! Тот факт , что это вообще возможно , чтобы иметь собственные инварианты является особенностью любой полезной системы типа: Абстракция. Я писал об этом больше в прошлой записи блога .

    Связь между индивидуальным семантическим инвариантом и UB заключается в том, что мы заявляем, что небезопасный код может полагаться на то, что его семантические инварианты сохраняются внешним кодом . Это делает неправильным просто помещать случайные вещи в поле размера Vec . Обратите внимание, что я сказал неправильно (я иногда использую термин « ненадежный» ), но не неопределенное поведение! Другой пример, демонстрирующий это различие (на самом деле, тот же пример), - обсуждение правил псевдонима для &mut ZST . Создание висящего хорошо выровненного ненулевого &mut ZST никогда не является непосредственным UB, но оно все равно неверно / ненадежно, потому что можно написать небезопасный код, который полагается, что этого не произойдет.

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


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

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

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

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

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


Это означает, что внутри модуля Wrap реализация Wrapмодуль может, например, временно записывать в него «неожиданные значения», если он не передает их пользователям, и компилятор с ними согласится.

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

let sz = self.size;
self.size = 1337;
self.size = sz;

а UB нет.


mod m {
    union MyPrivateUnion { /* private fields */ }
    extern {
        fn my_private_ffi_function() -> MyPrivateUnion; // Can return garbage (?)
    }
}

С точки зрения инварианта синтаксического макета my_private_ffi_function может делать что угодно (при условии совпадения ABI вызова функции и подписи). Что касается семантического настраиваемого инварианта, который не виден в коде - тот, кто написал этот модуль, имел в виду инвариант, он должен задокументировать его рядом с определением объединения, а затем убедиться, что функция FFI возвращает значение, которое удовлетворяет инварианту. .

Наконец, я написал это сообщение в блоге о том, нужно ли и когда инициализировать &mut T , и о двух типах инвариантов, о которых я упоминал выше.

Осталось ли здесь что-то отслеживать, что еще не описано в https://github.com/rust-lang/rust/issues/55149 , или мы должны закрыть?

E0658 по-прежнему указывает здесь:

ошибка [E0658]: объединения с полями, отличными от Copy , нестабильны (см. проблему №32836)

В настоящее время это ужасно играет с атомами, поскольку они не реализуют Copy . Кто-нибудь знает обходной путь?

Когда https://github.com/rust-lang/rust/issues/55149 будет реализован, вы сможете использовать ManuallyDrop<AtomicFoo> в объединении. До тех пор единственный обходной путь - использовать Nightly (или не использовать union и найти альтернативу).

Если это реализовано, вам даже не понадобится ManuallyDrop ; в конце концов, rustc знает, что Atomic* не реализует Drop .

Поручаю переключить проблему отслеживания на новую.

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