One way to understand the implementation details better can be to look at the assembly output. For example if you put this
pub trait Trait {
fn method(&self) -> i32;
}
impl Trait for u8 {
fn method(&self) -> i32 {
1234
}
}
pub fn demo<'a>(x: &mut &'a dyn Trait, y: &'a u8) {
*x = y
}
into the playground and click the “ASM” or “Show Assembly” button (use the 3-dots menu next to “Build” if you need to find it)
playground::demo:
movq %rdi, -16(%rsp)
movq %rsi, -8(%rsp)
movq %rsi, (%rdi)
leaq .Lanon.1dea076e61d54acb33ff2a3b5217cc93.0(%rip), %rax
movq %rax, 8(%rdi)
retq
<u8 as playground::Trait>::method:
movq %rdi, -8(%rsp)
movl $1234, %eax
retq
.Lanon.1dea076e61d54acb33ff2a3b5217cc93.0:
.asciz "\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000\001\000\000\000\000\000\000"
.quad <u8 as playground::Trait>::method
.byte 1
.byte 17
.byte 1
.byte 37
.byte 14
.byte 19
.byte 5
.byte 3
.byte 14
.byte 16
.byte 23
.byte 27
.byte 14
.byte 17
.byte 1
.byte 85
.byte 23
.byte 0
.byte 0
.byte 2
.byte 52
.byte 0
.byte 3
.byte 14
.byte 73
.byte 19
.byte 2
.byte 24
.byte 0
.byte 0
.byte 3
.byte 19
.byte 1
.byte 29
.byte 19
.byte 3
.byte 14
.byte 11
.byte 11
.ascii "\210\001"
.byte 15
.byte 0
.byte 0
.byte 4
.byte 13
.byte 0
.byte 3
.byte 14
.byte 73
.byte 19
.ascii "\210\001"
.byte 15
.byte 56
.byte 11
.byte 0
.byte 0
.byte 5
.byte 15
.byte 0
.byte 73
.byte 19
.byte 3
.byte 14
.byte 51
.byte 6
.byte 0
.byte 0
.byte 6
.byte 36
.byte 0
.byte 3
.byte 14
.byte 62
.byte 11
.byte 11
.byte 11
.byte 0
.byte 0
.byte 7
.byte 57
.byte 1
.byte 3
.byte 14
.byte 0
.byte 0
.byte 8
.byte 46
.byte 1
.byte 17
.byte 1
.byte 18
.byte 6
.byte 64
.byte 24
.byte 110
.byte 14
.byte 3
.byte 14
.byte 58
.byte 11
.byte 59
.byte 11
.byte 63
.byte 25
.byte 0
.byte 0
.byte 9
.byte 5
.byte 0
.byte 2
.byte 24
.byte 3
.byte 14
.byte 58
.byte 11
.byte 59
.byte 11
.byte 73
.byte 19
.byte 0
.byte 0
.byte 10
.byte 46
.byte 1
.byte 17
.byte 1
.byte 18
.byte 6
.byte 64
.byte 24
.byte 110
.byte 14
.byte 3
.byte 14
.byte 58
.byte 11
.byte 59
.byte 11
.byte 73
.byte 19
.byte 63
.byte 25
.byte 0
.byte 0
.byte 11
.byte 19
.byte 1
.byte 3
.byte 14
.byte 11
.byte 11
.ascii "\210\001"
.byte 15
.byte 0
.byte 0
.byte 12
.byte 15
.byte 0
.byte 73
.byte 19
.byte 51
.byte 6
.byte 0
.byte 0
.byte 13
.byte 19
.byte 0
.byte 3
.byte 14
.byte 11
.byte 11
.ascii "\210\001"
.byte 15
.byte 0
.byte 0
.byte 14
.byte 1
.byte 1
.byte 73
.byte 19
.byte 0
.byte 0
.byte 15
.byte 33
.byte 0
.byte 73
.byte 19
.byte 34
.byte 13
.byte 55
.byte 11
.byte 0
.byte 0
.byte 16
.byte 36
.byte 0
.byte 3
.byte 14
.byte 11
.byte 11
.byte 62
.byte 11
.byte 0
.byte 0
.byte 0
oh hey that’s a bit lengthy… but we can use “Release” builds to help cut things down… so let’s try that
No symbols detected — they may have been optimized away.
Add the #[unsafe(no_mangle)] attribute to
functions you want to see assembly for. Generic functions
only generate assembly when concrete types are provided.
ah, and it even tells you how to fix that situation, so let’s try it
<u8 as playground::Trait>::method:
movl $1234, %eax
retq
demo:
movq %rsi, (%rdi)
leaq .Lanon.fe01ce2a7fbac8fafaed7c982a04e229.0(%rip), %rax
movq %rax, 8(%rdi)
retq
.Lanon.fe01ce2a7fbac8fafaed7c982a04e229.0:
.asciz "\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000\001\000\000\000\000\000\000"
.quad <u8 as playground::Trait>::method
Nice! So here we see what *x = y does, and hence how a value of type &'a dyn Trait can be produced (and then written to the target of the argument pointer x):
demo:
movq %rsi, (%rdi)
leaq .Lanon.fe01ce2a7fbac8fafaed7c982a04e229.0(%rip), %rax
movq %rax, 8(%rdi)
retq
The first line, moves %rsi into (%rdi). The latter is the assembly notation for “target of pointer/address”. Here one can infer that %rsi holds our function argument y, and %rdi holds our function argument x 
The interesting part is the next 2 lines
leaq .Lanon.fe01ce2a7fbac8fafaed7c982a04e229.0(%rip), %rax
just produces a pointer to the vtable into %raw; the vtable itself lives in the static memory as part of the program code, and you can find it right below the function, in the assembly. Then movq %rax, 8(%rdi) moves that vtable pointer into 8(%rdi) which is assembly notation for “dereference %rdi after offsetting by 8 bytes”. So that explains how a &dyn Trait reference is made: it consists of one pointer to the actual target, and right after that one pointer to the vtable.
The same kind of behavior will happen if you use raw-pointer types, e.g. (playground) with this function added
#[unsafe(no_mangle)]
pub unsafe fn demo_raw(x: *mut *const dyn Trait, y: *const u8) {
unsafe {
*x = y
}
}
it shows up in the assembly as follows
<u8 as playground::Trait>::method:
movl $1234, %eax
retq
demo:
movq %rsi, (%rdi)
leaq .Lanon.2aea8d44369186b67761c05407ddfe3e.0(%rip), %rax
movq %rax, 8(%rdi)
retq
demo_raw:
movq %rsi, (%rdi)
leaq .Lanon.2aea8d44369186b67761c05407ddfe3e.0(%rip), %rax
movq %rax, 8(%rdi)
retq
.Lanon.2aea8d44369186b67761c05407ddfe3e.0:
.asciz "\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000\001\000\000\000\000\000\000"
.quad <u8 as playground::Trait>::method
In case you’re wondering about this "\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000\001\000\000\000\000\000\000" part – that’s represented a bit weird in the assembly, but it’s generally consisting of
- drop code for the type
- type’s size
- type’s alignment
here rendered though as a “string-literal” (which is additionally confusing as it’s zero-terminated, so you’ll only see 7 bytes in the trailing \001\000\000\000\000\000\000) and that these values are little-endian.
You can see this by changing the type, e.g. with
#[unsafe(no_mangle)]
pub fn demo_string<'a>(x: &mut &'a dyn Trait, y: &'a String) {
*x = y
}
(playground) we can get
demo_string:
movq %rsi, (%rdi)
leaq .Lanon.1d4a3cc080dab32f19d796fbe22cc522.0(%rip), %rax
movq %rax, 8(%rdi)
retq
.Lanon.1d4a3cc080dab32f19d796fbe22cc522.0:
.quad core::ptr::drop_in_place<alloc::string::String>
.asciz "\030\000\000\000\000\000\000\000\b\000\000\000\000\000\000"
.quad <alloc::string::String as playground::Trait>::method
where \b is “backspace” (ASCII encoded as 8) i.e. the alignment of a String, 8 (on a 64-bit system); and \030 is featuring an octal number 030 which is the number 3 * 8 = 24, the size of a String (which consists of a pointer, a length, and a capacity; 3 word-sized values).
If you want to read this as rust code instead, the current vtable implementation details could be simulated as follows (note that this relies on implementation details and is not a sound way to create vtables, and can break in the future).
#[repr(C)]
struct TraitVtable<T> {
drop: Option<unsafe fn(*mut T)>,
size: usize,
align: usize,
method: fn(&T) -> i32,
}
static VTABLE_TRAIT_FOR_U8: TraitVtable<u8> = TraitVtable {
drop: None,
size: std::mem::size_of::<u8>(),
align: std::mem::align_of::<u8>(),
method: <u8 as Trait>::method,
};
#[unsafe(no_mangle)]
pub fn demo_manually<'a>(x: *mut *const dyn Trait, y: *const u8) {
unsafe {
let x: *mut u8 = x.cast();
x.cast::<*const u8>().write(y);
x.offset(8).cast::<&'static TraitVtable<u8>>().write(&VTABLE_TRAIT_FOR_U8);
}
}
(playground)
<u8 as playground::Trait>::method:
movl $1234, %eax
retq
demo_manually:
movq %rsi, (%rdi)
leaq playground::VTABLE_TRAIT_FOR_U8(%rip), %rax
movq %rax, 8(%rdi)
retq
It doesn’t show the contents of playground::VTABLE_TRAIT_FOR_U8 here in the filtered assembly view of the playground unfortunately (I think on godbolt.org, you might get a better output), but if you uncomment the original demo next to it, you can get this result
<u8 as playground::Trait>::method:
movl $1234, %eax
retq
demo_manually:
movq %rsi, (%rdi)
leaq .Lanon.b7685d77080b46e47a9a83f0de6d6703.0(%rip), %rax
movq %rax, 8(%rdi)
retq
demo_u8:
movq %rsi, (%rdi)
leaq .Lanon.b7685d77080b46e47a9a83f0de6d6703.0(%rip), %rax
movq %rax, 8(%rdi)
retq
.Lanon.b7685d77080b46e47a9a83f0de6d6703.0:
.asciz "\000\000\000\000\000\000\000\000\001\000\000\000\000\000\000\000\001\000\000\000\000\000\000"
.quad <u8 as playground::Trait>::method will happe
where LLVM actually managed to get rid of the second “copy” of this static data and unified the real vtable as .Lanon.b7685d77080b46e47a9a83f0de6d6703.0 with our hand-baked one, hence confirming it was accurate. With String in place of u8 it would work as well (==>> see this playground).