Blog series: Dyn async in traits

Though oft repeated, this is not true today.

1 Like

In the blog post Niko suggests converting impl Trait method arguments to dyn Trait when the method is called on a trait object ("we must have only a single copy for the vtable"). But is there any reason why this has to be the case? Couldn't the vtable include separate monomorphized variants of the method for all the different concrete impl Trait types that end up actually being used? That way, the choice of whether to use dynamic dispatch for the impl Trait argument is independent of whether Self uses dynamic dispatch.

This isn't a query with a well-formed answer. When I compile library A that uses dyn Trait, I cannot know the full set of generic instantiations in order to know the vtable layout. When dynamic linking is involved, it's not possible to know until runtime what types are used, based on what code is linked in dynamically.

That's not to say it couldn't be potentially beneficial as an optimization to monomorphize some common instantiations into the vtable. However, if it is done, it should probably be handled by some explicit request in the crate that defines the request. (This of course can be done manually at the definition and call site by just providing a default implemented method that delegates to the generic one.)

1 Like

Don't all methods with generic parameters have the same problems? What specifically is special about the case where Self is a trait object? Can't determining the final vtable structure be delayed until the final binary is produced?

Yes, which is the reason why the proposal is to polymorphize the methods to allow them to work at all on trait objects.

In general, no, because at (dynamic) link time the compiler (rustc, llvm) is done, and just the (dynamic) linker (ld, lld, mold, link.exe) is going work anymore.

Even when everything is statically linked, waiting would require every method that uses dyn Trait to wait until final binary compilation to be compiled, such that it knows the vtable layout in order to use the vtable to call methods.

If you want inlining to still work, this basically means a full rebuild every time, and not being able to reuse the result of compiling a library, because you called a generic method on a trait.

// In crate `base_lib`

pub trait ArgTrait;
// In crate `foo_lib`

use base_lib: ArgTrait;

pub trait FooTrait {
    dummy_method(&self, arg: impl ArgTrait) {}
}

#[derive(Default)]
pub struct FooStruct {};

impl FooTrait for FooStruct {}
// In binary crate

use base_lib::ArgTrait;
use foo_lib::{FooStruct, FooTrait};

struct ArgStruct {}

impl ArgTrait for ArgStruct {}

fn main() {
    FooStruct::default().dummy_method(ArgStruct {}); // [1] Doesn't work if foo_lib is dynamically linked
    (&FooStruct::default() as &dyn FooTrait).dummy_method(ArgStruct {}); // [2] Doesn't work at all currently
}

I don't have experience with dynamic linking in Rust, but AFAIK in the above code [1] already only works if foo_lib is statically linked. So impl Trait argument monomorphisation in methods on trait objects (as in [2]) being incompatible with dynamic linking is not a huge issue because this is impossible even in case [1] where Self is a concrete type.

As for the static linking case, I don't see why this (case [2]) should require a full recompilation—all you would need to do is to change the value of offsets from the start of the vtable, no?

Doing that requires recompiling all places where said offset is used. Binary patching won't work as LLVM may have for example const folded it away. Also LLVM doesn't record where the offset is used in the object file.

3 Likes

Not if the offset desugars to/is represented as an extern static or some such.

isn't this rather a bug than a feature?

Who defines the static? With --emit obj there is no place we can stuff a definition as we don't invoke the linker, nor create a final staticlib bundle which are the only places where we can know the whole crate graph.

1 Like

Arguably being able to instantiate a dyn Trait that is not Trait is a bug; I filed it as such for the reasons noted in the issue. But it's also been stable since 1.0, so changing it is a breaking change. It's possible the breakage would be small enough to get away with; I doubt many people are actually creating such instantiations.

For your phrasing to pedantically apply:

I would like to preserve the property dyn Trait: Trait for all object safe traits.

The definition of "object safety" would need to be appended to encapsulate something along the lines of: any where-clauses on the trait (which don't equate to supertraits) must be implemented by the programmer or automatically implementable by the compiler. (Or that such bounds constitute object unsafety, or that dyn Trait ignores such bounds, but those seem untenable to me.)

But if I understand, your suggestion only cares about instantiated dyn Trait.

Ah, that makes sense. Maybe it would be possible to use dynamic dispatch only when --emit obj is used? That would add a lot of extra complexity however...

You can combine --emit obj with --emit link. It will the produce an object file and regular rlib/staticlib/dylib/executable. Also it is already unfortunate enough for debugging optimization issues that --emit asm (eg used for showing assembly on the rust playground) and --emit obj force a single codegen unit, which can affect optimizations. Having a completely different input to the optimization passes would make this even more puzzling.

1 Like

Yes, I want to tweak object safety rules to allow more traits. Basically, I want to allow by-value self, and abstract types returning, things like type equality constraints are still not object safe.

If we can unsize a value to a dyn Trait, then it already implies that all requirements are held.

Right now I can't imagine any unproblematic where clause that doesn't imply any super traits.
(not about the likes of trait Foo where {size_of::<Self>() > 9} == true); I don't think we want such traits to be ever object safe...

They're safe today; consider:

trait Trait where for<'a> &'a Self: SomeOtherTrait { /* ... */ }

What about linking in the vtable offset with extern_weak linkage, and then using dynamic dispatch only if the symbol is null?