pointers - Verificación de tamaño de tipo genérico en tiempo de compilación
rust ffi (2)
Estoy intentando escribir enlaces de Rust para una biblioteca de colección C (Judy Arrays [1]) que solo proporciona espacio para almacenar un valor de ancho de puntero. Mi empresa tiene una cantidad justa de código existente que utiliza este espacio para almacenar directamente valores no punteros, como enteros de ancho de puntero y pequeñas estructuras. Me gustaría que mis enlaces de Rust permitieran el acceso seguro a las colecciones de este tipo utilizando genéricos, pero tengo problemas para que la semántica de ocultación de punteros funcione correctamente.
Tengo una interfaz básica que funciona con std::mem::transmute_copy()
para almacenar el valor, pero esa función no hace nada para asegurar que los tipos de origen y destino sean del mismo tamaño. Puedo verificar que el parámetro de tipo de colección es de un tamaño compatible en el tiempo de ejecución a través de una aserción, pero realmente me gustaría que la verificación se realice en tiempo de compilación.
Código de ejemplo:
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);
}
}
}
¿Hay una mejor manera de hacerlo o es esta verificación en tiempo de ejecución la mejor Rust 1.0?
( Pregunta relacionada , explicando por qué no estoy usando mem::transmute()
.)
[1] Soy consciente del proyecto Rust-Judy existente, pero no es compatible con el puntero que quiero, y estoy escribiendo estos nuevos enlaces en gran parte como un ejercicio de aprendizaje de todos modos.
¿Revisión de tiempo de compilación?
¿Hay una mejor manera de hacerlo o es esta verificación en tiempo de ejecución la mejor Rust 1.0?
En general, hay algunas soluciones de pirateo para hacer algún tipo de prueba de compilación de condiciones arbitrarias. Por ejemplo, está la caja static_assertions
que ofrece algunas macros útiles (incluida una macro similar a la static_assert
C ++). Sin embargo, esto es hacky y muy limitado .
En su situación particular, no he encontrado una forma de realizar la comprobación en tiempo de compilación. El problema raíz aquí es que no puede usar mem::size_of
o mem::transmute
en un tipo genérico . Temas relacionados: #47966 y #47966 . Por esta razón, la caja static_assertions
tampoco funciona.
Si lo piensa, esto también permitiría un tipo de error muy poco familiar para los programadores de Rust: un error al crear una función genérica con un tipo específico. Esto es bien conocido por los programadores de C ++: los límites del rasgo de Rust se utilizan para corregir esos mensajes de error a menudo muy malos e inútiles. En el mundo de Rust, uno tendría que especificar su requisito como rasgo vinculado: algo como where size_of::<T> == size_of::<usize>()
.
Sin embargo, esto no es posible actualmente. Hubo una vez un bastante famoso "sistema de tipo dependiente de const" RFC que permitiría este tipo de límites, pero fue rechazado por ahora. El soporte para este tipo de funciones avanza lenta pero constantemente. "Miri" se fusionó con el compilador hace algún tiempo, permitiendo una evaluación constante mucho más poderosa. Este es un habilitador para muchas cosas, incluido el RFC "Const Generics" , que en realidad se fusionó. Todavía no está implementado, pero se espera que aterrice en 2018 o 2019.
Desafortunadamente, todavía no habilita el tipo de enlace que necesita. Al comparar dos expresiones const para la igualdad, se dejó a propósito fuera de la RFC principal para resolverse en una RFC futura.
Por lo tanto, es de esperar que un límite similar a where size_of::<T> == size_of::<usize>()
eventualmente sea posible. ¡Pero esto no debe esperarse en un futuro cercano!
Solución
En su situación, probablemente introduciría un rasgo inseguro como AsBigAsUsize
. Para implementarlo, podría escribir una macro impl_as_big_as_usize
que realice una verificación de tamaño e implemente el rasgo. Tal vez algo como esto:
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!
}
}
}
Esto utiliza básicamente el mismo engaño que está utilizando static_assertions
. Esto funciona, porque nunca usamos size_of
en un tipo genérico, sino solo en tipos concretos de la invocación de macro.
Entonces ... esto obviamente está lejos de ser perfecto. El usuario de su biblioteca debe invocar impl_as_big_as_usize
una vez por cada tipo que desee usar en su estructura de datos. Pero al menos es seguro: mientras los programadores solo usan la macro para implementar el rasgo, el rasgo se implementa de hecho solo para tipos que tienen el mismo tamaño que usize
. Además, el error "el rasgo de AsBigAsUsize
no está satisfecho" es muy comprensible.
¿Qué pasa con la verificación en tiempo de ejecución?
Como dijo el alboroto en los comentarios, en su assert!
código, no hay ninguna verificación en tiempo de ejecución , porque el optimizador pliega constantemente la verificación. Probemos esa afirmación con este código:
#![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
}
Las expresiones asm!()
Locas sirven dos propósitos:
- “Ocultar”
t
de LLVM, de modo que LLVM no puede realizar optimizaciones que no queremos (como eliminar toda la función) - marcando puntos específicos en el código ASM resultante que veremos
Compílalo con un compilador nocturno (en un entorno de 64 bits):
rustc -O --emit=asm test.rs
Como de costumbre, el código de ensamblaje resultante es difícil de leer; Aquí están los puntos importantes (con un poco de limpieza):
_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
El par #APP
- #NO_APP
es nuestra expresión asm!()
.
- El caso
foo<bool>
: ¡puedes ver que se compila nuestra primera instrucciónasm!()
, Luego se realiza una llamada no condicionada apanic!()
Y luego aparece nada (ud2
simplemente dice "el programa nunca puede alcanzar este punto,panic!()
diverges "). - El caso
foo<u64>
: puede ver ambos pares#APP
-#NO_APP
(ambas expresionesasm!()
) Sin nada entre ellos.
Así que sí: el compilador elimina la comprobación por completo .
Sería mucho mejor si el compilador simplemente se negara a compilar el código. Pero de esta manera, al menos sabemos, que no hay gastos generales de tiempo de ejecución.
¡Contrariamente a la respuesta aceptada, puede verificar en tiempo de compilación!
El truco consiste en insertar, al compilar con optimizaciones, una llamada a una función C indefinida en la ruta del código muerto. Obtendrá un error de vinculador si su afirmación fallara.