Skip to content

RFC: Rethinking the design of the crates API #721

@elinorbgr

Description

@elinorbgr

So, I've been thinking about wayland-rs' API again (gotta love my brain, the running gag is still going strong), and I'm dumping these thoughts here, to see if they can serve as a starting point for a positive discussion.

As a preamble, as far as I can tell, the current API is for most users "good enough". Although some pain points remain (in particular regarding thread-safety), it is mostly usable and reasonable. As such, I don't think it's worth going into yet another redesign of the API unless we have a strong confidence that it would make things significantly better. This is why I'm opening this "RFC" issue: to gather feedback on those ideas, see if/how they can be fleshed out, and evaluate if it is a direction worth pursuing or not.

Design of the backend

In the latest big redesign, I've tried to make wayland-backend into an "as low-level as possible" wrapper around both a rust implementation of the protocol, and libwayland. It turns out that I could have gone deeper into this "going lower-level" route by relaxing a constraint I had set myself at the time: I tried to make the backend not only rust-safe, but also make it catch a lot of footguns that could trigger protocol errors. I considering relaxing that.

The backend would be reworked to completely remove the ObjectData trait, and the core API it would expose to process messages would be something akin to:

impl Backend {
    fn incoming_messages(&mut self, f: impl FnMut(Message)) { ... }
}

The user thus provides a callback that is repeatedly called for all incoming messages. Free to them to use it to process them in real time, or just store them into a buffer for later processing.

At the level of the backend, objects are only represented by their ObjectId, a handle that is Eq + Hash + Clone, and keeps representing uniquely the same object, even after it is destroyed.

The backend no longer automatically tries to track object destruction for you, and will expose a method like:

impl Backend {
    fn destroy_object(&mut self, id: ObjectId) { ... }
}

Similarly, the backend no longer checks the signature of outgoing messages, and will happily serialize to the wire whatever you give to it. This means that it would be easy to trigger protocol errors (but no rust-unsafety) if you use those API wrong.

Associating data to objects

An important need is the capacity to associate data with specific instances of Wayland objects. With the removal of ObjectData from the backend, we need a new mechanism to cover that.

I'm thinking of introducing a DataMap container, which would in essence be a glorified HashMap<ObjectId, TypeMap> (I'm assuming the runtime cost of the map lookup is negligible.). This container has in itself no synchronization backed into it, and the user would be free to use more than one if practical or relevant. Data is not automatically removed from the container, and the user needs to actively remove it when the relevant object is destroyed.

This puts data management in a much more manual fashion, as a trade-off for removing a large part of the friction related to thread-safety that was caused by storing the user data inside of the ObjectData of each object.

Dispatching messages

Probably the most central difficulty in API design for the Wayland protocol is the dispatching of incoming messages to the bits of logic that need to handle it. In this new potential design, the backend no longer does anything regarding that, so all must be done in wayland-client and wayland-server (or any other thing built on top of wayland-backend).

As a reminder of the necessary or desirable properties of such dispatching:

  • The ordering of messages must be preserved (within a client server-side, and within an event queue client-side)
  • Even when two objects are instances of the same interface, it must be possible to dispatch their incoming messages to different processing logic
  • Most interactions with the protocol (sending messages, creating or destroying objects, etc..) must be possible concurrently with a dispatching session
  • Ideally, a minimal amount of plumbing code would need to be written by the user

My current idea regarding that would be a system comprised of modules somewhat similar to an actor model structure: blocks that take messages as input (either raw messages from the Wayland socket, or higher-level user-defined enums), standardized with a trait, and that can be connected into one another to create a pipeline: a module keeps a reference (via owning, or an Rc<_>, etc..) to the nest module in the pipeline, and invokes it as necessary.

The entry point for this system would be integrated into the DataMap: each object could be associated with a handle to the initial module of the chain that needs to process its messages. As a whole, the full pipeline of modules would thus be a DAG. We could thus have a method like:

impl DataMap<E> {
    fn dispatch_message(&mut self, msg: Message) -> E { ... }
}

Where the type parameter E represents the final return type of the pipeline, to allow the representation of messages that need to be processed outside of the pipeline logic. Each module of the pipeline would have mutable access to the DataMap (or at least a subset of it) and maybe we can fit something like the global &mut State we are currently using as well.

The core idea of that is that this pipeline can take any shape the user wants, for example:

  • Each object is associated with a single module that does its processing (kinda similar to the current ObjectData)
  • Each object is associated with a module that parses its message into a common enum that is directly returned by dispatch_message() and handled directly by the main loop (like with a huge match { ... }
  • An hybrid setup where several modules are chained, each doing processing at its level (such as state-tracking) and generating higher-level messages to be processed down the chain.

In this, the modularity of crates like SCTK or Smithay would be achieved by providing such pre-implemented modules that a user could integrate in its pipeline.

Questions

So, this design is still quite high-level, and there are a lot of details to iron out, but I'd like to get the general feeling of the current users of wayland-rs with theses ideas. In particular: compared to what you are currently doing with wayland-rs, do you think this would make your life easier? Harder? What does this evoke you?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions