The situation

If you have used select(2) or poll(2) more than once, you may have noticed the regular pattern that comes up again and again:

  • The main task of a program is to listen and react on multiple input/output connections.
  • For each i/o connection (file descriptor), you have
  • a function that opens the file descriptor (Let's call it conn_open.) and
  • another function to be executed if an event happens on the file descriptor (conn_handle).
  • After conn_handle has been called, the number of connections may have changed: conn_handle may
  • add (think of accept(2))
  • or remove (think of close(2)) connections.
  • conn_open and conn_handle are closely related and belong to the same "object" or code (conn_object).

The problem

Each and every time this situation occurs, you have to (re-)write code to handle that case. I have seen it in some applications I have been writing, for instance ceofhack or fui.

The solution proposal

Write a solution to the problem once and only once, so you and me don't reapeat ourselves.

First of all, begin with the obvious part:

What do we have?

We have to bring together n times

  • open functions
  • file descriptors on which the following events can happen:
  • data is ready to be read from the file descriptor
  • data can be written to the file descriptor
  • an error occured on the file descriptor
  • handle functions

Furthermore, we have

  • one main loop that listens for events

How to connect them properly?

I assume that every conn_object knows best, which function to use for opening and handling and which type of event is interesting.

Thus if we create a special function conn_open for every conn_object that

  • returns this information to the caller, the caller can create
  • a list containing the needed information and
  • loop over the event list and call the corresponding handler.

The conn_open function may look like this:

connection_entry = conn_open();

Where connection_list is a list of connection_entries like this:

struct connection_list {
   struct connection_entry *next;
} list;

enum type {
   IN,
   OUT,
   ERR
};

struct connection_entry {
   int fd;
   void (*handler)(struct connection_list *);
   int type;
};

The conn_open function can add or remove entries from the list. Whether the list is an array, linked list, hash or whatever may be implementation specific.

The main loop

Before launching the main listener loop, we need to initialise the list and run the conn_open function of every conn_object:

struct connection_list list = init_connection_list();

connection_add(&list, a_conn_open());
connection_add(&list, b_conn_open());

Having done this, our main loop now looks pretty simple, doesn't it?

while(1) {
   /* create poll or select list from connection list, whatever you prefer */
   poll_or_select_struct = connection_list_topoll_or_select(&connection_list);

   changed_events = poll_or_select(&poll_or_select_struct);

   for(event = changed_events; event != NULL; event = event->next) {
      exec_handler(event->fd, &connection_list);
   }
}

The function exec_handler would search for the registered handler of the changed file descriptor, which could look like this:

conn_handle(fd, &connection_list)

Firstly we pass the fd, because one handler may be registered for more than one connection. Secondly we add the connection_list, because the handler may add more connections or remove itself.

Although I'm mainly speaking about poll and select, the idea also applies to kqueue, epoll and co.

Further thoughts

Combine handle and open functions

For a second I thought the functions conn_handle and conn_open could be merged into a single function, which would get a negative file descriptor, if called the first time. But as this means the conn_handle would need to check every time it is called whether the to call the open part or not, this is probably not a good idea.

Creating an implementation

I am currently actively (!) working on fui and think about creating an implementation in ruby and if it works fine, another one in C for ceofhack.

Getting feedback

I would appreciate any feedback regading this idea, whether the problem is no problem at all, has been solved before or the idea may be a solution for your problem, too: You can contact me for this special post at nico-io-poll-select-idea (near) schottelius.org.