Show full content
This is the second part in the series on writing a QNX resource manager in Rust. See the first part for background information on message passing and resource managers in QNX.
This post will describe the design I came up with for writing a resource manager in Rust. It is neither complete, nor optimal, and I am certain things will change as both myself and others gain some experience in writing resource managers in Rust. Nevertheless, we have to start somewhere…
DisclaimerI am a Rust novice (or noob, as the cool kids would say). It is quite possible that the approach I have taken in implementing a resource manager is anywhere between sub-optimal and completely misguided. Comments from bona fide Rust experts (or would-be experts) are welcome, either on the blog post or in GitLab.
StructureThe code is divided in two. The qnxmsg module provides generic message handling, and will eventually become a crate of its own. The server module implements the Raspberry Pi GPIO server, which includes hardware access to the GPIOs and the implementation of the I/O message handlers.
At the heart of a resource manager is the ability to handle messages sent by clients. To that effect, the resource manager needs to
- receive messages on a channel;
- interpret the messages;
- act on each message type;
- reply to the client.
The Channel structure is a simple wrapper around a channel ID, as returned by a call to ChannelCreate(). The implementation provides a receive() function, which calls MsgReceive() on that channel, populating a MsgBuf structure. That structure is just an array of bytes, whose size anticipates the longest message that the server can handle. Once initialized with data returned from a call to MsgReceive(), the server can determine the type of the message from the first two bytes, and then reinterpret the buffer using the concrete type of the message. This reinterpretation, as provided by the get_data_as() generic function, ensures that the data received from the client is sufficiently large for the message type, preventing one common bug in resource managers.
The buffer structure MsgBuf is an example of the benefits of Rust over C when it comes to data access. Whereas resource managers written in C are prone to out of bounds access to the message buffer, especially once it has been cast to a concrete message type, Rust makes accidental illegal access impossible. You can still crash a program with out of bounds access (not every bug is caught at build time), but overwriting adjacent memory is not an option.
The message_loop() function provides the fundamental structure of the resource manager. Each call to message_loop() receives and handles one message or one pulse (asynchronous notifications received in band with messages). Once a message or a pulse is received, the function decodes its type, and then invokes a handler for this type. These top-level handlers are implemented in the iomsg sub-module. Each handler is responsible for reinterpreting the message buffer as the concrete message type, and for invoking the resource-manager-specific implementation of that handler (e.g., the handler for the _IO_READ message invokes the implementation of read() on a Raspberry Pi GPIO pseudo-file). Some top-level handlers perform more work. For example, the top-level handler for _IO_CONNECT extracts the path safely from the message as a string.
The message loop implementation also handles combine messages, which are another source of bugs in traditional resource managers. Combine messages allow certain combinations of messages to be performed as a single transaction, primarily for performance. For example, a stat() call is implemented as a combination of _IO_CONNECT, _IO_STAT and _IO_CLOSE, while a pread() call is implemented as a combination of _IO_SEEK and _IO_READ. Handling combine messages can cause problems with C resource managers, as the handlers are invoked with an offset into the message buffer, and must account for that to avoid out-of-bounds access. The Rust implementation takes care of that.
As explained in the previous post, QNX messages are synchronous, with every interaction between the client and server consisting of two messages: one from the client to the server (the request) and one from the server to the client (the response, or reply). This two-way interaction has not had a proper name historically (or, at least, I did not see one over the years), and so I decided to refer to it as a “transaction”. The kernel identifies a transaction with a value referred to as a “receive ID”, as returned by a call to MsgReceive(). This receive ID, which I refer to as the transaction ID, is then used in subsequent calls by the server to functions such as MsgReply(), MsgError(), MsgRead() and more. Each of these functions operates on actors (the client thread) and data involved in the current transaction.
The Transaction structure carries a few bits of information beyond the ID. One is the messsage information structure, filled by the kernel and returned by the MsgReceive() call. This structure includes the IDs of the client (process and thread), as well as the lengths of the request and the response buffers.
Importantly, the Transaction structure keeps track of whether the server has already replied, by maintaining a state for the transaction. A common source of problems with resource manager implementation is the double-reply: in an attempt to make developer lives easier, the C resource manager framework abstracts the concept of a reply, allowing handler functions to return in a way that tells the framework to handle the reply on behalf of the handler. However, if the programmer is unaware of this, or is just not careful, a handler can reply directly, while still instructing the framework to reply on its behalf. The Rust implementation addresses this problem by forcing replies (both successful and error indications) to go through the Transaction object. Once one of the reply*() variants, or the error() function, has been invoked, the transaction is marked as terminated, asserting on any attempt to call such a function a second time.
A node is any entity that is associated with a path. In the case of the GPIO resource manager, there are three types of nodes:
- the directory
/dev/gpio; - a per GPIO pseudo file
/dev/gpio/<PIN>, where ` is a number between 0 and 63; - a pseudo file
/dev/gpio/msg.
The msg node provides the main programmatic interface to the resource manager, while the <PIN> nodes are useful for quick-but-limited read()/write() access to each GPIO pin, e.g., turning on a pin with the shell command echo on > /dev/gpio/17.
The GPIONode structure implements each of these node types. The implementation includes a stat() function, which can be used to fill a struct stat structure, as defined by POSIX.
Nodes are associated with paths via a HashMap container. This is a sub-optimal choice, that was taken for expediency in completing a first-cut version of the resource manager. Since nodes needs to be owned by the hash table, while at the same time provided to functions that have to access them, the nodes are stored using a Rc<> smart pointer. In this example, nodes are not updated once created, and therefore do not require inner-mutability.
A “session” is another new term for an existing concept that lacks a proper name. A session corresponds to a connection from the client to the server, as it exists between an _IO_CONNECT message (typically the result of a call to open()) and an _IO_CLOSE message (typically the result of a call to close(), and also sent automatically when the client exits). A session can only be established if the server accepts the _IO_CONNECT message, which it does based on various considerations, including client permissions and limits.
The GPIOSession structure, used by this particular resource manager, keeps track of the node that was opened, what access permissions were used, and, for a node which can be read from (the directory, the pin nodes), also the offset into the read data.
Session objects are kept in a HashMap container (again, a sub-optimal choice, but will do for now), where the key consists of the process and connection IDs of the client. Like nodes, sessions need to be kept using Rc<> smart pointers, so that they can be found in the table but then carried around as references in various handler functions. Unlike nodes in the GPIO server, though, sessions can be mutated by handlers (for updating the offset value), and therefore the table stores the sessions in RefCell<> types, which allow for inner mutability.
Access to the Raspberry Pi GPIOs is provided, as usual, via a non-cached, strongly-ordered virtual mapping of the hardware registers. The RPiGPIO structure holds the resulting virtual address. The implementation of the structure provides functions for changing the roles of each pin, writing to output pins, reading input pins, etc. These are all achieved via straight-forward bit manipulations. The use of an initialized Pin structure in the argument to these functions avoids extra checks that the pin number is valid.
Everything comes together with the GPIOServer structure, which includes:
- a channel for receiving messages
- a node table to store the different nodes by their paths
- a session table for established connections from clients
- the mapped hardware registers
- a vector of gpio pin structures.
The server implements the IoMsgImpl trait, which is the interface between the top-level handlers and the resource-manager-specific ones. For example, the top-level handler for the _IO_READ message invokes the trait’s read() function, implemented by the server to read the value of an input node (if the current session is associated with a pin node), or the directory (if the current session is associated with the directory node). Each server handler ends either with a reply to the client (completing the transaction), or with an error return value, which the message loop handles by propagating it to the client. It is also possible for the handler to invoke the error() function of Transaction directly, though I find the pattern of returning an error clearer.
The connect() function creates a new session and adds it to the session table. Other I/O handlers find the session in the table based on the client’s information (and fail if no session is found). The session holds a reference to the node that the client requested (identified by its path), which requires a clone of the Rc<GPIONode> object.
On top of the standard I/O messages, the server also handles the generic _IO_MSG type. This type allows for messages that don’t fit nicely in the file abstraction. It is possible to use write() to pass arbitrary bytes that can be interpreted as structured messages, but that loses type cohesion. Various flavours of UNIX came up with devctl() and ioctl() for this purpose, but these attempt to solve a problem that doesn’t exist with QNX message passing to begin with: structured messages are built into the system.
The benefit of _IO_MSG is that it has just a small header that identifies its type (allowing the server to identify it alongside other I/O messages), and then leaves the payload to be defined by the implementation. The GPIO server defines messages for setting and getting the role of each pin, for writing to output pins and for reading from input pins. Future versions will catch up with the C version, to provide PWM and event registration.
Is Rust a good choice for implementing resource managers? I will discuss the challenges I faced when implementing this resource manager, how they affected the design, and what are the trade-offs I see so far in moving from C to Rust.

︎

