ecore fd handlers - Monitoring file descriptors

This is a very simple example where we will start monitoring the stdin of the program and, whenever there's something to be read, we call our callback that will read it.

Check the full code for this example here.

This seems to be stupid, since a similar result could be achieved by the following code:

while (nbytes = read(STDIN_FILENO, buf, sizeof(buf)))
{
buf[nbytes - 1] = '\0';
printf("Read %zd bytes from input: \"%s\"\n", nbytes - 1, buf);
}

However, the above code is blocking, and won't allow you to do anything else other than reading the input. Of course there are other methods to do a non-blocking reading, like setting the file descriptor to non-blocking and keep looping always checking if there's something to be read, and do other things otherwise. Or use a `select` call to watch for more than one file descriptor at the same time.

The advantage of using an Ecore_Fd_Handler is that you can monitor a file descriptor, while still iterating on the Ecore main loop. It will allow you to have timers working and expiring, events still being processed when received, idlers doing its work when there's nothing happening, and whenever there's something to be read from the file descriptor, your callback will be called. And it's everything monitored in the same main loop, no threads are needed, thus reducing the complexity of the program and any overhead caused by the use of threads.

Now let's start our program. First we just declare a context structure that will be passed to our callback, with pointers to our handler and to a timer that will be used later:

/*
* gcc -o ecore_fd_handler_example ecore_fd_handler_example.c `pkg-config --cflags --libs ecore`
*/
#include <Ecore.h>
#include <unistd.h>
struct context
{
Ecore_Fd_Handler *handler;
Ecore_Timer *timer;
};

Then we will declare a prepare_callback that is called before any fd_handler set in the program, and before the main loop select function is called. Just use one if you really know that you need it. We are just putting it here to exemplify its usage:

static void
_fd_prepare_cb(void *data EINA_UNUSED, Ecore_Fd_Handler *handler EINA_UNUSED)
{
printf("prepare_cb called.\n");
}

Now, our fd handler. In its arguments, the `data` pointer will have any data passed to it when it was registered, and the `handler` pointer will contain the fd handler returned by the ecore_main_fd_handler_add() call. It can be used, for example, to retrieve which file descriptor triggered this callback, since it could be added to more than one file descriptor, or to check what type of activity there's in the file descriptor.

The code is very simple: we first check if the type of activity was an error. It probably won't happen with the default input, but could be the case of a network socket detecting a disconnection. Next, we get the file descriptor from this handler (as said before, the callback could be added to more than one file descriptor), and read it since we know that it shouldn't block, because our fd handler told us that there's some activity on it. If the result of the read was 0 bytes, we know that it's an end of file (EOF), so we can finish reading the input. Otherwise we just print the content read from it:

static Eina_Bool
_fd_handler_cb(void *data, Ecore_Fd_Handler *handler)
{
struct context *ctxt = data;
char buf[1024];
size_t nbytes;
int fd;
if (ecore_main_fd_handler_active_get(handler, ECORE_FD_ERROR))
{
printf("An error has occurred. Stop watching this fd and quit.\n");
ecore_main_loop_quit();
ctxt->handler = NULL;
return ECORE_CALLBACK_CANCEL;
}

Also notice that this callback returns ECORE_CALLBACK_RENEW to keep being called, as almost all other Ecore callbacks, otherwise if it returns ECORE_CALLBACK_CANCEL then the file handler would be deleted.

Just to demonstrate that our program isn't blocking in the input read but still can process other Ecore events, we are going to setup an Ecore_Timer. This is its callback:

fd = ecore_main_fd_handler_fd_get(handler);
nbytes = read(fd, buf, sizeof(buf));
if (nbytes == 0)
{
printf("Nothing to read, exiting...\n");
ecore_main_loop_quit();
ctxt->handler = NULL;
return ECORE_CALLBACK_CANCEL;
}

Now in the main code we are going to initialize the library, and setup callbacks for the file descriptor, the prepare callback, and the timer:

buf[nbytes - 1] = '\0';
printf("Read %zd bytes from input: \"%s\"\n", nbytes - 1, buf);
return ECORE_CALLBACK_RENEW;
}
static Eina_Bool
_timer_cb(void *data EINA_UNUSED)
{
printf("Timer expired after 5 seconds...\n");
return ECORE_CALLBACK_RENEW;
}
int
main(void)
{
struct context ctxt = {0};
if (!ecore_init())
{
printf("ERROR: Cannot init Ecore!\n");
return -1;
}
ctxt.handler = ecore_main_fd_handler_add(STDIN_FILENO,
ECORE_FD_READ | ECORE_FD_ERROR,
_fd_handler_cb,
&ctxt, NULL, NULL);
ecore_main_fd_handler_prepare_callback_set(ctxt.handler, _fd_prepare_cb, &ctxt);
ctxt.timer = ecore_timer_add(5, _timer_cb, &ctxt);

Notice that the use of ecore_main_fd_handler_add() specifies what kind of activity we are monitoring. In this case, we want to monitor for read (since it's the standard input) and for errors. This is done by the flags ECORE_FD_READ and ECORE_FD_ERROR. For the three callbacks we are also giving a pointer to our context structure, which has pointers to the handlers added.

Then we can start the main loop and see everything happening:

printf("Starting the main loop. Type anything and hit <enter> to "
"activate the fd_handler callback, or CTRL+d to shutdown.\n");
ecore_main_loop_begin();
if (ctxt.handler)
ecore_main_fd_handler_del(ctxt.handler);
if (ctxt.timer)
ecore_timer_del(ctxt.timer);
ecore_shutdown();
return 0;
}

In the end we are just deleting the fd handler and the timer to demonstrate the API usage, since Ecore would already do it for us on its shutdown.