pointers rust ffi

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ón asm!() , Luego se realiza una llamada no condicionada a panic!() 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 expresiones asm!() ) 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.