- PVSM.RU - https://www.pvsm.ru -
Язык программирования Rust [1], невзирая на всеохватывающую идеологию безопасности данных, располагает и небезопасными методиками программирования, ведь порой они могут повышать скорость путём устранения лишних вычислений, а порой это просто жизненная необходимость.
Одной из них является наш сегодняшний экземпляр — интринсика mem::transmute<T, U>
[2], предназначенная для того и другого понемногу, пригождаясь в крайне необычных ситтуациях.
mem::transmute<T, U>
реализована непосредственно компилятором, так как по определению не может быть описана синтаксическими средствами языка Rust. Она попросту реинтерпретирует биты одного типа данных как биты другого:
pub unsafe extern "rust-intrinsic" fn transmute<T, U>(e: T) -> U
Перед реинтерпретацией данная функция берёт на себя владение реинтерпретируемой переменной типа данных T
, а после будто "забывает" её (но без вызова соответствующего деструктора).
Никакого копирования в физической памяти не происходит, ибо данная интринсика просто даёт понять компилятору, что содержимое типа T
— это, на самом деле, тип U
.
Ошибка компиляции возникнет в том случае, если типы T
и U
имеют разные длины. Ни аргумент, ни возвращаемое значение не могут выступать неправильными значениями [3].
Как Вы могли уже заметить, данная функция помечена как небезопасная, что логично, ибо она способна порождать неопределённое поведение [4] вследствие множества факторов:
non-repr(C)
типами;Однако данная методика оправдывает себя в некоторых частных случах, например, получения бит-паттерна типа данных с плавающей точкой (или, что более обобщённо, type punning, где T
и U
не являются сырыми указателями):
let bitpattern = unsafe {
std::mem::transmute::<f32, u32>(1.0)
};
assert_eq!(bitpattern, 0x3F800000);
Преобразование сырого указателя к указателю на функцию. Стоит отметить, что данная техника не является переносимой между теми платформами, где указатели на функции и обычные указатели разнятся в размерах.
fn foo() -> i32 {
0
}
let pointer = foo as *const ();
let function = unsafe {
std::mem::transmute::<*const (), fn() -> i32>(pointer)
};
assert_eq!(function(), 0);
Расширение лайфтайма или укорачивание инвариантного лайфтайма. Это очень небезопасный и в то же время продвинутый синтаксис Rust:
struct R<'a>(&'a i32);
unsafe fn extend_lifetime<'b>(r: R<'b>) -> R<'static> {
std::mem::transmute::<R<'b>, R<'static>>(r)
}
unsafe fn shorten_invariant_lifetime<'b, 'c>(r: &'b mut R<'static>)
-> &'b mut R<'c> {
std::mem::transmute::<&'b mut R<'static>, &'b mut R<'c>>(r)
}
В основном оператор as
[6] может без особых усилий предотвратить появление неопределённого поведения, вызванного mem::transmute<T, U>
:
let ptr = &mut 0;
let val_transmuted = unsafe {
std::mem::transmute::<&mut i32, &mut u32>(ptr)
};
// Теперь соединяем `as` и перезаимствование. Но помните, что
// цепочка операторов `as не является транзитивной
let val_casts = unsafe { &mut *(ptr as *mut i32 as *mut u32) };
А также могут быть использованы безопасные методы, делающие то же самое, что и аналогичный вызов mem::transmute<T, U>
:
// Далеко не самый безопасный метод превращения строки
// к байтовому массиву
let slice = unsafe { std::mem::transmute::<&str, &[u8]>("Rust") };
assert_eq!(slice, &[82, 117, 115, 116]);
// Вы вправе просто использовать метод `as_bytes()`, который
// далает то же самое
let slice = "Rust".as_bytes();
assert_eq!(slice, &[82, 117, 115, 116]);
// Также есть возможность использования байтового
// строчного литерала
assert_eq!(b"Rust", &[82, 117, 115, 116]);
А данный исходный код демонстрируют три реализации функции slice::split_at_mut()
[7]: с использованием mem::transmute<T, U>
, as
операторов и функции slice::from_raw_parts()
[8].
use std::{slice, mem};
// Существует много способов реализовать это, а также множество
// проблем с данным подходом
fn split_at_mut_transmute<T>(slice: &mut [T], mid: usize)
-> (&mut [T], &mut [T]) {
let len = slice.len();
assert!(mid <= len);
unsafe {
let slice2 = mem::transmute::<&mut [T], &mut [T]>(slice);
// Во-первых, mem::transmute<T, U> не является типобезопасным,
// ведь всё, что он проверяет - это размеры типов T и U.
//
// Во-вторых, здесь мы имеем две изменяемые ссылки, указывающие
// на одну и ту же область памяти
(&mut slice[0..mid], &mut slice2[mid..len])
}
}
// Эта функция избавляется от проблем с типобезопасностью; `&mut *`
// будет лишь порождать `&mut T` из `&mut T` или `*mut T`.
fn split_at_mut_casts<T>(slice: &mut [T], mid: usize)
-> (&mut [T], &mut [T]) {
let len = slice.len();
assert!(mid <= len);
unsafe {
let slice2 = &mut *(slice as *mut [T]);
// Однако мы всё ещё имеем две мутабельные ссылки, указывающие
// на один и тот же сегмент памяти
(&mut slice[0..mid], &mut slice2[mid..len])
}
}
// Это реализация стандартной библиотеки, что является лучшим
// решением
fn split_at_stdlib<T>(slice: &mut [T], mid: usize)
-> (&mut [T], &mut [T]) {
let len = slice.len();
assert!(mid <= len);
unsafe {
let ptr = slice.as_mut_ptr();
// Сейчас мы имеем три мутабельные ссылки, указывающих
// на один сегмент памяти: `slice`, r-value ret.0 и r-value ret.1.
//
// `slice` не будет использован после `let ptr = ...`, и таким
// образом, он будет считаться "мёртвым", что означает, что
// мы имеем лишь два иммутабельных слайса.
(slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid))
}
}
Говоря другими словами, применение mem::transmute<T, U>
оправдано лишь в тех случаях, когда ничто другое уже не помогает (здесь уместна аналогия с рефлексией в некоторых языках).
Обычная интринсика mem::transmute<T, U>
может оказаться непригодной в тех случах, где передача владения переменной типа T
невозможна. На помощь приходит её вариация mem::transmute_copy<T, U>
[9]:
pub unsafe fn transmute_copy<T, U>(src: &T) -> U
Как Вы уже могли догадаться, она вместо перемещения единственного аргумента совершает его полное копирование и передаёт владение полученного результата. Данная вариация будет работать медленнее, поэтому её рекомендуется применять реже.
В отличие от mem::transmute<T, U>
, текущий аналог не порождает ошибку компиляции, если типы T
и U
имеют разную длину в байтах, но вызывать её настоятельно рекомендуется лишь в том случае, когда они имеют одинаковый размер.
Также стоит помнить о том, что если размер типа U
превышает размер T
, то данная функция порождает неопределённое поведение.
Ещё раз отмечу, что рассматриваемые функции следует применять лишь в тех случаях, когда без них совсем не обойтись. Как говорит Номикон [10], это действительно самая небезопасная вещь, которую вы можете сделать в Rust.
Все примеры из данной статьи взяты с официальной документации mem::transmute<T, U>
[2], а также были использованы материалы отсюда [9] и отсюда [10]. Надеюсь, статья была Вам полезна.
Автор: Gymmasssorla
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/nenormal-noe-programmirovanie/314744
Ссылки в тексте:
[1] Rust: https://www.rust-lang.org/tools/install
[2] mem::transmute<T, U>
: https://doc.rust-lang.org/std/mem/fn.transmute.html
[3] неправильными значениями: https://doc.rust-lang.org/nomicon/what-unsafe-does.html
[4] неопределённое поведение: https://doc.rust-lang.org/reference/behavior-considered-undefined.html
[5] несвязанному лайфтайму: https://doc.rust-lang.org/nomicon/unbounded-lifetimes.html
[6] оператор as
: https://doc.rust-lang.org/std/keyword.as.html
[7] slice::split_at_mut()
: https://doc.rust-lang.org/std/primitive.slice.html#method.split_at_mut
[8] slice::from_raw_parts()
: https://doc.rust-lang.org/std/slice/fn.from_raw_parts.html
[9] mem::transmute_copy<T, U>
: https://doc.rust-lang.org/std/mem/fn.transmute_copy.html
[10] Номикон: https://doc.rust-lang.org/nomicon/transmutes.html
[11] Источник: https://habr.com/ru/post/448240/?utm_campaign=448240
Нажмите здесь для печати.