Skip to content

2020, [piegames]: An idiomatic API

piegames edited this page Nov 23, 2020 · 1 revision

You may want to read the previous design notes first because I'm referring to the code they describe.

What I think an API should look like to the user

The event-driven API design with state machines, as is used in the core module, did not turn out to work very well in my opinion. The public API that resulted from this involved a lot of code to invoke, with a lot of variants to uphold. (Look at the Python API for how to use its Transit object for an example of what I mean. Though it's a lot better in Python than in Rust due to the different type systems).

The API style I aim for (and I think is more idiomatic Rust) can be seen in async-tungstenite as an example:

  • There is code to set up a connection
  • Once a connection is established, it is represented as a struct (WebsocketStream) to interact with
  • Drop it to close the connection, maybe there's an optional explicit close method

The key benefit of this separation is that no setup or teardown things need to be handled during the connection.

Changes made to the API/IO layer

For Wormhole, this means that events like GotCode or GotWelcome need not be handled after the connection has been made. Sadly, the connection setup is not as simple as a method to call. Instead, it is split into two parts. The connect_to_server method will give you the response of the handshake and a struct on which you can call the second part. The transit API is built in a similar way.

In core, I kept the existing state machine / events system mostly intact, but with major modifications to the "outer world communication". First of all, there is no blocking/async/tokio abstraction anymore. At the moment, everything is async_std down the line. I separated the API and the IO layer (which befire where both combined in the WormholeWrapper I think) and moved the IO layer into the Wormhole. The API of core is still getting ApiEvents and firing ApiActions. But instead of making this methods of a struct, it simply exposes a Sender and Reveiver, respectively.

The "sane wrapper" around core is in the root module itself. Basically, the stream and sink to the API are used to handle all handshake-related events. After this, it wraps both in a layer that only exposes Vec<u8> – message events. The user can only send and receive messages to the other client and none of the other event types. They are all hidden by the API.

As before, the Wormhole gets events from the outside and responds with actions to perform. The asynchronicity comes from the IO layer: it may receive messages (and thus produce events) at any time. I see two possible solution to this: either give both the library caller and the IO a Arc<Mutex<Wormhole>> or the like, so that both can take turns at processing their events. Or to put the Wormhole in an own task and communicate with both via channels. I chose the former solution because the first one wouldn't solve the problem of the produced actions (API events may produce IO actions and vice versa). Therefore, we run the Wormhole in what is called the "event loop".