Get the offset of a field from the base of a struct

In some cases you want to get the address of a field of a struct, without there actually being an instance of an allocated object there. A simple example is the vulkano crates impl_vertex macro. It needs to get the offset of a field from the base of a struct. This is very hard to represent in current rust. The way vulkano gets around this is by forcing the type to implement Default, and read a field later.

slightly modified code from vulkano:

let dummy = <$out>::default();

let dummy_ptr = (&dummy) as *const _;
let member_ptr = (&dummy.$member) as *const _;

let offset = member_ptr as usize - dummy_ptr as usize;

There is no reason we should have to construct the default value here, as the compiler knows the offset to the field at compile time. One idea I've had in the past is something like this:

let dummy = std::ptr::null::<StructName>();
let offset = &(*dummy).member as *const _ as usize;

However, this has two problems; first, it dereferences a null pointer. This is UB, but I don't think it should be UB because we dereference a null pointer, but rather that the member we later reference is referenced through a borrow, so that borrow points to an invalid value.

If we could make the rules around getting a raw pointer from a previously dereferenced value less restrictive, maybe we could make this pattern non UB, because all we're really doing is offsetting the raw pointer with some compile time constant value. Although, offsetting would also be UB because we're not inside the same allocated object(we're not inside any allocated object).

Another problem is that the member might call deref, so the rule would have to restrict the usage of deref somehow.

A much simpler idea is to just have a built-in macro for getting the byte offset to a struct field.

See Need for -> operator for Unsafe Code Guidelines via Raw pointer field projection

1 Like

Until we have such a thing, the memoffset crate provides such a macro.

2 Likes

In the next stable Rust version, you can get the offset of a field through a MaybeUninit<Struct> using the addr_of macro, so you don't need to require that the type implements Default.

https://play.rust-lang.org/?version=beta&mode=debug&edition=2018&gist=8b6d86fa1d269c7344db999cc3505f56

macro_rules! get_offset {
    ($type:ty, $field:tt) => ({
        let dummy = ::core::mem::MaybeUninit::<$type>::uninit();

        let dummy_ptr = dummy.as_ptr();
        let member_ptr = unsafe{ ::core::ptr::addr_of!((*dummy_ptr).$field) };
        
        member_ptr as usize - dummy_ptr as usize
    })
}

fn main(){
    dbg!(get_offset!((String, String), 0));
    dbg!(get_offset!((String, String), 1));
}
[src/main.rs:13] get_offset!((String, String), 0) = 0
[src/main.rs:14] get_offset!((String, String), 1) = 24
1 Like

Looks like addr_of implicitly dereferences to get to the field, so the macro I posted above causes UB for any pointer type. That looks like a bug to me, it shouldn't implicitly dereference through any pointer.

All of these compile: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=7fc0ed41d825d16900077063ecf3ccb7

dbg!(get_offset!(std::rc::Rc<(String, String)>, 0));
dbg!(get_offset!(std::sync::Arc<(String, String)>, 0));
dbg!(get_offset!(std::sync::MutexGuard<'_, (String, String)>, 0));
dbg!(get_offset!(&mut (String, String), 0));
dbg!(get_offset!(&(String, String), 0));
dbg!(get_offset!(Box<(String, String)>, 1));

You can guard against implicit deref using the same trick used in the memoffset crate. Unfortunately doing so this way seems to preclude use with tuples, as tuples can't be used for the used syntax.

macro_rules! get_offset {
    ($type:ty, $field:tt) => ({
        type T = $type;
        // Make sure the field actually exists. This line ensures that an error
        // is generated if $field is accessed through an implicit deref.
        #[allow(clippy::unneeded_field_pattern)]
        let T { $field: _, .. };
    
        let dummy = ::core::mem::MaybeUninit::<$type>::uninit();

        let dummy_ptr = dummy.as_ptr();
        let member_ptr = unsafe{ ::core::ptr::addr_of!((*dummy_ptr).$field) };
        
        member_ptr as usize - dummy_ptr as usize
    })
}
error[E0026]: struct `Rc` does not have a field named `0`
  --> src/main.rs:19:53
   |
19 |     dbg!(get_offset!(std::rc::Rc<(String, String)>, 0));
   |                                                     ^ struct `Rc` does not have this field

error[E0026]: struct `Arc` does not have a field named `0`
  --> src/main.rs:20:56
   |
20 |     dbg!(get_offset!(std::sync::Arc<(String, String)>, 0));
   |                                                        ^ struct `Arc` does not have this field

(Due to the use of the temporary type binding to allow using type arguments, types with lifetimes fail in a worse way. Also Box causes an ICE :grimacing:)

3 Likes

@CAD97 Figured out an approach that gives the same errors that you showed here, while effortlessly working with generic structs, but it ICEs with Box too :eyes: .

Adding this to the macro:

type Type<T> = T;

let Type::<$type> { $field: _, .. };

Code in context: Rust Playground

1 Like

addr_of doesn't dereference anything, but . implicitly dereferences in Rust.