#thread #server-client #sync #channel #message-passing #networking

ump

Micro message passing library for threads/tasks communication

14 releases (8 breaking)

0.12.1 Oct 2, 2023
0.11.0 Jul 28, 2023
0.9.0 Sep 9, 2022
0.8.1 Sep 1, 2021
0.6.4 Oct 12, 2020

#347 in Concurrency


Used in ump-server

0BSD license

24KB
210 lines

Micro-Message Passing Library

The ump crate is a simple client/server message passing library for intra-process communication. Its primary purpose is to allow cross async/non-async communication (for both the server and client endpoints).


lib.rs:

Micro Message Pass (ump) is a library for passing messages between thread/tasks. It has some similarities with the common mpsc channel libraries, but with the most notable difference that each time a client sends a message the server must send back a reply.

The primary purpose of ump is to create simple RPC like designs, but between threads/tasks within a process rather than between processes over networks.

High-level usage overview

  1. An application calls channel to create a linked pair of a Server and a Client.

  2. The server calls [Server::wait()]/[Server::async_wait()], which blocks and waits for an incoming message from a client.

  3. A client, in a separate thread or task, sends a message to the server and wait for a reply using:

    • [Client::req()] for non-async contexts.
    • [Client::areq()] to async contexts.
    • [Client::req_async()] (and wait for a reply using the returned WaitReply)
  4. The server's wait call returns two objects: The message sent by the client, and a ReplyContext.

  5. After processing its application-defined message, the server must call the [ReplyContext::reply()] on the returned reply context object to return a reply message to the client.

    Typically the server calls wait again to wait for next message from a client.

  6. The client receives the reply from the server and processes it.

Example

use std::thread;

use ump::channel;

let (server, client) = channel::<String, String, ()>();

let server_thread = thread::spawn(move || {
  // Wait for data to arrive from a client
  println!("Server waiting for message ..");
  let (data, mut rctx) = server.wait().unwrap();

  println!("Server received: '{}'", data);

  // Process data from client

  // Reply to client
  let reply = format!("Hello, {}!", data);
  println!("Server replying '{}'", reply);
  rctx.reply(reply);

  println!("Server done");
});

let msg = String::from("Client");
println!("Client sending '{}'", msg);
let reply = client.req(msg).unwrap();
println!("Client received reply '{}'", reply);
println!("Client done");

server_thread.join().unwrap();

In practice the req/reply types will probably be enums used to indicate command/return type with associated data. The third type argument to channel is an error type that can be used to explicitly pass errors back to the sender.

Semantics

There are some potentially useful semantic quirks that can be good to know about, but some of them should be used with caution. This section will describe some semantics that you can rely on, and others that you should be careful about relying on.

Stable invariants

Semantics that should not change in future versions.

  • The reply contexts are independent of the Server context. This has some useful implications for server threads that spawn separate threads to process messages and return replies: The server can safely terminate while there are clients waiting for replies (implied: the server can safely terminate while there are reply contexts in-flight).
  • A cloned client is paired with the same server as its origin, but in all other respects the clone and its origin are independent of each other.
  • A client can be moved to a new thread.
  • Any permutation of sync/async server/clients can be combined. async code must use the async method variants when available.

Unstable invariants

Semantics you can trust will work in the current version, but they exist merely as a side-effect of the current implementation. Avoid relying on these if possible.

  • A single client can be used from two different threads. If a Client object in placed in an Arc, is cloned and passed to another thread/task then both the clone and the original can be used simultaneously. In the future this may not be allowed. It is recommended that a new clone of the client be created instead.

Dependencies

~1.1–8MB
~26K SLoC