Opaque pointers and references


#1

&opaque T (incl. &opaque self) and *opaque T would be types that let the callee or w/e be aware of a valid object, but not be able to do anything with it except call other opaque methods.

The main use-case is for when you need trait objects and static methods, such as in the eventbus crate.

trait Event {
fn cancellable(&opaque self) -> bool:
}

you can’t (safely) do anything with opaque pointers, except use their type or their vtable(s). in fact, the compiler should optimize them into ZSTs with vtables. they are NOT FFI-safe.

the whole <object as Trait>::static_method() thing from the other thread was the wrong approach tbh and I should’ve thought about it for a little longer…


TypeFns, variadic generics, compile-time reflection, macro-like fns
#2

There is an RFC for opaque types.


#3

These aren’t types. They aren’t even FFI-safe. These are just so you can do:

trait Event {
fn cancellable (& opaque self) -> bool {
    // cannot use self here, except to call other opaque (static) methods
    // this is literally so that mutating self doesn't change the return value of cancellable
    false
}
fn cancelled (&self) -> bool {
    false
}
fn set_cancelled(&mut self, _cancel: bool) {
    panic!("not cancellable");
}
}

trait MyEvent : Event {
}

fn handle_myevent(&mut dyn MyEvent) {
// stuff here
}

#4

I really don’t see how this different from associated consts. It sounds like you want to be able to have some sort of fn foo<dyn T>() thing, where foo is not monomorphized on T but instead passed some kind of phantom vtable for T's statics?


#5

I just want trait objects with a static fn. Does this not make sense?


#6

You’re probably not explaining it very well, because it sounds contradictory – like you want a statically dispatched method to be dynamically dispatched. But this is probably quite a distance from the actual problem you’re solving, because one wonders why normal methods, associated consts, or specialization doesn’t work for you.


#7

Because,

trait Event {
const CANCELLABLE: bool;
}

let foo: &mut dyn Event = whatever;

doesn’t (currently) work.

I very much want dynamic dispatch of objectless (selfless) methods, and that’s not contradictory. you’re just… agh, not thinking, I guess.

I’m a practical person, I don’t follow etymology, I follow usage. you should give it a try, it’ll let you break a lot of barriers.

static methods are methods that depend on a real (concrete) type but not on an object of that type. I want that with dynamic dispatch - I want metadata of the concrete type that got coerced into a trait object, but ignoring the concrete object.


#8

So why don’t you just ignore the concrete object? If you call Event::cancellable(), you either have to pass it an argument or you don’t. If you don’t, it isn’t object safe because there’s no object data to dispatch with, and if you do, then you have an object and you can already just ignore it at your leisure.


#9

The issue is that I want to disallow cancellable to depend on the object’s data. It should only depend on its type. And I need dynamic dispatch/trait objects.

Opaque pointers just mean “we have an object, but you’re not getting its data”. It still allows vtables and dynamic dispatch, but removes the data.


#10

Ofc, we can do all of this with PITs, but we don’t have PITs either:

trait Event {
// has a vtable and data, but it's UB (and generally disallowed) to use self's data
// effectively equivalent to an &opaque pointer
fn cancellable(&self()) -> bool {
    false
}
}

#11

The question is not only can you do it, but should you do it. You’re taking statically computed data and basically forcing it to be accessed through dynamic dispatch.

Firstly, this handcuffs your users. If the data really is state-dependent, then why shouldn’t they be allowed to express that? Why not tell your users how the data will be used and let them provide impls to their own advantage?

Secondly, it is “weak security”, because accessing an object through a trait object confines you to an interface anyway. If Event::cancellable(&opaque self) can’t depend on object state, the second you call Event::cancel(&mut self), the user is free to look at object state anyway. So why bother? Users control their object state, they can make anything they want depend on it.

Thirdly, this is an anti-pattern. If the data is statically determinable, you should encode it in static structures so that it can be determined at compile time:

#![feature(specialization)]
pub trait Event {}
pub trait CancellableEvent {}
trait MaybeCancellableEvent { default fn is_cancellable(&self) -> bool { false }}

impl<T: Event> MaybeCancellableEvent for T {}
impl<T: Event + CancellableEvent> MaybeCancellableEvent for T { fn is_cancellable(&self) -> bool { true} }

Here, you already established that the result of the method call can’t depend on object state because you don’t even need an object to get the result. But if you have a dyn MaybeCancellableEvent, you can do exactly what you want. Without forcing your users to implement any weird methods at all.

Lastly, this is extremely niche. It still doesn’t solve any other problems with object safety, so you can’t do things like return Self or use generics.


#12

I don’t need to return Self or use generics, like, at all.

Also, I cannot use specialization. These traits and objects and methods are used within public macros, which would require the trait to be public, so specialization would still allow the library to override the whole thing. It also wouldn’t work on stable.

Guess I should just specify that it’s a logic error for the cancellability to depend on the data. This would be almost like making it UB, and I wanted it to be UB, but you don’t want it to be UB, so… /:

You cannot find a solution for my specific use-case that doesn’t involve being able to call static (or, alternatively, opaque) methods - or being able to access associated consts - using dynamic dispatch.


#13

Neither of these solutions works on stable, so that’s not a differentiator.

You cannot find a solution for my specific use-case that doesn’t involve being able to call static (or, alternatively, opaque) methods - or being able to access associated consts - using dynamic dispatch.

I’m not convinced that you haven’t purposefully over-constrained the problem so that there are no available solutions to it.


#14

The problem: cancellability is a property of the concrete/backing type, not of the instance/variable/object. otoh I also need trait objects.

They’re two simple constraints, the fact that I can’t have them both says a lot about the language.


#15

You could restructure your event trait to:

trait Event {
    // normal event stuff here
}

trait CancelEvent: Event {
    fn cancel(&mut self);
    fn is_cancelled(&self);
}

Or you could do something like

trait Event {
    // normal event stuff here
    fn cancel(&mut self) where Self: Cancel;
    fn is_cancelled(&self) where Self: Cancel;
}

trait Cancel {}

And you will have your behavior. The ability to cancel an event is a property of the type. Seems simple enough to me.


#16

The former doesn’t work with generics that need to choose when to stop event processing based on cancellation. The latter isn’t objectifiable.


#17

Then put it in the type. Why do you want to dynamically call functions to get static properties of types? Nothing else in the language works that way, because it is fundamentally inefficient.

There are many ways to satisfy these constraints. But you keep ‘slipping’ your other constraints around so that no alternative design will work.

enum EventObject<'a> {
    Cancellable(&dyn CancellableEvent),
    NonCancellable(&dyn NonCancellableEvent),
}

You need dynamic dispatch, but you do you really need it where you’re trying to put it? You keep describing the solution, but you need to describe the problem. (I some way other than “I don’t have the exact solution I ask for.”)


#18

I’m also curious: what other languages offer a similar feature? I don’t think there are any.

The “standard” implementation in e.g. Java would be just to have a interface method, and state that it’s an error for the result of the method to change.

I also don’t see the value in preventing an event builder having a set_cancellable fn.

Without describing the how (dynamically dispatched associated data/fn), what are you trying to achieve? (How would you achieve it in another language?) What benefit does it have over the simple solution?


#19

My crate - eventbus - provides an event bus.

  • An event bus is an object with a post method (or something equivalent), that takes an event and dispatches that event to a bunch of handlers.

    pub fn EventBus::post<T: Event + ?Sized>(&self, event: &mut T)
    
  • Some events are nullifiable (can be mutated such that it results in a null action), cancellable (stops calling further handlers), both, or neither.

  • Since nullifiability doesn’t affect the workings of the event bus, it need not be handled by the event bus. However, cancellability does affect the event bus, so the event bus needs to be aware of it somehow.

  • Cancellability, like nullifiability, is a property of the type, not of the object. If an event type is cancellable, all events of that type are cancellable. If an event type is nullifiable, all events of that type are nullifiable (especially because of &mut T so you can just replace a “non-nullifiable” with a “nullifiable”).

  • It’s often desirable to allow alternative implementations of an event. For example, consider a hypothetical NoteBlockPlayEvent and AdvancedNoteBlockPlayEvent:

    pub struct NoteBlockPlayEvent {
        note: u8, instrument: EnumInstruments,
    }
    pub struct AdvancedNoteBlockPlayEvent {
        compat: NoteBlockPlayEvent,
        instrument: String,
    }
    // impls omitted
    

    If using structs like this, you have a few options:

    • Don’t post NoteBlockPlayEvents for AdvancedNoteBlockPlayEvents (not the nicest - I’ll explain this down below)
    • Store the original EnumInstruments and only change the String instrument if it has changed - this isn’t good enough, as the inst could be changed by one handler and then changed “back” by another, when it should no longer map to the same String.

    But, if you instead use traits:

    pub trait NoteBlockPlayEvent : Event {
        fn get_note(&self) -> u8;
        fn get_inst(&self) -> EnumInstruments;
        fn set_note(&mut self, u8);
        fn set_inst(&mut self, EnumInstruments);
    }
    struct NoteBlockPlayEventImpl {
        note: u8, instrument: EnumInstruments,
    }
    pub trait AdvancedNoteBlockPlayEvent : NoteBlockPlayEvent {
        fn get_adv_inst(&self) -> String;
        fn set_adv_inst(&mut self, String);
    }
    struct AdvancedNoteBlockPlayEventImpl {
        note: u8, compat_instrument: EnumInstruments,
        instrument: String,
    }
    // impls omitted
    

    Then you can have the following properties:

    • You can trivially post an ANBPE as an NBPE.
    • ANBPEI can set the String instrument appropriately when NBPE's set_inst is called, and we don’t need to store the original EnumInstruments anymore. Like this:
      impl NoteBlockPlayEvent for AdvancedNoteBlockPlayEventImpl {
          fn set_inst(&mut self, inst: EnumInstruments) {
              self.compat_instrument = inst; // update compat instrument, used by get_inst
              self.instrument = InstUtils::map_to_string(inst); // update stringy instrument
          }
      }
      
  • If you want to use traits, you need to be able to have trait objects.

  • And as I said previously, the concrete type has some properties we need to access.

As it says on the Cargo.toml’s description field, eventbus is heavily inspired by the MinecraftForge event bus. It has many/most/all of these properties: cancellability is a property of the type, you can extend and override stuff like this, etc. I’d like to have all those same properties in Rust.

Also, I mentioned something about “Don’t post NBPEs for ANBPEs” not being the nicest. The reason for that is these: https://ftbwiki.org/Arcane_Ear_(Thaumcraft_3) // https://ftbwiki.org/Arcane_Ear_(Thaumcraft_4)

This post is straight up based on this: https://github.com/eNByeX/NoteBetter

One of the features of that mod was interoperability with Arcane Ears. This is a mod I’ve put a lot of time into, so even tho the last commit was a long time ago, I still remember how it was put together. (in fact I didn’t check these once as part of writing this post!) So, basically, everything I’m saying here? it’s actually very useful, and has a history of practical usage behind it.


#20

Question: why do you want the cancellability of an event to not ever depend on the state of the event itself? What will break if you let someone have an event that cannot be cancelled after a certain point of no return of the event?