Developer manual for the ZZ IPC framework, version 2004.10.19

TABLE OF CONTENTS

  1. GETTING STARTED
  2. CONCEPTS
    1. DATA CHANNELS
    2. TRANSFERRED STRINGS
    3. INPUTS AND OUTPUTS
    4. CLOSED SOCKETS
    5. MAIN LOOP AND CALLBACKS
    6. SHARED MEMORY SEGMENTS
    7. THE OPTION PARSER
  3. TUTORIAL
  4. KNOWN BUGS AND LIMITATIONS

1. GETTING STARTED

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

2. CONCEPTS

2.1 DATA CHANNELS

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.

2.2 TRANSFERRED STRINGS

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.

2.3 INPUTS AND OUTPUTS

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.

2.4 CLOSED SOCKETS

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.

2.5 MAIN LOOP AND CALLBACKS

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

2.6 SHARED MEMORY SEGMENTS

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.

2.7 THE OPTION PARSER

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.

3. TUTORIAL

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? 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".

4. KNOWN BUGS AND LIMITATIONS