Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
minimalistic localhost remote procedure call
C
branch: master

This branch is 1 commit ahead, 31 commits behind a-rubini:master

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
examples
.gitignore
COPYING
Makefile
README
minipc-client.c
minipc-core.c
minipc-int.h
minipc-server.c
minipc.h

README

This is a mini remote procedure call, but I called it IPC in error.

It currently being suggested as a replacement for what we have in the
white rabbit system, because the way it is there is not portable
(assembly code and knowledge of the ABI is required for each
platform).  Nonetheless, I borrowed quite some idea from the
implementation by Tomasz Wlostowski, who must definitely be
considered co-author and my inspiring muse.

	Basic Ideas
	-----------

The package is meant to be as simple as possible, while still having
some abstraction to be useable on different environments than the
white rabbit switch.  There are other RPC environments, but they are
usually oversized for our needs. Besides, I personally love small
things that I can use as completely-understandable examples when
teaching.

A process can be a server, or a client, or both.  A server program
opens a Unix domain socket and then accepts client programs. Thus,
performing a server action means either accepting a new client or
performing a client request.  To avoid requiring the library
to be the only I/O of the process, I export the fd_set structure
defining the server active file descriptors (I don't directly
support poll(2), only select(2)).

A client program only works on a single file descriptor, so extracting
the fd is supported (thus you can use poll yourself).

An exported function is defined by a data structure, which lists both
the argument list and the return value. Data types supported are
integers, doubles, strings and generic structures.  We might (or might
not) export the thing over TCP and add endian conversion, but at this
point the data type is really just informative.

An argument list is a zero-terminated array of 32-bit integers, which
specifies both the type and the size of the argument being passed.
The return value is a single 32-bit integer specifying type and size.
The header provides macros to build the actual values.

	Sharing information
	-------------------

Client and server must agree on the functions being called remotely.
A function is identified by a string name, 18 bytes at most.

This is the "procedure definition" structure, 32 bytes plus the argument
list:

struct minipc_pd {
        minipc_f *f;            /* pointer to the function */
        char name[20];          /* name of the function */
        uint32_t flags;
        uint32_t retval;        /* type of return value */
        uint32_t args[];        /* the list of arguments, null-terminated */
};

Client and server are expected to share a data structure, or at least
the macros used to build the data structure. Name, arg list and retval
should match.  The server will then fill the function pointer. No
user-accessible flag is currently in use.

the "args" array must be terminated by a "MINIPC_ARG_END" value, which
is guaranteed to be bitwise zero. The other arguments encode type and
size, even if some types (like integers, floats) always have the same
size. See the ATYPE macros in "minipc.h"


        The client
        ----------

The client will create a client channel. The "minipc_ch" structure
simply hosts a file descriptor, so you have these functions:

  struct minipc_ch *minipc_client_create(const char *name, int flags);
  int minipc_fileno(struct minipc_ch *ch);

The name argument is a filename used for the Unix domain socket, it
must include no path separators, as the socket is created in the
directory MINIPC_BASE_PATH, (default "/tmp/.minipc").  When TCP is
added, it will use names of the form ":TCP:3456".

With the fileno a client may poll for writing, although it is not
usually needed (the server will only send reply packets, one reply
for each request).

A client can make a request like this:

  int minipc_call(struct minipc_ch *ch, int millisec_timeout,
                const struct minipc_pd *pd, void *ret, ...);

The arguments are passed as a list of values (stuctures and strings
are passed by pointer); the return pointer is filled with the returned
data item.  The minipc_call function picks data from the variable arguments
according to the argument types set forth in the "pd" procedure
definition. Similarly, the return value is stored where pointed by
ret, according to pd->retval.  Note that the return value must at least
be 4 bytes long.

The "minipc_call" returns 0 on success and -1 with errno on failure.
If send, poll or recv return an error, such errno is preserved.
If poll times out (using the provided timeout) errno is set to ETIMEDOUT.
If the server returns an error (using MINIPC_ATYPE_ERROR), the
local errno is set to EREMOTEIO and the remote one is saved using
the retval pointer (which is guaranteer to point to an int-sized or bigger
area).

To close the connection, a client can call

  int minipc_close(struct minipc_ch *ch);


        Diagnostics
        -----------

A "minipc_ch" channel can have an associated log file (you may
use stderr or whatever). This function sets the log file:

  int minipc_set_logfile(struct minipc_ch *ch, FILE *logf);

You can set the logfile to NULL (default at channel creation) if you
want to stop sending diagnostics. The function does not open or
close the file, as otherwise it wouldn't work with stderr, so if you want
to open a log file on disk, it's your duty to do so (and possibly
set it as unbuffered, since the library only calls fprintf on it,
with no explicit fflush).

        The server
        ----------

The server must open a server channel and process requests as needed.
The following functions are available for the server:

  struct minipc_ch *minipc_server_create(const char *name, int flags);

  int minipc_export(struct minipc_ch *ch, const struct minipc_pd *pd);
  int minipc_unexport(struct minipc_ch *ch, const struct minipc_pd *pd);

  int minipc_server_action(struct minipc_ch *ch, int timeoutms);

  int minipc_server_get_fdset(struct minipc_ch *ch, fd_set *setptr);


The export and unexport functions should be self-explicative. Note
that the exported funcion name is part of the pd structure. The "pd"
passed to "minipc_unexport" must be the same as the one passed to
"minipc_export", not just a pointer to a data structure with the same
contents.

The server action either accepts a new client or handles all pending
client requests. For every packet received from a client, a reply
packet is sent back. So, even if communication is based on SOCK_STREAM,
packet boundaries are preserved by using only synchronous communication.

the "get_fdset" function returns an fdset structure, so the caller
may use select() in the main loop by augmenting the minipc fdset
with its oen.  The server action calls poll on itself (with the
specified timeout), so if you already used select you can pass 0
as timeoutms in minipc_server_action.

The functions being exported are typedef'd as follows:

  typedef int (minipc_f)(const struct minipc_pd *, void *retval, void *args);

They receive a pointer to themselves, a pointer to the unmarshalled
arguments and a pointer to the data being returned.  Thus, the
function itself should pick up data from the pointers (they are
usually pointing to the packet buffer, so you should not change
the arguments themselves before calling the real function with the
ABI of the current CPU.

For example, exporting sqrt would look like the following

  extern double sqrt(double x);

  int export_sqrt(const struct minipc_pd *pd, uint32_t *args, void *ret)
  {
        double in, out;

        in = *(double *)args;
        out = sqrt(in);
        *(double *)retval = out;
	return 0;
   }

   struct minipc_pd sqrt_pd = {
         .f = export_sqrt,
         .id.s = "sqrt",
         .retval = MINIPC_ARG_ENCODE(MINIPC_AT_DOUBLE, double),
         .args = {
                 MINIPC_ARG_ENCODE(MINIPC_AT_DOUBLE, double),
                 MINIPC_ARG_END,
        },
   }

Please note that while using doubles or integers is easy, unmarshalling
a string from the args array is not trivial. The library
offers a generic unmarshall function:

    uint32_t *minipc_get_next_arg(uint32_t arg[], uint32_t atype);

The function receives the current pointer to an argument and the
current type. While the argument is used as-is (with casting), the
pointer to the next argument may be variable (because of strings) or
need alignment in general (in the unlikely case of non-multiple-of-4
structures).  This function offers a portable solution.

Example

  int export_strcat(const struct minipc_pd *pd, uint32_t *args, void *ret)
  {
      char *s1, char *s2, char sout[256];

      s1 = (char *)args;
      args = minipc_get_next_arg(args, pd->args[0]);
      s2 = (char *)args;

      strncpy(sout, s1, sizeof(sout));
      strncat(sout, s2, sizeof(sout));
      strncpy(ret, sout, MINIPC_MAX_REPLY));
      return 0;
  }


The client calling the RPC server will do it in this way,
assuming "struct minipc_ch *client" and "struct minipc_pd pd_sqrt"
are valid variables:

   double operand;
   double result;
   int error;

   error = minipc_call(client, &pd_sqrt, &result, operand);
   if (error < 0) { ... }



        Communication protocol
        ----------------------

The protocol is an internal detail, but worth knowing in case a bug is
discovered. Relevan structures are in the internal header
"minipc-int.h".

Request packets are "struct mpc_req_packet", thus build:

	20 bytes for function name, copied from "pd" structure
	an array of 32-bit integers

Each argument passed to the remote function is stored into one or
more such 32-bit integers, in host byte order. Note that the
actual size of the argument is written in the "pd" args list.
Marshalling is preformed by minipc_call() with varargs evaluation.

Reply packets are "struct mpc_rep_packet", which has two fields:

	uint32_t type
	uing8_t val[]

The type is checked to be the same as what is defined as "ret" in the
"pd" structure. The val array is a plain byte array that is copied to
the "ret" pointer passed to minipc_call (like "result" in the sqrt
example above). When "type" is an error value (MINIPC_ATYPE_ERROR) the
payload is a 4-byte errno value.  Note that "type" is not passed back
to the caller of "minipc_call()", errors are identified by a negative
return value.


	Examples
	--------

Examples are in the subdirectory examples/ so plese check examples/README.


	 BUGS
	 ----

Please note that there are a number of untested corner cases.

- please don't try going near 1k argument/retval size, off-by-1 errors
are most likely present (and I have no tests yet to feel safe)

- strings passing is not currently tested, please wait for next test

- struct passing should obviously work, but please wait for a sample/test
Something went wrong with that request. Please try again.