Inter-Process Function Call

I've written a tool to do this, it's called ipftool. You can download it here: It's similar to itf2, but it works for different processes which can live on different machines. Currently, it only works for two processes, but it's not so hard to extend it further.

The tool takes an input file and generates code and header files for each process. The processes establish a socket connection using provided functions. After that, function calls on one machine are forwarded to the other seamlessly.

The Definition File

The definition file should have two process definitions. A process definition starts with the keyword "process" which is followed by a process identifier. This identifier is currently only used for documenting output files, but will be more useful later on.

Each process should be assigned a header file and source file in which ipftool will write the necessary glue code. This is accomplished by:

  header "headerfilename"
  source "sourcefilename"
directives.

After this, methods which will run on the current process should be declared. i.e. if method K is declared under process A, this means that other processes will send messages to A, which will run method K.

A method declaration has the following general syntax:

  method name(arguments)
name is an identifier which should obey C identifier rules. arguments is a list of declarations (possibly empty). Each declaration starts with a type name followed by one or more argument names where each argument name is separated from others by a comma. The last argument name should be followed by a semicolon if there are more declarations to follow. Otherwise, the semicolon isn't necessary but allowed. For example:
  method foo(i8 a,b,c;f32 b)
  method bar(u32 width,height; au8 image;)
are valid declarations.

Types

The tool recognizes the following types of data: The tool doesn't support any other composite type such as structs etc.

A type name consists of the following components:

The following is a complete list of all recognized types:
  i8  i16  i32  u8  u16  u32  f32 f64
 ai8 ai16 ai32 au8 au16 au32 af32 af64
 si8 si16 si32 su8 su16 su32
Here is a sample definition file:
process main

method input(i32 a)
source "imain.c"
header "imain.h"

process term

method result(i32 b)
method message(si8 a)
method getinput()

source "iterm.c"
header "iterm.h"

Communication Basics

First of all, you need to decide which process is going to listen for connections and which process is going to connect to it. The server process will use the following functions to manage connections.
int IPFserver(int port);
int IPFaccept();
int IPFserverSOCK();
int IPFdataSOCK();
The first function starts listening for connections on the given port. It returns -1 if anything fails (see errno). Otherwise, the return value is a socket descriptor which can be monitored for read status thru a select() or poll() call.

When select() returns readable status on the server socket, you can call IPFaccept() to accept the new connection. If you do this without select() checking, it will block until a connection request arrives. This function returns -1 if the connection couldn't be established. Otherwise, it returns a socket descriptor for the connection, which also can be used to select().

The last two functions return the current values for the server and data sockets. The server socket descriptor never changes; the function is there solely for convenience. However, the data socket descriptor may be changed to -1 if the remote peer closes the connection. In your event loop, before entering the select() call, you should check the data socket for validity using the given function.

For the client side, things are simpler. The function

int IPFconnect(char *host,int port);
connects to the given host at the specified port. It returns 0 if the connection was successful. For the client side, there is no server socket although the relevant code is still present. The choice of client/server assignment is left to user code.

Once a connection is made, you can monitor the data socket descriptor for read availability. Once that happens, you may call

int IPFread();
in order to process incoming messages. The return value is 1 if there is an error in communication. Otherwise, it's zero. Again, you can do this without using select(), but the function will block in that case.

Methods

In order to do actual work, you need to write the methods. A prototype for each method will be generated in the header files. If a method K is declared under process A, then it will have the return type of void in the code generated for A. For other processes, it will have the return type int.

In the implementing process A, method K doesn't have a return value because the protocol doesn't allow return values. In the caller processes, K has a return value of int because that indicates whether communication was successful or not (zero for success).

Here are the headers generated for the example definition file presented earlier:

/*** IPF header file for process main ***/

int IPFserver(int port);
int IPFaccept();

int IPFconnect(char *host,int port);

int IPFread();
int IPFserverSOCK();
int IPFdataSOCK();

/************************************************************************
 *   Functions implemented on this side. Implement these functions      *
 *   in this program. The peer will cause these to be called.           *
 ***********************************************************************/
void input(int32_t a) ;
/************************************************************************
 *   Functions implemented on the other side. Call these to run         *
 *   code on the other machine.                                         *
 ***********************************************************************/
int result(int32_t b) ;
int message(char* a) ;
int getinput() ;

/*** IPF header file for process term ***/

int IPFserver(int port);
int IPFaccept();

int IPFconnect(char *host,int port);

int IPFread();
int IPFserverSOCK();
int IPFdataSOCK();

/************************************************************************
 *   Functions implemented on this side. Implement these functions      *
 *   in this program. The peer will cause these to be called.           *
 ***********************************************************************/
void result(int32_t b) ;
void message(char* a) ;
void getinput() ;
/************************************************************************
 *   Functions implemented on the other side. Call these to run         *
 *   code on the other machine.                                         *
 ***********************************************************************/
int input(int32_t a) ;
Let's look at the getinput method. In the main process, it's declared with an int return value, indicating communication status. In the term process, it's declared with a void return type because that process implements getinput.

Now, for the C types. 8 bit integers are mapped to corresponding char types. 16 and 32 bit integers are mapped to (u)int(16/32)_t. You just need <stdint.h> for the generated headers. Floats are represented by float(32 bits) and double(64 bits).