Epoll Egg Released for Chicken Scheme

Quite a few people suggested I turn the core technology that powers Rooster into a Chicken Egg. Eggs are Chicken Scheme's version of third-party libraries/extensions. Rooster used epoll on Linux by interfacing the C sys/epoll functionality with Chicken Scheme. I spent some time getting feedback in the IRC channel (#chicken on freenode) yesterday and today. I'm happy to announce that I've released a brand new egg that brings epoll support to Chicken Scheme. There are a couple places you can get the epoll egg:

  • $ git clone git://github.com/davidreynolds/epoll.git
  • $ sudo chicken-install epoll

If you cloned the git repo:

$ cd epoll
$ sudo chicken-install epoll.setup

I also rewrote Rooster as a Chicken Scheme module and made it use the epoll egg. I had some difficulty coming up with a good way to return a vector of pairs from C into Scheme. I didn't exactly want to use my old way of calling back into Chicken from C because a user would then have to provide their own Scheme callback. That's basically what I ended up doing, however.

I wrapped C epoll_wait so that it calls into the epoll egg with the vector of pairs. Where this differs from the old Rooster code is that the interface to Chicken epoll-wait that a programmer has access to actually takes a callback function as an argument. This callback function is defined by the programmer and accepts a list of (fd . events) pairs. The epoll egg callback, SCM_epoll_wait_cb converts the vector of pairs into a list of pairs and passes it to the user-defined callback function.

It seems to work pretty well. As for the future of Rooster, I'm considering breaking it into a couple different projects. I'd like to create a web server with the epoll egg. I also need to work on getting some documentation up for the epoll egg and for Rooster.

To the Chicken Scheme Community

Thanks for the great feedback you've been providing and for helping me get my first Chicken Egg released. Hacking on Chicken is pretty neat and it's definitely a great Scheme implementation. I'm looking forward to producing some more eggs and interesting software in Chicken.

Rooster: An epoll Server written in Chicken Scheme and C

My last few blog posts have been sort of leading up to this point: writing an actual server in Chicken Scheme that uses epoll. The server is written in such a way that lets the programmer determine what to do with incoming requests. All the server does is manage sockets and pass input to a programmer-defined request handler. Here's a simple echo server example that uses Rooster:

(require 'rooster)

(define (handler fd rbuf)
  (send-to-client fd rbuf))

(run-rooster handler)

The function `send-to-client` is defined in server.scm and exported so it can be used in an external program. send-to-client writes the buffer to the file descriptor. Note that send-to-client doesn't actually _send_ a buffer to fd. It fills the write buffer for that fd and sends the write buffer when epoll tells the server it's ready to write on that socket.

Rooster requires epoll and Linux for now, but I'd like to fall back to standard poll if epoll isn't available. The main idea I had for Rooster was to create some kind of MUD with it and treat Rooster itself as a sort of game engine.

It'd be interesting to see a new web server come from this as well. I don't see why Rooster couldn't provide the core of any kind of server. New servers could be written on top of Rooster and be completely different from each other.

I'm expecting to be making a ton of changes to Rooster and its API, so it's not at a point where you can rely on the APIs provided unless you feel like locking down to one build of Rooster. I put the code for Rooster on Github earlier today so please take a look. The README should get you started building it and writing your own socket handlers.

Using epoll with Chicken Scheme

Late last week I wrote a blog entry on how to interface Chicken Scheme and C. It was a short and sweet introduction to writing Scheme that calls into C that in turn calls back into Scheme. I spent some of the weekend adapting the vector_of_pairs code to implement a working echo server using epoll. There's a little more C code involved and a lot more Scheme code than before. If you've seen Tornado's epoll.c wrappers, you'll quickly recognize the similarities between their code and mine, with mine being adapted to Scheme instead of Python.

First, here's the epoll.c gist:

The _epoll_wait function is almost a direct copy of the vector_of_pairs implementation from my last blog post. The basic idea of epoll is to initialize an epoll file descriptor (_epoll_create), add events to epoll (_epoll_ctl), and wait for epoll to tell you of events as they occur (_epoll_wait).

In order to actually implement an echo server using epoll, you need to write some standard boilerplate to set up a server socket, bind it to an address:port, and start accepting connections. Thankfully, the tcp unit in Chicken Scheme handles quite a bit of that.

Here's the test_epoll.scm file:

The first step to setting up a server socket is by calling tcp-listen. That function returns a server socket listener.

(define listener (tcp-listen 6666))

Next, you initialize epoll. To do this, you define a foreign-lambda to bind the foreign function _epoll_create to a Scheme function:

(define ##epoll#epoll_create (foreign-lambda int "_epoll_create"))
(define epfd (##epoll#epoll_create))

Those two lines define the FFI and creates the epoll file descriptor. All events are tied to the epfd. At the bottom of the test_epoll.scm file, there's the main loop. It takes as an argument the socket listener created above and extracts the listener's file descriptor (tcp-listener-fileno). ev-main-loop then adds the server socket to epoll and watches for the read event. An infinite loop is then entered and epoll_wait is called with a timeout of 200 milliseconds.

The function SCM_epoll_wait_cb is the callback that the C _epoll_wait function calls. Right now _epoll_wait always calls back into Chicken, so next time I'll make it dependent on there actually being events ready to operate on. SCM_epoll_wait_cb converts the vec object into a list of pairs and passes that list to the event handler (fd-event-list-handler).

fd-event-list-handler is where the magic happens. If there aren't any fd's to operate on, fd-event-list-handler evaluates to an empty list. When a server processes sockets, it has to differentiate between new connections and old connections. It does this by checking if the socket being operated on is the same as the server socket, which would mean we have a new connection. A new connection has to be passed to the accept function to create the client socket. Old connections are operated on normally.

There are two event conditions that the handler recognizes when dealing with old connections: read and write. I set up two hash tables for handling a client socket's I/O: fd-write-table and fd-read-table. The lookups are based on the client's file descriptor and initialized to empty strings once the client socket is accepted.

When the server reads from a socket, it writes that data into a temporary buffer and writes the modified string to the client's output buffer (using send-to-client). Modified as in splits on the newline character and only writes the string up to and including that point. send-to-client doesn't actually do any writing. It just appends strings to the output buffer. When epoll tells us that a socket is ready to write on, that's when we write the output buffer to the socket. Both buffers are emptied by this point.

After each read/write, the file descriptor is updated in epoll to reflect the new event. After a read takes place, the socket is ready for writing to. After a write, the socket is ready for reading from.

Like the last entry, this code requires Linux and Chicken Scheme. Compile the two files above to get a working server: csc epoll.c test_epoll.scm -o server

Once the server is running you can telnet to localhost on port 6666.