Проверка размера обобщенного типа компиляции

Я пытаюсь написать привязки Rust для библиотеки коллекции C (Judy Arrays [1]), которая предоставляет только место для хранения значения ширины указателя. У моей компании есть довольно много существующего кода, который использует это пространство для непосредственного хранения значений не указателя, таких как целые числа указателей и небольшие структуры. Я хотел бы, чтобы мои привязки Rust позволяли безопасный доступ к таким коллекциям с использованием типов, но у меня возникли проблемы с правильной обработкой семантики указателя.

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

Пример кода:

pub struct Example<T> {
    v: usize,
    t: PhantomData<T>,
}

impl<T> Example<T> {
    pub fn new() -> Example<T> {
        assert!(mem::size_of::<usize>() == mem::size_of::<T>());
        Example { v: 0, t: PhantomData }
    }

    pub fn insert(&mut self, val: T) {
        unsafe {
            self.v = mem::transmute_copy(&val);
            mem::forget(val);
        }
    }
}

Есть ли лучший способ сделать это, или это проверка времени выполнения, которую поддерживает лучший Rust 1.0?

(Связанный вопрос, объясняя, почему я не использую mem::transmute().)

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

Ответы

Ответ 1

Проверка времени компиляции?

Есть ли лучший способ сделать это, или это проверка времени выполнения, которую поддерживает лучший Rust 1.0?

Короче: Нет, сейчас нет лучшего способа (начиная с Rust 1.10). В настоящее время проверка времени выполнения - это лучшее, что мы можем сделать.

Однако есть надежда: удивительная "const-зависимая система типов" RFC, скорее всего, сделает случаи, которые можно проверить во время компиляции, Я попытаюсь вспомнить обновление моего ответа после принятия и внедрения RFC (надеюсь, это будет!).


Как насчет проверки времени выполнения?

Как сказано в комментариях bluss, существует проверка времени выполнения, поскольку константа оптимизатора сбрасывает чек. Пусть тестовый оператор с этим кодом:

#![feature(asm)]

fn main() {
    foo(3u64);
    foo(true);
}

#[inline(never)]
fn foo<T>(t: T) {
    use std::mem::size_of;

    unsafe { asm!("" : : "r"(&t)) }; // black box
    assert!(size_of::<usize>() == size_of::<T>());
    unsafe { asm!("" : : "r"(&t)) }; // black box
}

Сумасшедшие выражения asm!() служат двум целям:

  • "скрывать" t от LLVM, так что LLVM не может выполнять оптимизации, которые мы не хотим (например, удаление всей функции)
  • маркировка определенных точек в полученном ASM-коде, который мы рассмотрим

Скомпилируйте его с помощью ночного компилятора (в 64-битной среде!):

rustc -O --emit=asm test.rs

Как обычно, полученный код сборки трудно читать; вот важные места (с некоторой очисткой):

_ZN4test4main17he67e990f1745b02cE:  # main()
    subq    $40, %rsp
    callq   _ZN4test3foo17hc593d7aa7187abe3E
    callq   _ZN4test3foo17h40b6a7d0419c9482E
    ud2

_ZN4test3foo17h40b6a7d0419c9482E: # foo<bool>()
    subq    $40, %rsp
    movb    $1, 39(%rsp)
    leaq    39(%rsp), %rax
    #APP
    #NO_APP
    callq   _ZN3std9panicking11begin_panic17h0914615a412ba184E
    ud2

_ZN4test3foo17hc593d7aa7187abe3E: # foo<u64>()
    pushq   %rax
    movq    $3, (%rsp)
    leaq    (%rsp), %rax
    #APP
    #NO_APP
    #APP
    #NO_APP
    popq    %rax
    retq

Пара #APP - #NO_APP - наше выражение asm!().

  • Случай foo<bool>: вы можете видеть, что наша первая команда asm!() скомпилирована, затем выполняется безусловный вызов panic!(), а потом ничего не приходит (ud2 просто говорит: "Программа никогда не сможет достичь этого места, panic!() расходится" ).
  • Случай foo<u64>: вы можете видеть пары #APP - #NO_APP (оба выражения asm!()) без каких-либо промежутков между ними.

Итак, да: компилятор полностью удаляет проверку.

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