Безопасно ли клонировать стертый тип Ar c через необработанный указатель? - PullRequest
5 голосов
/ 09 января 2020

Я нахожусь в ситуации, когда я работаю с данными, обернутыми в Arc, и иногда мне приходится использовать into_raw, чтобы получить необработанный указатель на основные данные. Мой вариант использования также требует стирания типа, поэтому необработанный указатель часто приводится к *const c_void, а затем возвращается к соответствующему конкретному типу при воссоздании Arc.

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

pub unsafe fn clone_raw(handle: *const c_void) -> *const c_void {
    let original = Arc::from_raw(handle);
    let copy = original.clone();
    mem::forget(original);
    Arc::into_raw(copy)
}

Есть ли что-то, что я пропускаю, что сделало бы это на самом деле небезопасным? Кроме того, я предполагаю, что ответ будет относиться и к Rc, но если есть какие-либо различия, пожалуйста, дайте мне знать!

1 Ответ

4 голосов
/ 10 января 2020

Это почти всегда небезопасно.

Arc<T> - это просто указатель на выделенную кучу структуру, которая примерно выглядит как

struct ArcInner<T: ?Sized> {
    strong: atomic::AtomicUsize,
    weak: atomic::AtomicUsize,
    data: T,  // You get a raw pointer to this element
}

into_raw() дает указатель на элемент data. Реализация Arc::from_raw() принимает такой указатель, предполагает , что это указатель на data -элемент в ArcInner<T>, возвращается в память и предполагает , чтобы найти ArcInner<T> там. Это предположение зависит от структуры памяти T, в частности от ее выравнивания и, следовательно, от точного размещения в ArcInner.

Если вы вызываете into_raw() на Arc<U>, а затем вызываете from_raw() как если это было Arc<V>, где U и V отличаются выравниванием, вычисление смещения, где U / V находится в ArcInner, будет неправильным, и вызов .clone() повредит структура данных. Разыменование T поэтому не требуется для запуска небезопасной памяти .

На практике это может не быть проблемой: поскольку data является третьим элементом после двух usize -элементов большинство T, вероятно, будут выровнены таким же образом. Однако, если реализация stdlib изменится или вы в конечном итоге скомпилируете для платформы, где это предположение неверно, реконструкция Arc<V>::from_raw, созданного Arc<U>, где расположение памяти в V и U будет другим, будет быть небезопасным и безумным sh.


Обновление:

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

#[repr(align(32))]
struct Foo;

let foo = Arc::new(Foo);

В этом примере Foo будет выровнен до 32 байтов, делая размер ArcInner<Foo> 32 байта (8 + 8 + 16 + 0), в то время как ArcInner<()> всего 16 байтов (8 + 8 + 0 + 0). Поскольку невозможно определить, каково выравнивание T после стирания типа, невозможно восстановить действительный Arc.

Существует аварийный люк, который может быть безопасным в практика: Оборачивая T в другой Box, раскладка ArcInner<T> всегда одинакова. Чтобы навязать это любому пользователю, вы можете сделать что-то вроде

struct ArcBox<T>(Arc<Box<T>>)

и реализовать на нем Deref. Использование ArcBox вместо Arc заставляет расположение памяти ArcInner всегда оставаться одинаковым, потому что T находится позади другого указателя. Это, однако, означает, что любой доступ к T требует двойной разыменования, что может негативно повлиять на производительность.

...