Require Zero-Sized Type

Hello rust hive-mind,

Is there a way to limit a trait to only being supported on zero-sized types? (Sort of like a !Sized bound instead of ?Sized.) I've done some searching, and the only references I can find are related to either handling zero sized types as opposed to requiring them, or general discussions about ZST support in relation to integration with other compiler features. (Which I will not require, as this is an internal implementation detail for the system in question.)

I'm working on a system that doesn't work if there is any data associated with a value, but it still needs dynamic dispatch on an interface. The vtable of a trait object is ideal for my needs, and I have it working properly via std::raw::TraitObject, but improper use will result in a run-time assert. I would rather the mistake was caught at compile time.

Any suggestions?

2 Likes

There is no way right now, and it might not be a good idea. If someone has a struct like this:

struct MyStruct {
    _private: (),
}

Its a ZST, but they've intentionally designed it to be forward compatible to add fields to it. Usually, changing the size of a type is not considered a breaking change.

(Also, ZSTs are not !Sized - they have a size & its zero. Dynamically sized types, like slices and trait objects, are !Sized.)

On to your actual problem, its not as ergonomic as using a trait, but you could create a "vtable" yourself using a struct of function pointers:

struct Vtable {
     foo: fn(i32) -> i32,
     bar: fn(&mut String),
     // ...
}

If they add fields to it, I need them to get an error. The type in question must be stateless for my system to work properly.

A struct vtable would work, but is far less ergonomic because it would still need a global instance somewhere so I can pass around a pointer to it. I would basically have to implement it under a macro in order to be useable, and I would rather avoid that. Using a synthesized trait object instead is basically just way easier and provides all the same guarantees. (Assuming your type is always zero sized, of course...)

The problem is when a library adds a field to a type. Now you get a build error you can't fix except by refusing to accept a semver compatible upgrade. This sounds like its not particularly relevant to your system, but its a consideration for the language as a whole. In any event, the fact is that there's no way today to require a type be a ZST.

Rather than checking the size of the struct, couldn't you simply try to just make statefulness ineffective?

Just to throw an idea out there:

pub struct VTable {
    pub foo: fn(&A) -> &B,
    pub bar: fn(C, C, C) -> Electro<'static>,
    ...
}

pub trait Trait {
    fn vtable(&self) -> VTable;
}

Now, of course, somebody could still sneak state in there by e.g. having a RefCell<bool> and putting different function pointers in the VTable based on that flag. Perhaps you have additional constraints in your situation that may make this a non-issue; but if it is a problem, maybe we could go one step further:


Well, okay. This next idea is a fair bit more than "one step further." But hear me out. This is basically a proof of concept for using two traits to simulate object-safe static methods.

The idea is:

  • Make it impossible for anyone to implement Trait outside the crate or module where it is defined.
  • Have a second trait, TraitImpl which is the one people implement. This trait doesn't have to be object safe. Either give it all static methods, or require people to implement it on a unit-like type of your choosing.

I'll assume your implementors are in a different crate from the trait. (they don't have to be)

upstream/lib.rs

/// The trait you *implement*.
/// However, this isn't the trait you *use*.  See Trait.
pub trait TraitImpl: Sized {
    fn foo(u32) -> i32;
    fn boxing_day(Vec<u64>) -> Box<Box<Box<&'static [((), u64)]>>>;
    // fn ...
}

/// A type that you can't construct.  Ha ha!
///
/// This prohibits you from writing your own impl of Trait outside this crate.
pub struct Token(());

/// The trait you use to make a trait object from TraitImpl.
pub trait Trait {
    fn foo(&self, x: u32) -> (Token, i32);
    fn boxing_day(&self, x: Vec<u64>) -> (Token, Box<Box<Box<&'static [((), u64)]>>>);
    // fn ...
}

// Generic impl of TraitObj that forwards to 'impl Trait<Type> for GreatJustice<Type>' impls.
// Any other impl besides this one is only capable of panicking.
impl<A: TraitImpl> Trait for A {
    fn foo(&self, x: u32) -> (Token, i32) {
        (Token(()), <A as TraitImpl>::foo(x))
    }

    fn boxing_day(&self, x: Vec<u64>) -> (Token, Box<Box<Box<&'static [((), u64)]>>>) {
        (Token(()), <A as TraitImpl>::boxing_day(x))
    }
}

downstream/main.rs

extern crate upstream;

use upstream::{TraitImpl, Trait};

struct Struct;

impl TraitImpl for Struct {
    fn foo(x: u32) -> i32 {
        (x * 2) as _
    }

    fn boxing_day(_: Vec<u64>) -> Box<Box<Box<&'static [((), (u64))]>>> {
        Box::new(Box::new(Box::new(&[])))
    }
}

fn main() {
    let _: &Trait = &Struct;
}

Edit: Okay, technically this still has a loophole in that you can write an impl of Trait by calling the methods of Trait defined on another type to receive a Privileged value. I need a logician!
Edit: Vastly simplified the implementation. Loophole is still present.

In my case, I can assure you that I need implementations of the trait to be stateless, or it will break guarantees elsewhere in the system. Even immutable state would allow it to be used incorrectly.

You really need to be more specific about the kinds of risks you are facing.

  • When you talk about state, do you only mean data? To me, "state" extends beyond data, and well beyond what you can control within Rust's type system. Any function can just read a file from the file system.
  • Would it be dangerous if a type with nonzero size implemented the trait and all of its method bodies were panic!() or loop { }? This is basically what I did in my second example, modulo the loophole I am trying to fix.

Edit: Hm. I just noticed that when I "simplified" the second example, I also eliminated a potentially key property it originally satisfied, which was that Trait was only impl'd for a zero-sized type. (rather than the same types that impl TraitImpl) That is false. Both versions impl Trait for user-defined types.

What if you just bound it with Default and never actually let them pass one in, instead creating a new one every time. Then they can't give you any state to hold...

That was one of my first ideas. Default is not object safe.

1 Like

I’m curious to know what you’re doing that requires the data portion of the fat pointer to be non-existent (essentially). Why can’t it be ignored if the trait functions don’t take self? Or why can’t the trait itself be stateless in its API? I must be missing something.

1 Like

This sounds a bit like an X-Y problem. What you really want is to statically ensure a type is 100% state-less (not just immutable), but what you are asking for is a type which has zero size. These two things aren't the same.

Here's a trivial example which has zero size yet uses mutable state (I'm on mobile so it probably won't compile).

struct ZeroSized;

impl Foo for ZeroSized {
  fn bar(&self) -> usize {
    static BAZ: AtomicUint = 0;
    BAZ.fetch_add(1, Ordering::Relaxed)
  }
}

I don't know if it's ever going to be possible for you to statically make sure something doesn't contain state using the type-system. The operating system itself is one massive bundle of state and your code will run on an OS, therefore there's always going to be the ability to have state.

A better solution would be to try and make it so it doesn't matter if an object contains state. For example, you could design your API in such a way that all processing/mutation gets "recorded" by applying it to an object you pass in.

Also, I wouldn't recommend playing around with std::raw::TraitObject. Using unsafe makes it even easier for you to break this "no state" constraint...

2 Likes

As I mentioned in another message, you are correct, I am after a stateless type. Obviously that's not entirely possible due to global state access. Expressing that via a zero-sized type constraint comes close to communicating that requirement, but I can currently only implement that via documentation and a runtime assert.

You're assuming an awful lot about what I'm trying to accomplish with that statement.

If the proper behavior of the rest of the system requires that processing to be idempotent, that can only be guaranteed if the processor is stateless. Immutability comes closer to ensuring idempotency, but that's relatively easy to circumvent via RefCell & etc. Requiring a zero sized type (or more accurately, a stateless vtable) communicates much of the idempotency requirement, and provides the advantage of not requiring an allocation and state transmission across processing contexts.

I really do want implementations of this trait to be zero sized, and I would prefer to express that as a constraint on the trait, but that doesn't appear to be possible.

Okay, so you really do need statelessness.

Here is my next and possibly final question:

Do you have unsafe code relying on this property to prevent Undefined Behavior?

Given you can’t require true statelessness even if you could require a ZST, maybe add unsafe trait Stateless {} and require that on the trait as well. Basically, this becomes a marker like Send/Sync, albeit without the OIBIT part.

4 Likes

I'm not quite sure what you mean. I do have a small amount of unsafe code to extract & encapsulate the TraitObject manipulations into an opaque type. Is that what you mean?

An extra stateless trait is a good idea. Why specify it as unsafe though?

If a trait represents a property that's important to safety, in a way that the compiler can't enforce, then making it an unsafe trait is the way to put that onus on the one implementing the trait.

8 Likes

I am asking if your code may invoke Undefined Behavior if an implementation is stateful.

Because if that is the case, then @vitalyd's suggestion is more or less your only option. Because you ultimately cannot prevent people from having a stateful implementation (even if you succeeded at requiring ZSTs!), you are obligated to mark this trait as unsafe, and to explicitly document the conditions for unsafety, so that consumers are forced to use unsafe when implementing it (and are therefore aware of the danger that lurks about).

/// A traity trait.
///
/// # Safety
///
/// For any given type, all calls to a method on the trait must return
/// the same output (that is to say, the implementation must be stateless).
/// Otherwise, behavior is Undefined.
pub unsafe Trait {
    fn method(&self, arg: Arg) -> Output;
    ...
}

(of course, I have only made an assumption about the precise property that you require, and you should adjust the documentation as appropriate)

1 Like

I understand. Thanks, that's a good tip.

1 Like

The discussion opened my eyes to unsafe marker traits. I doubt I will need them anytime soon but it's good to know about them.

Thanks all :smile:

2 Likes