But in my scenario, Part1 and Part2 are always inside the Whole (and it is known at compile time).
In that case, there's no reason for Part1 and Part2 to be structs of their own: their fields could instead be defined directly on Whole. Defining them as structs of their own might have made sense if that encapsulated some sub-behaviour, but the fact these parts need access to their siblings belies the suitability of such an approach.
If, for some reason, Part1 and Part2 must nevertheless remain as structs in their own right, then you could:
Pass a reference to the relevant sibling into any method that requires it:
impl Part1 {
fn do_something(&self, part0: &Part0) {
// do whatever with self and part0
}
}
and/or
Implement relevant behaviours on Whole rather than the sub-part:
impl Whole {
fn do_something(&self) {
// do whatever with self.part1 and self.part0
}
}
Of course, using unsafe code, it is nevertheless possible to discover the layout of a struct and access its members in the manner that you describe: I just don't see it being useful for the scenario described in this question. Nevertheless...
Ideally you'd want to embed knowledge of the layout of Whole's members directly into your compiled code. Unfortunately, because rustc does not directly provide means by which the layout of a struct's members can be discovered during compilation, the only way this could be done is by manually calculating the position of each member. However, as documented in The Default Representation in The Rust Reference (emphasis added):
Nominal types without a repr attribute have the default representation. Informally, this representation is also called the rust representation.
There are no guarantees of data layout made by this representation.
Consequently, if Whole is using Rust's default representation, it is impossible to determine the layout of its members during compilation (however, if Whole uses The C Representation, which guarantees a particular layout of a struct's members, one can manually calculate their positions at compile-time, which the repr_offset crate greatly simplifies); instead, the layout of Whole's members can be determined at runtime:
use std::ptr;
impl Whole {
fn new() -> Self {
let mut me = Self { ... };
unsafe {
let addr_of_part0 = ptr::addr_of!(me.part0).cast::<u8>();
let addr_of_part1 = ptr::addr_of!(me.part1).cast::<u8>();
let addr_of_part2 = ptr::addr_of!(me.part2).cast::<u8>();
me.part1.offset_to_part0 = addr_of_part0.offset_from(addr_of_part1);
me.part2.offset_to_part0 = addr_of_part0.offset_from(addr_of_part2);
me.part2.offset_to_part1 = addr_of_part1.offset_from(addr_of_part2);
}
me
}
}
And then one can do:
impl Part1 {
unsafe fn part0(&self) -> &Part0 {
let addr_of_self = ptr::addr_of!(*self).cast::<u8>();
let addr_of_part0 = addr_of_self.offset(self.offset_of_part0);
&*addr_of_part0.cast()
}
}
impl Part2 {
unsafe fn part0(&self) -> &Part0 {
let addr_of_self = ptr::addr_of!(*self).cast::<u8>();
let addr_of_part0 = addr_of_self.offset(self.offset_of_part0);
&*addr_of_part0.cast()
}
unsafe fn part1(&self) -> &Part1 {
let addr_of_self = ptr::addr_of!(*self).cast::<u8>();
let addr_of_part1 = addr_of_self.offset(self.offset_of_part1);
&*addr_of_part1.cast()
}
}
Note that these functions are unsafe because they rely on you, the programmer, only invoking them when you know that the respective offset_of_... fields are valid offsets to the relevant type that can be dereferenced into a shared pointer with the same lifetime as self. You likely will only ever be able to guarantee this when you are accessing self through an owning Whole, otherwise you could easily end up violating memory safety and triggering undefined behaviour—@Netwave's suggestion of using refcounted ownership is one thoroughly sensible way to avoid this risk.