1 Introduction

In the last decade, the software industry has seen a shift towards programming languages that promote the coordination of concurrent and/or distributed software components through the exchange of messages over communication channels. Languages with native message-passing primitives (e.g., Go, Elixir and Rust) are becoming increasingly popular. In particular, Rust has been named the most loved programming language in the annual Stack Overflow survey for four consecutive years (2016–19)Footnote 1.

The advantage of message-passing concurrency is well-understood: it allows cheap horizontal scalability at a time when technology providers have to adapt and scale their tools and applications to various devices and platforms. Message-passing based software, however, is as vulnerable to errors as other concurrent programming techniques [16]. Much academic research has been done to develop rigorous theoretical frameworks for verification of message-passing programs. One such framework is multiparty session types (MPST) [5] – a type-based discipline that ensures that concurrent and distributed systems are safe by design. It guarantees that message-passing processes following a predefined communication protocol, are free from communication errors and deadlocks.

Rust is a particularly appealing language for the practical embedding of session types. Its affine type system allows for static typing of linear resources – an essential requirement for the safety of session type systems. Rust combines efficiency with message-passing abstractions, thread and memory safety [15], and has been used for the implementation of large-scale concurrent applications such as the Mozilla browser, Firefox, and the Facebook blockchain platform, Libra. Despite the interest in the Rust community for verification techniques handling multiple communicating processesFootnote 2, the existing Rust implementations [8, 9] are limited to binary (two-party) session types.

In this short paper, we present our design and implementation for multiparty session types in Rust. Our design follows a state-of-the-art encoding of multiparty into binary session types [13]. We generate local types in Rust, utilising the Scribble toolchain [12, 18]. Our library for MPST programming in Rust, mpst-rust, is implemented as a thin wrapper over an existing binary session types library [9]. Differently from other MPST implementations that check the linear usage of channels at runtime (e.g. [6, 13]), we rely on the Rust affine type system to type-check MPST programs. In addition, since we generate the local types from a readable global specification, errors caused by an affine (and not linear) usage of channels, a well-known limitation of the previous libraries [8, 9], are easily avoided.

This paper is organised as follows: Sect. 2 gives an overview of our framework with a usecase; Sect. 3 shows our implementation and discusses the advantages of our approach; and Sect. 4 concludes with related and future work. Our library is available from https://github.com/NicolasLagaillardie/mpst_rust_github.

2 From Binary to Multiparty Sessions in Rust

Framework Overview: MPST in Rust. Our design resembles the top-down methodology of multiparty session types, as illustrated in Fig. 1. It follows three main steps [5, 17]. First, a global type, also called a global protocol, is defined as a shared contract between communicating endpoint processes. A global protocol is then projected to each endpoint process, resulting in a local type. A local type involves only the interactions specific to a given endpoint. Finally, each endpoint process is type-checked against its projected local type.

The specific parts of our framework that distinguish it from other state-of-the-art MPST works are highlighted in red, which corresponds to our new library for MPST programming in Rust, mpst-rust. It is realised as a thin wrapper on top of an existing Rust library for validation of binary (2-party-only) session types. Developers use the MPST primitives provided by mpst-rust to implement endpoint programs. Also, our framework allows the types for each communication primitive to be either (1) generated from the Scribble toolchain; or (2) written by the developers. The Scribble toolchain [18] provides facilities for writing, verifying and projecting global protocols. Our framework guarantees that processes implemented using mpst-rust primitives with Scribble-generated types are free from deadlocks, reception errors, and protocol deviations. Next, we explain, via an example, how the framework of MPST can be applied to Rust.

Fig. 1.
figure 1

MPST Workflow (left) and Amazon Prime Video usecase (right)

Example: Amazon Prime Video Streaming. The Amazon Prime Video streaming service is a usecase which can take full advantage of multiparty session types. Each streaming application connects to servers, and possibly other devices, to access services, and follows some specific protocol. To present our design, we use a simplified version of the protocol, illustrated in the diagram in Fig. 1 (right). The diagram should be read from top to bottom. The protocol involves three services – an service, a and a . At first, connects to by providing an identifying id. If the id is accepted, the session continues with a choice on to either request a video or end the session. The first branch is, a priori, the main service provided by Amazon Prime Video. cannot directly request videos from , and has to go through instead. On the diagram, the choice is denoted as the frame alt and the choices are separated with the horizontal dotted line. The protocol is recursive, and can request new videos as many times as needed. The arrow going back on side in Fig. 1 represents this recursive behaviour. To end the session, first sends Close message to , which then subsequently sends a Close message to .

Fig. 2.
figure 2

Rust implementation of role (left) and its local Scribble protocol (right)

Implementing the Authenticator role Using mpst-rust . Due to space limitations, we only show the implementation of the role (hereafter role ), the implementations of the other roles (role for the and role for the ) are similar. The Rust code for role using the mpst-rust library is given in Fig. 2 (left). It closely follows the local protocol in Fig. 2 (right), that is projected from the global protocol by the Scribble toolchain. First, line 1 declares a function authenticator that is parametric in a multiparty channel s of type VideoP_A. The type VideoP_A specifies which operations are allowed on s. This type can either be written by the developer, or generated by Scribble (cf. Listing 1).

On line 3, receives an identifying id from . The function , provided by mpst-rust library returns the received value (the id) and the new multiparty channel, to be used in subsequent communications. Line 3 rebinds the multiparty channel s with the new channel that is returned. Then, on line 4, we send back the answer to C, by utilising another mpst-rust communication primitive, . The variable s is rebound again to the newly returned multiparty channel. Note that although the name of the function, , suggests a binary communication, the function operates on a multiparty channel s. Our implementation follows the encoding, presented in [13], which encodes a multiparty channel as an indexed tuple of binary channels. Internally, extracts from s the binary channel established between and and uses it for sending.

Lines 9–26 proceeds by implementing the recursive part of the protocol. The implementation of realises an internal choice – A can either receive a VideoRequest or a Close. This behaviour is realised by the mpst-rust macro (line 12), which is applied to a multiparty channel s of a sum type between ChoiceA::Video and ChoiceA::End. The behaviour of each branch in the protocol is implemented as an anonymous function. For example, code in lines 13–21 supplies an anonymous function that implements the behaviour when sends a VideoRequest, while lines 22–26 handle the Close request. Finally, closes all binary channels stored inside s. The types of the multiparty channel, as well as the generic types in the declaration of the mpst-rust communication functions, enable compile-time detection of protocol violations, such as swapping line 3 and line 4, using another communication primitive or using the wrong payload type.

figure ai

Typing the Authenticator Role. The types for the role, used in Fig. 2 (left), are given in Listing 1. These types can be either written by the developer or generated from a global protocol, written in Scribble. Reception error safety is ensured since the underlying mpst-rust library checks statically that all pairs of binary types are dual to each other. Deadlock-freedom is ensured only if types are generated from Scribble since this guarantees that types are projected from a well-formed global protocol.

Next, we explain a type declaration for the Authenticator role. Lines 27–37 specify the three SessionMpst types which correspond to the types of the session channels used in Fig. 2 (left) – types VideoP_A (line 1), Video_PRec_A (line 9), and the types used inside the offer construct – ChoiceA::Video (line 13), and ChoiceA::End (line 22).

In the encoding of [13], which underpins mpst-rust, a multiparty channel is represented as an indexed tuple of binary channels. This is reflected in the implementation of SessionMpst, which is parameterised on the required binary session types. For example, the \(\texttt {VideoP\_A{<}N{>}}\) takes as a parameter the binary types between A and C, and between A and B. At the beginning of the protocol (lines 1–7 in Fig. 2 (left)) B and A do not interact, hence the binary type for B is End. The type \(\texttt {InitA{<}N{>}}\) (line 2 in Listing 1) specifies the behaviour between A and C, notably that A first receives a message, then it sends a message, and later it continues as the type \(\texttt {RecvChoice{<}N{>}}\). The binary session types between A and B, and between A and C are given in lines 12–14 and lines 2–9 respectively; we use the primitives declared in the existing binary session types library [9]. The generic parameter refers to a such as .

The third parameter for \(\texttt {VideoP\_A{<}N{>}}\) (line 27) is a queue-like data structure, QueueAInit (line 17), that codifies the order of usage of each binary channel inside a multiparty session channel. This is needed to preserve the causality, imposed by the global protocol. The queues for the other SessionMpst types are given in lines 21–24. For instance, the queue for the ChoiceA:Video branch of the protocol is QueueAVideo. Note that, according to the protocol, A first has to receive a VideoRequest message from C, and then it has to forward that message to B Hence, swapping of lines 17 and 18 from Fig. 2 is a protocol violation error. We can detect such violations since the queue for the type ChoiceA::Video, QueueAVideo (line 21), is specified as \(\texttt {RoleAtoC{<}RoleAtoB ...{>}}\), which codifies that first the channel for C and then the channel for B should be used. Note that none of the defined queues is recursive. Recursion is implicitly specified on binary types, while each queue is related to a SessionMpst type.

Distributed Execution Environment. The default transport of mpst-rust is the built-in Rust communication channels (crossbeam_channel). Also, to test our example in a more realistic distributed environment, we have also connected each process through MQTT (MQ Telemetry Transport) [7]. MQTT is a messaging middleware for exchanging messages between devices, predominantly used in IoT networks. At the start of the protocol, each process connects to a public MQTT channel, and a session is established. Therefore, we have mapped binary channels to MQTT sockets, in addition to the built-in Rust channels.

3 Design and Implementation of mpst-rust

Multiparty Channels as an Ordered Tuple of Binary Channels. The main idea of the design of our framework is that a multiparty session can be realised with two ingredients: (1) a list of separate binary sessions (one session for each pair of participants) and (2) a queue that imposes the ordering between the binary channels. Listing 2 (lines 2–3) shows the implementation of a multiparty channel in a 3-party protocol. The structure holds two fields, and , that are of a binary session type. For an illustration purpose, we show only the implementation of a multiparty channel for three processes. The same approach can be generalised, using our code generation tool, to any number of communicating processes. For example, in case of a protocol with four roles, each multiparty session will have four fields – a field for the binary session between each pair of participants and a field for the queue.

The order of usage of the binary channels of a SessionMpst object is stored inside the queue field. For instance, the behaviour that role A has to communicate first with role B, then with a role C, and then the session ends can be specified using a queue of type . Note that all queue types, such as RoleAtoB, RoleAtoC, are generated.

MPST Communication Primitives as Wrappers of Binary Channels. As explained in Sect. 2, programming with mpst-rust relies on communication primitives, such as , that have the sender and receiver roles baked into their name. To ensure that the binary channels are used as specified by the global protocol, each communication function is parametric on a generic quadruple type where is a payload type, and are binary session types and is a type for a queue (MPST-queue type) that imposes the order in which the binary sessions inside a multiparty session must be used.

figure ax

Listing 2 (lines 5–9) shows the implementation for . As clear from the type parameters, the client of the function should supply a MPST-queue type . The binary session type should be encapsulated in a . The body of the function sends the message of type on the binary channel stored in the first field, (corresponding to the binary session with role ), of the multiparty session . Since the communication is on a binary channel, we reuse the binary primitive from [9].

External and internal choices are implemented as macros that require an argument of type SessionMpst. The implementation of offer_mpst_a_to_c is given in lines 11–14. In essence, a choice is implemented as a broadcast from one role to the others. In our usecase, the active role that makes the choice is C. Hence, the macro offer_mpst_a_to_c explicitly performs a receive (recv_mpst_a_to_c(s)) on the session channel s. The received value is pattern matched and passed to any of the functions given as arguments to offer_mpst_a_to_c. Similarly, in lines 19–26 is a macro that performs a select operation. The active role C sends the selected label to all roles in the protocol. In our particular example, C sends the selected label l to A and B.

Discussions. Our implementation, although intuitive, does not resolve the inherent conflict between Rust, which is affine, and session types, which are linear. The implementation suffers from the same drawback as [9]. However, the MPST methodology is a step forward in terms of usability. Differently than the Rust local types which can get convoluted, the syntax of global protocols is user-friendly and readable. Developers can use the global protocol as guidance, and hence avoid errors such as prematurely ending of a session. Moreover, as observed in Kokke’s library [9], most of the errors are caused by misuse of methods and functions. Since we are code-generating the local types, the chance of misspelling is significantly reduced. Another viable option for our framework is to take the bottom-up approach: to check directly whether a set of manually-written Rust local types satisfy safety/liveness properties by a model checker [14] or the multiparty compatibility (a property which guarantees deadlock-freedom of communicating automata, which are equivalent to local session types) [2, 11].

4 Related and Future Work

The Rust library in [8] implements binary session types, following [4]. It checks at compile-time that the behaviours of two endpoint processes are dual, i.e the processes are compatible. The library in [9], based on the EGV calculus by Fowler et al. [3], provides constructs for writing and checking binary session types, and additionally supports exception handling constructs. We build on top of the library in [9] since it offers several improvements in comparison to [8]. Most importantly, the treatment of closing a channel prematurely in [8] may lead to memory leaks. Both libraries suffer from a well-known limitation of binary session typesFootnote 3. Notably, since deadlock-freedom is ensured only inside a session, a Rust endpoint process, that communicates with more than one other process, is prone to deadlocks and communication errors. Our framework solves that limitation by expanding the scope of a session to multiple participants.

Our proposed design follows the methodology given by [6], which generates Java communicating APIs from Scribble. This, and other multiparty session types implementations, exploit the equivalence between local session types and communicating automata to generate session types APIs for mainstream programming languages (e.g., Java [6, 10], Go [1], F# [13]). Each state from state automata is implemented as a class, or in the case of [10], as a type state. To ensure safety, state automata have to be derived from the same global specification. All of the works in this category use the Scribble toolchain to generate the state classes from a global specification and detect linearity violations at runtime. This paper proposes the generation of protocol-specific APIs, which promotes type checking of protocols at compile-time. This is done by projecting the endpoints’ state space in those protocols to groups of channel types in the desired language. In the future, we plan to implement the bottom-up approach, in addition to the top-down approach outlined in this paper, as to compare their productivity and scalability.