Developer manual for the ZZ IPC framework, version 2004.10.19
TABLE OF CONTENTS
- GETTING STARTED
- CONCEPTS
- DATA CHANNELS
- TRANSFERRED STRINGS
- INPUTS AND OUTPUTS
- CLOSED SOCKETS
- MAIN LOOP AND CALLBACKS
- SHARED MEMORY SEGMENTS
- THE OPTION PARSER
- TUTORIAL
- KNOWN BUGS AND LIMITATIONS
ZZ comes with a C library called libzz for managing input and output.
The header is included
with #include <zz.h> and the library is linked with -lzz. The first use of
the libzz API must be a call to zz_init(), with the
application name as argument.
This call does initializations which are needed by the rest of the API,
and sets the application name which is used in debugging and error messages.
There is no call to shut down the library; zz_init()
takes care of this by
registering an atexit() function.
zz_init() also registers signal handlers,
which intercept SIGINT and SIGTERM signals and exit with exit(0).
To provide an example, this is one of the simplest possible ZZ applications. It
prints a message to the user, with the number of outputs and the number of
inputs.
#include <zz.h>
int main(int argc, char *argv[])
{
zz_init("zztest");
zz_message("%d outputs, %d inputs", zz_outputs(), zz_inputs());
return 0;
}
Save it as zztest.c and compile with gcc -lzz -o zztest zztest.c or similar. Run zz ./zztest a@ ./zztest a% and
you will receive the following output, or similar:
zztest (2442) message: 1 outputs, 0 inputs
zztest (2443) message: 0 outputs, 1 inputs
A data channel is a socket pair (SOCK_STREAM, AF_LOCAL) where
null-terminated strings of text,
typically without newlines, are sent in both directions.
For each end of a data
channel, libzz maintains a zz_socket structure. A pointer to such a
structure is often passed around when using the libzz API, but the structure
members are private to the library.
The text strings exchanged through the socket pairs are typically commands such
as "connect 10.0.0.5" and metadata such as "width 640". Some of these are
synthesized by applications, and some by the library itself. There is no
limit to the string length.
The producer end of a data channel is called an output,
and the consumer end is
called an input.
The application starts with a number of inputs and a number of
outputs. These numbers do not change during the life of the application, but
data channels may be closed.
There is no functional difference between an input
and an output, this is only a label which helps the user define a network of
channels. You might, for instance, make an application which takes two images,
blends them and outputs the result. This application would then take two input
channels and one output channel.
A data channel becomes broken when one of the processes using it exits.
When libzz detects that a peer socket has been closed, whether in the
main loop or during a send, libzz marks its socket as closed,
invokes a user defined callback (if any),
and stops listening on the socket.
libzz is designed around its main loop, and
the main loop is designed to let applications be designed around it.
The main loop listens for input and custom events, and is the place
where callbacks are invoked. The main loop either cycles through the
idle callback, or blocks until something happens, depending on whether
the idle callback is set.
Callbacks can be registered for
- receiving a string matching a given pattern.
- receiving a chunk of memory.
- taking action when a socket is closed.
- taking action at a "read" event, as indicated by select().
- taking action at a "write" event, as indicated by select().
- taking action at a "exception" event, as indicated by select().
- background computation.
Shared memory segments are created and destroyed with
zz_alloc() and zz_free(), much like heap memory.
They can be received with the memory callback,
and sent with zz_send_mem().
In each case, the segment is referred to by a pointer to
a zz_mem structure. Its members are private to libzz, but
zz_mem_ptr() returns the pointer to the segment itself, and
zz_mem_size() returns the size of the segment.
libzz comes with a flexible option parser for argv[] options.
No structure or special pointers need to be maintained to use the parser;
it simply manipulates the "current context" which it remembers.
A new option is created with zz_op_add_option().
It is given match-strings such as "--help" with zz_op_add_name().
Finally, one or more targets are attached to it; either a custom target
or one of zz_op_target_*() where * denotes a data type.
Typically, a pointer to a variable of that type is provided.
Help screens are composed automatically.
This tutorial will lead you through a simple application using libzz,
in the same order that code is executed. The application will accept
strings and memory on any input. A received string is expected to
be a number, choosing which output to forward memory segments to.
First, we will require these includes:
#include <zz.h>
#include <stdlib.h>
and define these global variables:
int verbose = 0;
int output = 0;
where verbose is a flag, and output is our
output selector.
Now, beginning with the main() function,
int main(int argc, char *argv[])
{
int i;
zz_init("example");
we call zz_init(), which should
be the first thing we do. Our variable i will be needed
later, to iterate through sockets.
Now, we create a table of options:
zz_op_set_usage("Usage: example [options], with options:");
zz_op_add_option("This help screen");
zz_op_add_name("-h");
zz_op_add_name("--help");
zz_op_target_help();
zz_op_add_option("Verbose output");
zz_op_add_name("-v");
zz_op_add_name("--verbose");
zz_op_target_boolean(&verbose);
and parse the array of arguments:
zz_op_parse(&argc, argv);
If the user invoked the help screen, we will never come past this line.
If the user provided the "verbose" option, our variable verbose
will be set to 1. The next thing to do is to check that we have
at least one input, and at least one output:
if (!zz_inputs()) zz_fail("must have at least one input");
if (!zz_outputs()) zz_fail("must have at least one output");
We also want to monitor the status of our sockets; so that we exit as
soon as the number of open inputs or the number of open outputs
drops to zero.
To do this, we define a close callback:
void handle_close(zz_socket *sock, void *user_data)
{
if (!zz_open_inputs() || !zz_open_outputs()) zz_exit();
}
and in main(), we register this callback for every
socket:
for (i = 0; i < zz_inputs(); ++i)
zz_close_callback(zz_input(i), NULL, handle_close);
for (i = 0; i < zz_outputs(); ++i)
zz_close_callback(zz_output(i), NULL, handle_close);
Now, the central parts are the pattern callback which selects
an output, and the memory callback which forwards a segment.
This is our pattern callback:
void handle_string(zz_socket *sock, char *str, void *user_data)
{
int number;
number = atoi(str);
if (number < 0 || number >= zz_outputs()) {
if (verbose) zz_message("illegal index from %s %d: %d",
zz_io_string(sock), zz_index(sock), number);
} else {
output = number;
if (verbose) zz_message("index %d selected from %s %d",
number, zz_io_string(sock), zz_index(sock));
}
}
where we convert the received string to a number, and check that
it is not out of bounds. This is our memory callback:
void handle_mem(zz_mem *mem, void *user_data)
{
if (verbose) zz_message(
"segment received from %s %d, forwarding to output %d",
zz_io_string(zz_mem_sock(mem)),
zz_index(zz_mem_sock(mem)), output);
zz_send_mem(zz_output(output), mem);
zz_free(mem);
}
This might seem incredibly sloppy at first; we do not know whether
zz_output(output) is open, and why do we risk destroying
the segment before it is received?
- zz_send_mem() simply takes no action if the socket is
closed.
- zz_free() will not destroy the segment as long as
there are peers which have been sent a reference to the segment,
but have not acknowledged it. The segment will be marked for
destruction, and destroyed later by the main loop when the
acknowledgement is received.
Finally, we round off main() with registering these
callbacks and invoking the main loop:
for (i = 0; i < zz_inputs(); ++i) {
zz_pattern_callback(zz_input(i), "*", NULL, handle_string);
zz_mem_callback(zz_input(i), NULL, handle_mem);
}
zz_main();
return 0;
}
The string "*" is simply a pattern that matches every string.
The NULL values we have provided are user data, anything
we might want to pass the callback for that particular socket
(and pattern). The source code to this example is included in the
source directory, and called "example.c".
- Sending a shared memory segment to oneself might give
unexpected results.
This will be remedied in future versions.
- There is no way to "try" to allocate a segment;
if the operation fails, the application will terminate
with an error message.
This will be remedied in future versions.