-
Notifications
You must be signed in to change notification settings - Fork 58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How are virtual function calls specced? #338
Comments
I suggest: Abstract machine devirtualization tables. The Importantly, these devirtualization tables are not located anywhere in AM memory. Compilers can implement this pattern via vtables, justified by the as-if rule. This has the advantage of making the behavior of virtual calls completely explicit, while allowing compilers the full flexibility of implementing their vtables using whatever representation they like. The meaning of the |
The way I'd define it is
with
pub trait Foo{
fn foo(&self);
}
impl Foo for i32{
fn foo(&self){println!("Foo");}
} ] |
Yeah, I believe our definitions are functionally equivalent, except that I consider (maybe unreasonably much) the operational-ness of my definition to be a significant benefit that we should try and retain wherever possible |
True - mine is a better prose spec, yours is a better description of the operational semantics (w/o exposing impl-details). |
Does the metadata pointer have guaranteed static lifetime? Is it guaranteed to be Regarding the difference between Jakob's and Connor's specs: Connor's mentions that it is UB to call My answers to these questions:
|
Presumaby. though it's fun. Mostly it would just spec-in that if you call a |
I'd wonder if there is a benefit to specifying that calls don't care about the dynamic lifetime if, for the purposes of the spec, all operations that can validly produce the metadata pointer produce something with a |
That's a fair point. Given that we want to be able to use "anything Rust code could do" as a way to spec unknown functions and FFI, I think we need to add an intrinsic for creating non-static dyn metadata even if there isn't anything in the surface language to invoke it, if only to say "FFI or other unknown calls, or compiler intrinsics, can maybe do this". |
To be more concrete, let's consider some cases with the following code: trait Foo {
fn foo(&self);
}
impl Foo for i32 {
fn foo(&self) {}
}
impl Foo for u8 {
fn foo(&self) {}
} (I will use
Here's another fun thing: is it legal for the compiler to not create any dyn metadata for |
It is immediate UB anyway, because the |
Regarding the remaining questions: Generally, I agree with Mario. That being said, I would like to clarify something though: I believe that the devirtualization tables should map pointer values to function pointer values. Importantly, this means that the domain is let d: &dyn A = something();
let (val, metadata): (_, *const ()) = d;
let metadata_new = &mut *(metadata as *mut ()) as *const (); // Reborrow the `metadata` through a `&mut ()`
let d = (val, metadata_new);
d.foo(5); As soon as we lower virtual function calls to an IR in which there are vtable pointers instead of devirtualization tables, this will read outside of With that worked out, specifying the behavior of dyn upcasting is easy: Create a |
I don't see the issue: Reborrowing creates a new tag, so it would be rejected as a legal metadata value. Or do you want to accept that code?
Ah, I don't think this one is quite right. I don't think we want to promise that metadata acquired through unsizing is unique or a fresh id, and in fact we might want to promise the opposite. It also looks like another interesting application of NB. I would say that the compiler is allowed to return nondeterministically any metadata value valid for the type and trait (which was already in the map beforehand). It's not entirely clear to me whether the "allocation" of a new entry in the map would be observable, but it seems like a safe bet to not allow such a thing. Put another way, I think that metadata values should already exist at program start in the same way as function pointer values. (Dyn unsizing has a lot in common with casting |
Ralf had mentioned at some point that they might not be. It seems possible to make them the same thing, but that's off-topic here, so just for safety I'm including both. Besides that, I think I had misread your original proposal. Re-reading it now, I agree that ours are actually the same, and both correctly disallow the code I posted above |
What's wrong with saying that basically what Miri does is the spec? I assume it is the concern I raised about wanting to change vtable layout in the future? Seeing all this extra complication here, I wonder if we cannot carefully delineate some part of this as being a part of the spec that might change in the future -- like how exactly the fn ptr is determined given the vtable ptr and trait? |
I believe the purpose of the complication here is precisely to allow the compiler freedom to do other things, in particular devirtualization. I think it is right that even beyond simply assuming that a vtable pointer is in unspecified layout like a repr(Rust) struct, it can also simply not be at the address it purports to live at - this is an observable property since you could hand-code an offset pointer access to get at the function pointer, and this apparatus allows us to say that accessing vtable data yourself is UB, which is necessary if the table isn't actually there because it was devirtualized but you've hacked together another access of the function manually to fool the compiler's devirtualization analysis.
I think the operative question is: is the determination of fn ptr from vtable ptr and trait something that well-defined rust code could do? Assuming it knew the compiler's layout. The thing the Jakob's (and Connor's) models allow you to do is say "no this function is magic and you cannot fake it with rust code" which seems like the maximally conservative choice if we want to consider the possibility of compilers being clever and reading a lot into the structure of virtual calls. Another thing: you say determination of the "fn ptr" from the vtable ptr, but AFAIK the fn ptr itself is not something you have access to with non-weird rust code: you can only call the function, not observe its location. An explicit (but unspecified) repr for vtables would presumably imply that this fn ptr is accessible, which might limit what a compiler can do with inlining and deleting the function.
What exactly does Miri do? I assume it just has a concrete vtable layout. One issue with this is that the vtable layout has to match rustc's or else it could miss UB in user code. With this abstract approach, you can "naturally" model vtables directly (i.e. in actual data structures instead of in simulated memory) in Miri and you only need to know spec stuff to write it, which seems like a plus. |
I actually initially wanted to do something like what Ralf said, where we essentially claim that the AM is parametrized by a vtable shim which accepts the metadata and outputs the function pointer. The problem is that you also have to address the question of what this shim may be - clearly it may not |
I wonder whether the talk about |
Yeah, I'm not sure what I was thinking. Completely agreed though, unsizing should attach an existing metadata to the pointer, not create a new one |
Could you give an example?
Yes. In fact the code that generates vtables is shared between Miri and the codegen backends. |
trait Foo {
fn foo(&self);
}
#[repr(C)]
struct FooVTable<T> {
drop: unsafe fn(&mut T),
size: usize,
align: usize,
foo_ptr: fn(&T),
}
#[repr(C)]
struct DynFoo<'a, T> {
data: &'a T,
vtable: &'a FooVTable<T>,
}
// Safety: I looked at rustc layout stuff and this is totally okay, pinky promise
// (as long as `dynptr` is in fact pointing to a `T`)
unsafe fn undyn<T: Foo>(dynptr: &dyn Foo) -> DynFoo<'_, T> {
std::mem::transmute(dynptr)
}
impl Foo for i32 {
fn foo(&self) {
println!("called i32::foo({self})")
}
}
fn main() {
let dynptr: &dyn Foo = &0i32;
let undyned = unsafe { undyn::<i32>(dynptr) };
// equivalent to: dynptr.foo()
(undyned.vtable.foo_ptr)(undyned.data);
} This is some "perfectly normal" code that takes advantage of unstable (but not UB) layout details in order to call The way this interacts with devirtualization is that I would like the compiler to be able to reason:
These optimizations are valid in Jakob's model because the
I assume you realize that this is problematic re: making a specification which is independent of rustc so that alternate rust implementations can exist. |
An alternative rust compiler may choose a different vtable layout and different versions of rustc may choose different vtable layouts. It is just whatever you do miri and the codegen backends need to agree on a single vtable layout for any specific rustc version and thus having miri generate vtables is the best option we have. |
Right, clearly for rustc's miri this is the right choice, but the issue of "how do we spec this" still remains. If we allow other implementations to do it differently, what are the rules governing what they may do? Is it really just the layout of the vtable that is allowed to be changed? That seems unnecessarily restrictive, an implementation should be able to add an extra indirection in there if it feels like it. |
Yeah, same as with
That is not what I expected devirtualization to do -- I thought it would be about replacing virtual calls by direct calls, but what you describe sounds quite different? I had indeed expected your code to be defined if one guessed vtable layout correctly, similar to
Oh definitely, I would see that as part of the layout. Basically: to create a vtable, the compiler will return a pointer to some data, backed up by a set of allocations. Mutating any of that is UB (i.e., this is all read-only memory). The compiler also has procedures that, given a vtable, can look up the size and alignment, drop function, or virtual trait function of the type; it guarantees that when generating a vtable for a If you fake your own vtable, you get 0 guarantees for what these procedures will do when you run them on that vtable. To be clear, I am not set against something like @JakobDegen's proposal (assuming it can be implemented in Miri). I just would like to better understand the motivation. |
In terms of motivation, two comments:
This is definitely an option, but there's a distinction that I think is worth noting: For
As written, this rule does not prevent the compiler from including a
I had not considered that, but I do think this is equivalent, and that does indeed seem quite elegant. Especially if we already do this with function pointers, this kind of direction seems worth pursuing to me. |
"must" is too strong, we could make it ill-defined. C does that (standard C, but of course people use those patterns anyway). https://robbertkrebbers.nl/thesis.html has a proposed formalization of that. But for Rust I don't think we should do that -- it's just worth mentioning that the same design decision we have for vtables here, absolutely also exists for
It is fairly easy to say that these operations cannot have any observable side-effects. So I see no problem here. |
How are we defining "observable side-effects" here? (To be clear, I'm not trying to be pedantic/annoying, I ask because I tried to do this and failed) Specifically, it seems insufficient to restrict to "real" side-effects (IO and such), since functions without real side-effects could still be impure by eg writing to a |
I'd assume same way as C++, modified slightly: volatile access and I/O.
…On Wed, May 18, 2022 at 17:20 Jakob Degen ***@***.***> wrote:
these operations cannot have any observable side-effects
How are we defining "observable side-effects" here? (To be clear, I'm not
trying to be pedantic/annoying, I ask because I tried to do this and failed)
—
Reply to this email directly, view it on GitHub
<#338 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABGLD22L5YEMEYNSMIZSQQLVKVNKBANCNFSM5V6EAHEA>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Heh, sorry, sniped you with the edit |
I can add as many side effects to a pure function as I want as long as the observable behaviour is preserved - if a tree falls in a forest, and the user isn't arround to hear it, it does not make a sound. |
But you can't show that the observable behavior is preserved: trait A {
fn foo(&self);
}
struct S;
impl Foo for S { /* snip */ }
static mut V: i32 = 0;
(&S as &dyn A).foo();
dbg!(V); Requiring that the compiler shim not do observable side-effects does not prevent it from doing |
The original motivation that spawned this thread, though, was that it is very hard to say what a virtual function call does on the Abstract Machine if we keep the vtable observable. Quoting from above:
Also see the discussion starting around here.
Ah right that's the C/C++ term for it. |
How does it "give us stronger guarantees"? There are only two things that would affect if we can change it in future:
Unless we get to a point where everyone is running all their code through MIRI, then it doesn't seem like this decision affects either (1) or (2).
This concerns me because IMO, unsafe code should be able to do low-level memory reads of basically any appropriately mapped memory (into MaybeUninit) without immediately invoking UB (and outside of MIRI the vtable is in readable memory for the time being, so it should not be UB to read it). I'm onboard with the idea of not making any stability guarantees about what lives in the vtable, or even where the vtable is stored, or if we even use a vtable at all, but these seem like things that should be implementation-specific, not UB. I think maybe part of the issue lies with the notion of backwards compatibility for the AM:
It's not necessary for the AM to be backwards compatible in the first place. Only the stable parts of the Rust language need to be backwards compatible, so it would be perfectly fine to have vtable access be well-defined in one version of the AM (but have no related stability guarantees in the language) and then for vtable access to be UB in a later version (if say, the vtable really didn't live in memory in that version). |
Here's a summary of the problem: to say something is "unspecified", we have to say what the set of possible choices is that the implementation can pick from. This is required so that programmers can argue their code is correct with every possible choice, and hence with every possible implementation. For For vtables, this is much harder. I sketched it above, but it quickly derailed into a discussion of how to make all the things I was talking about actually precise. Please read the discussion if you want to know the details, I won't repeat it all here. I think it can be done, but it's complicated and requires some fairly advanced PL techniques. Making vtable accesses UB is a lot easier. So unless you have a good reason for wanting the program to directly access the vtable, and a good proposal for describing the set of all possible choices for what happens on a virtual call, I think we should stick with making vtable access UB. Note that just saying the layout of the vtable is unspecified is insufficient, since we probably want to allow introducing indirections, and then everything becomes a lot more complicated.
Unsafe code is not allowed to read from function pointers either. This is similar.
No, that would not be fine. If we have a spec, and some piece of code is correct for all possible choices of unspecified behavior, then we must ensure that this piece of code remains correct as we evolve the spec. If we don't do this, it becomes impossible to write unsafe code that will keep working with future versions of Rust. |
Let's imagine we have an unstable intrinsic
It's perfectly possible: just don't use unstable features.
Hmmm. It seems like there are a lot of things that should not be expected to work in general, but where it needs to work in specific instances (ie. specific versions of Rust, on specific platforms). Like, there exist low-level usecases for reading from a function pointer or a vtable. Which of the following is your view:
|
To put what Ralf has said a different way, the AM does not spec what a particular implementation of rust does, but the base-line for all implementations (which is then parameterized by choices for implementation-defined behaviour, and affected by nondeterministic choices for unspecified behaviour). Thus anything that the AM does specify (outside of definitions for actually unstable features) is by definition stable. Within those constraints it is possible to reason about what the behaviour of the program is on the abstract machine, independant of what implementation of the abstract machine is in use, if you can rule it as stable under all possible parameters and non-deterministic choices. Such code is portable between implementations of rust (whether they be different targets, different versions of the same compiler, or even entirely different compilers).
This isn't the same as
(1) and (2) are exactly the same, in my opinion. In rust, you can't do either of these things, it's UB. Full stop. That being said, if you write code for a particular implementation of rust, then you can rely on both explicit or implicit promises it makes refining the rules of rust (for example, it's particular documented choices for implementation-defined behaviour, or promises for layout of certain types with otherwise unstable layout). At that point, your target is not the rust abstract machine, but a particular implementation, and you lose the portability guarantee. |
Calling a virtual function is not an unstable feature. We have to say what it does, on the AM level, and guarantee that this specification will not change in the future (other than permitting clients to do more things -- but every previously correct client must remain correct). |
I can imagine that, though it seems extremely niche. Maybe one day someone suggests a way to make these operations not UB in the Abstract Machine while also still providing useful reasoning principles. I could imagine something like "for each function and vtable, there is an unspecified number of bytes starting at the given address that may be read. writing to any of them is immediate UB. copying those bytes elsewhere does not make a functional copy of the original function/vtable, it results in a regular boring data allocation". Miri then chooses to set that number of readable bytes to 0, but other implementations could choose differently. This is very different from the status quo in Miri, where you can cobble together a fn ptr, size, and alignment, and this will actually pass as a vtable. I think that is a promise we cannot hold. |
Unspecified behavior and nondet choice are two very different things, btw. Nondet is what I call |
In C++, unspecified behaviour is defined as the nondeterministic aspects of the abstract machine, so unless rust is somehow different in this regard, I'd consider the two synonymous.
Would you be able to share some examples, please? |
Then why can't the standard library rely on the fact that the implementation makes these promises (at least for now) in order to implement
No, but reading from a vtable within the AM could be an unstable feature (which is then used by std). I would like it if we could make "your code may become UB in the future" (ie. you are relying on an unstable feature) and "your code is UB today" two distinct results rather than conflating them. |
I guess Rust is different then. Nondeterminism is a runtime thing. It is an action taken by the program as it runs on the Abstract Machine. The compiler is allowed to "refine" non-determinism by reducing the set from which the choice is taken from during compilation, and even completely resolve the choice to a concrete value. The most canonical example for this in Rust is the address of a freshly created allocation. If we said that struct layout was non-deterministic, that would mean that each time you start a Rust program, it would be allowed to pick fresh offsets then and there. (Or even each time a struct value is created? Non-determinism is an effect, you have to say when it happens. "Struct layout is non-deterministic" is not a well-defined statement.) That is not the specification we want. Maybe you mean that compilation is non-deterministic, but then you have to be more precise and distinguish between non-determinism during compilation and non-determinism during execution of the Abstract Machine program.
We have not decided to make any such promises for rustc yet. Also note that I said "I could imagine" and "maybe one day". I didn't say I think any of these are good ideas or reflect how we think of Rust today. Also we care about running this code in Miri which aims to be a faithful implementation of the Rust Abstract Machine. I don't think the notion of an "unstable AM feature" makes much sense. The purpose of the AM is to specify which programs you can write. Either the AM says my program is fine, then it must stay fine, or it says the program is not fine, then I need to fix it. We could of course have multiple different AMs but that would be a mistake IMO, that's basically forking the language (akin to The standard library runs on the same AM as everyone else. It it okay to exploit layout assumptions there since those are compile-time decisions, but it in my opinion is not okay to have UB in the standard library, no matter how much you know about the compiler. (I know authors of some other compilers disagree, but in rustc this is the policy we follow, AFAIK.) |
On Mon, Jul 18, 2022 at 16:26 Ralf Jung ***@***.***> wrote:
Nondeterminism is a *runtime* thing. It is an action taken by the program
as it runs on the Abstract Machine. The compiler is allowed to "refine"
non-determinism by reducing the set of to choose from during compilation,
and even completely resolve the choice to a concrete value. The most
canonical example for this in Rust is the address of a freshly created
allocation.
You can have unspecified runtime behaviour. In fact, I'd argue that
unspecified *behaviour* is fundamentally a runtime concept, except as it
relates to constant evaluation.
If we said that struct layout was non-deterministic, that would mean that
each time you start a Rust program, it would pick the offsets then and
there. That is not the specification you want to write.
I'm unsure this argument holds. I would argue, that the implementation is
in fact free to alter the layout of structures between executions, bounded
on the fact that layouts can be examined (and thus observed and fixed)
during translation (for example, by a const-eval panic or a type mismatch,
such as the ones used by the const_assert! et. al macros). Thus, the
implementation could alter the layouts of any type which is *not* so
observed between executions or otherwise by translating the program again
between executions (which would occur, for example, implementation was an
interpreter that makes no meaningful distinction between translation and
execution - such is the case with miri).
I would argue that the program observing distinict choices for unspecified
layout would be fundamentally the same as the program observing distinct
non-deterministic choices at runtime, barring the fact that such
observations can alter the properties of the program during translation,
rather than merely during execution (such as making the program ill- or
well-formed).
… Maybe you mean that *compilation* is non-deterministic, but then you have
to be more precise and distinguish between non-determinism during
compilation and non-determinism during execution of the Abstract Machine
program.
Then why can't the standard library rely on the fact that the
implementation makes these promises (at least for now) in order to
implement DynMetadata?
We have not decided to make any such promises for rustc yet.
Also we care about running this code in Miri which aims to be a faithful
implementation of the Rust Abstract Machine.
I don't think the notion of an "unstable AM feature" makes much sense. The
purpose of the AM is to specify which programs you can write. Either the AM
says my program is fine, then it must stay fine, or it says the program is
not fine, then I need to fix it. We could of course have multiple different
AMs but that would be a mistake IMO, that's basically forking the language
(akin to -fno-strict-aliasing in C).
—
Reply to this email directly, view it on GitHub
<#338 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABGLD2ZN6G3JBXUBM3JSRU3VUW4WDANCNFSM5V6EAHEA>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Well that's kindof my issue. If the choice is so binary then there is no room for implementation-specific programs to exist at all, and that is a problem because I believe a lot, if not most, useful programs rely on some behaviour that is implementation-specific. There is a class of programs which will never work on MIRI but which we should not consider to be "wrong". What do we do for inline assembly here? Surely we say that such programs are simply "out of scope" of the abstract machine, without saying they definitely have UB? |
That's not true. As I keep saying, struct layout is implementation-specific (unspecified, whatever you want to call it). I also disagree with your thesis that most useful programs rely on implementation-specific behavior, unless you mean rely on it transitively through the standard library. Outside the standard library me and many others generally consider relying on implementation-specific behavior a soundness bug. But I don't think vtables and vtable access should be implementation-specific, at least not in the form of allowing programs to read from the vtable. Instead what I do in rust-lang/rust#99420 is add some implementation-specific intrinsics that can be specified on the AM in a coherent way. This avoids the need for "implementation-specific UB" or anything like that. Now Miri can fully enforce opaque vtables and run stdlib code that needs to fetch the size from a vtable. |
I don't think this is the right thread for fundamental discussions about the limits of Abstract Machine specifications, but there is a coherent way to integrate inline assembly and C FFI into this picture. (Basically, the author of the code has to "axiomatize" what transition on the AM state is implemented by this "gap in the code". That transition needs to be one that could also have been taken by regular Rust code, to ensure correctness of compiler analyses.) Indeed Miri will not be able to run such code. So yes we could probably have implemented those parts of the stdlib via inline assembly (though the "must be possible in regular Rust code" part could become tricky, if a compiler analysis wants to actually rely on the fact that regular Rust code can never read the size field -- but now that we have an intrinsic for this, that is an operation the optimizer has to take into account anyway). I think an intrinsic is much cleaner though. |
make vtable pointers entirely opaque This implements the scheme discussed in rust-lang/unsafe-code-guidelines#338: vtable pointers should be considered entirely opaque and not even readable by Rust code, similar to function pointers. - We have a new kind of `GlobalAlloc` that symbolically refers to a vtable. - Miri uses that kind of allocation when generating a vtable. - The codegen backends, upon encountering such an allocation, call `vtable_allocation` to obtain an actually dataful allocation for this vtable. - We need new intrinsics to obtain the size and align from a vtable (for some `ptr::metadata` APIs), since direct accesses are UB now. I had to touch quite a bit of code that I am not very familiar with, so some of this might not make much sense... r? `@oli-obk`
make vtable pointers entirely opaque This implements the scheme discussed in rust-lang/unsafe-code-guidelines#338: vtable pointers should be considered entirely opaque and not even readable by Rust code, similar to function pointers. - We have a new kind of `GlobalAlloc` that symbolically refers to a vtable. - Miri uses that kind of allocation when generating a vtable. - The codegen backends, upon encountering such an allocation, call `vtable_allocation` to obtain an actually dataful allocation for this vtable. - We need new intrinsics to obtain the size and align from a vtable (for some `ptr::metadata` APIs), since direct accesses are UB now. I had to touch quite a bit of code that I am not very familiar with, so some of this might not make much sense... r? `@oli-obk`
make vtable pointers entirely opaque This implements the scheme discussed in rust-lang/unsafe-code-guidelines#338: vtable pointers should be considered entirely opaque and not even readable by Rust code, similar to function pointers. - We have a new kind of `GlobalAlloc` that symbolically refers to a vtable. - Miri uses that kind of allocation when generating a vtable. - The codegen backends, upon encountering such an allocation, call `vtable_allocation` to obtain an actually dataful allocation for this vtable. - We need new intrinsics to obtain the size and align from a vtable (for some `ptr::metadata` APIs), since direct accesses are UB now. I had to touch quite a bit of code that I am not very familiar with, so some of this might not make much sense... r? `@oli-obk`
I have a weird question, hopefully it's alright to ask here. So my question would be: #![feature(ptr_metadata)]
#![feature(thin_box)]
#![feature(trait_upcasting)]
use std::boxed::ThinBox;
use std::ops::Deref;
trait A {
fn a(&self);
}
trait B: A {
fn b(&self);
}
impl A for i32 {
fn a(&self) {
println!("a");
}
}
impl B for i32 {
fn b(&self) {
println!("b");
}
}
fn main() {
let x: ThinBox<dyn B> = ThinBox::new_unsize(5);
x.b();
if let Some(x) = try_upcast(x) { // MIRI returns None :(
x.a()
}
}
fn try_upcast(x: ThinBox<dyn B>) -> Option<ThinBox<dyn A>> {
// somehow check that dyn A vtable is a subset of the dyn B vtable
// would be nice to have this as an intrinsic
let fat_ptr: &dyn B = x.deref();
let meta_1 = core::ptr::metadata(fat_ptr);
let meta_2 = core::ptr::metadata(fat_ptr as &dyn A);
let upcast_is_trivial = format!("{meta_1:?}") == format!("{meta_2:?}");
if upcast_is_trivial {
// manual "upcast"
Some(unsafe { core::mem::transmute(x) })
} else { None }
} |
We don't guarantee any encoding of vtables. The fact that the vtable of B contains a valid vtable of A is just an optimization and should not be visible to end users. As such checking that it is a subset should remain UB IMHO. |
I was hoping we could say that the result of checking is unspecified but consistent for one particular implementation. Something along the lines of how
|
No, currently vtables are deliberately unobservable in the program -- any attempt to even read from a vtable using a regular load is UB. I have no idea what the debug printer of If there is a usecase for directly accessing the vtable from the program, that needs to be carefully specified and RFC'd first. |
I understand, but this is a little different. No access to the vtable is happing, just a comparison of the address of the vtable. The debug printer just prints the vtable address. Here's another way of phrasing the question: Is it ok to observe the address of a vtable and if yes is it safe to transmute a |
No, that's not safe either. vtables can be deduplicated, so if two types have the same size and alignment and a NOP drop, they might share the vtable.
That's incorrect, too -- the transmute might fail in the future if the size of the metadata changes. (In the future please open a new issue unless you are sure it is the same question as another issue. Now we have two completely independent discussions mixed in this overly long thread.) |
To make the question more precise, consider this code:
What does the spec say happens at the
(&dyn A)::foo(r, x)
call?Whatever we choose must be strict enough to disallow inserting an extra
println!("AAAAA");
that the user did not write, but also permissive enough to not stabilize the layout of vtables.This is forked off from #328 .
The text was updated successfully, but these errors were encountered: