Магические трансформации типов данных в Rust: Интринсика mem::transmute<T, U>

m-yvsgqiqpdkzs8z020qxwp8qry.jpeg

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

Одной из них является наш сегодняшний экземпляр — интринсика mem::transmute, предназначенная для того и другого понемногу, пригождаясь в крайне необычных ситтуациях.

mem::transmute реализована непосредственно компилятором, так как по определению не может быть описана синтаксическими средствами языка Rust. Она попросту реинтерпретирует биты одного типа данных как биты другого:

pub unsafe extern "rust-intrinsic" fn transmute(e: T) -> U

Перед реинтерпретацией данная функция берёт на себя владение реинтерпретируемой переменной типа данных T, а после будто «забывает» её (но без вызова соответствующего деструктора).

Никакого копирования в физической памяти не происходит, ибо данная интринсика просто даёт понять компилятору, что содержимое типа T — это, на самом деле, тип U.

Ошибка компиляции возникнет в том случае, если типы T и U имеют разные длины. Ни аргумент, ни возвращаемое значение не могут выступать неправильными значениями.

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


  • Создание экземпляра любого типа с недопустимым состоянием;
  • Создание примитива с недопустимым значением;
  • Чтобы удоволетворить вывод типов, возможно порождение совершенно неожиданного выходного типа при его неуказании;
  • Преобразования между non-repr(C) типами;
  • Преобразование в обычную ссылку без явно заданного лайфтайма приводит к несвязанному лайфтайму;
  • Конверсия иммутабельной ссылки к мутабельной.

Однако данная методика оправдывает себя в некоторых частных случах, например, получения бит-паттерна типа данных с плавающей точкой (или, что более обобщённо, type punning, где T и U не являются сырыми указателями):

let bitpattern = unsafe {
    std::mem::transmute::(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<'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 может без особых усилий предотвратить появление неопределённого поведения, вызванного mem::transmute:

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:

// Далеко не самый безопасный метод превращения строки
// к байтовому массиву
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(): с использованием mem::transmute, as операторов и функции slice::from_raw_parts().

use std::{slice, mem};

// Существует много способов реализовать это, а также множество
// проблем с данным подходом
fn split_at_mut_transmute(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.
        //
        // Во-вторых, здесь мы имеем две изменяемые ссылки, указывающие
        // на одну и ту же область памяти
        (&mut slice[0..mid], &mut slice2[mid..len])
    }
}

// Эта функция избавляется от проблем с типобезопасностью; `&mut *`
// будет лишь порождать `&mut T` из `&mut T` или `*mut T`.
fn split_at_mut_casts(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(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 оправдано лишь в тех случаях, когда ничто другое уже не помогает (здесь уместна аналогия с рефлексией в некоторых языках).

Обычная интринсика mem::transmute может оказаться непригодной в тех случах, где передача владения переменной типа T невозможна. На помощь приходит её вариация mem::transmute_copy:

pub unsafe fn transmute_copy(src: &T) -> U

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

В отличие от mem::transmute, текущий аналог не порождает ошибку компиляции, если типы T и U имеют разную длину в байтах, но вызывать её настоятельно рекомендуется лишь в том случае, когда они имеют одинаковый размер.

Также стоит помнить о том, что если размер типа U превышает размер T, то данная функция порождает неопределённое поведение.

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

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

© Habrahabr.ru