Проверка размера универсального типа во время компиляции

Я пытаюсь написать привязки 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] Мне известно о существующем проекте rust-judy, но он не поддерживает нужное мне хранение указателей, и я все равно пишу эти новые привязки в основном в качестве обучающего упражнения.


person llasram    schedule 19.05.2015    source источник
comment
Это не работает. Он копирует первое слово val и сохраняет его в v. О, и если вы хотите сохранить указатель, сохраните указатель на что-то, что действительно существует - например, указатель на T в Box<T>.   -  person bluss    schedule 20.05.2015
comment
Я хочу скопировать первое слово val, если тип val точно соответствует размеру слова. Целью здесь является взаимодействие FFI с существующим кодом C с использованием данных, хранящихся таким образом.   -  person llasram    schedule 20.05.2015
comment
Я не думаю, что ржавчина может сейчас ограничить размер T в целом. Однако assert, конечно, мономорфизируется и компилируется во время компиляции, поэтому, по крайней мере, нет накладных расходов.   -  person bluss    schedule 20.05.2015
comment
Хороший момент для assert! разрешения на бездействие или panic! во время компиляции. Если такая проверка во время выполнения на самом деле лучшее, что может сделать Rust 1.0, я приму это как ответ!   -  person llasram    schedule 20.05.2015
comment
Вы также можете написать несколько #[test], которые содержат эти assert!.   -  person porglezomp    schedule 03.09.2015


Ответы (2)


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

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

В общем, есть несколько хитрых решений для выполнения какого-либо тестирования произвольных условий во время компиляции. Например, есть static_assertions crate, который предлагает несколько полезных макросов (включая один макрос, похожий на макрос static_assert в C ++) . Однако это взломано и очень ограничено. .

В вашей конкретной ситуации я не нашел способа выполнить проверку во время компиляции. Основная проблема здесь в том, что нельзя использовать mem::size_of или mem::transmute для общего типа. Связанные вопросы: # 43408 и # 47966. По этой причине ящик static_assertions тоже не работает.

Если задуматься, это также допускает ошибку, совершенно незнакомую программистам на Rust: ошибку при создании экземпляра универсальной функции с определенным типом. Это хорошо известно программистам на C ++ - границы свойств Rust используются для исправления часто очень плохих и бесполезных сообщений об ошибках. В мире Rust вам нужно будет указать ваше требование как привязку к трейту: что-то вроде where size_of::<T> == size_of::<usize>().

Однако в настоящее время это невозможно. Когда-то существовал довольно известный RFC "зависимой от константы системы типов", который разрешить такие ограничения, но пока отклонены. Поддержка таких функций медленно, но неуклонно развивается. Некоторое время назад "Miri" была добавлена ​​в компилятор, что сделало возможным более мощное вычисление констант. Это средство для многих вещей, включая RFC "Const Generics", который был фактически объединен. Он еще не реализован, но ожидается, что он появится в 2018 или 2019 году.

К сожалению, он по-прежнему не обеспечивает нужного вам вида привязки. Сравнивая два константных выражения на равенство, были намеренно исключены из основного RFC для решения в будущих RFC.

Таким образом, следует ожидать, что в конечном итоге станет возможным ограничение, подобное where size_of::<T> == size_of::<usize>(). Но в ближайшее время этого ожидать не стоит!


Обходной путь

В вашей ситуации я бы, вероятно, ввел небезопасный признак AsBigAsUsize. Чтобы реализовать это, вы можете написать макрос impl_as_big_as_usize, который выполняет проверку размера и реализует признак. Может быть, примерно так:

unsafe trait AsBigAsUsize: Sized {
    const _DUMMY: [(); 0];
}

macro_rules! impl_as_big_as_usize {
    ($type:ty) => {
        unsafe impl AsBigAsUsize for $type {
            const _DUMMY: [(); 0] = 
                [(); (mem::size_of::<$type>() == mem::size_of::<usize>()) as usize];
            // We should probably also check the alignment!
        }
    }
}

Здесь используются те же уловки, что и static_assertions. Это работает, потому что мы никогда не используем size_of для универсального типа, а только для конкретных типов вызова макроса.

Итак ... это явно далеко не идеально. Пользователь вашей библиотеки должен вызывать impl_as_big_as_usize один раз для каждого типа, который он хочет использовать в вашей структуре данных. Но, по крайней мере, это безопасно: до тех пор, пока программисты используют макрос только для имплантации признака, признак фактически реализуется только для типов, имеющих тот же размер, что и usize. Кроме того, очень понятна ошибка «trait bound AsBigAsUsize is notailed».


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

Как сказано в комментариях, в вашем assert! коде нет проверки во время выполнения, потому что оптимизатор постоянно свертывает проверку. Давайте протестируем этот оператор с помощью этого кода:

#![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

Пара _22 _-_ 23_ является нашим asm!() выражением.

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

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

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

person Lukas Kalbertodt    schedule 27.09.2016

Вопреки принятому ответу, вы можете проверить его во время компиляции!

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

person Demi    schedule 04.05.2018
comment
Хотя этот ответ может быть или не быть правдой, в его нынешней форме он настолько широк и не содержит примера, что трудно сказать, что также означает, что кому-то будет сложно реально использовать это предложение. - person Shepmaster; 05.05.2018