XS the easy way

XS has a reputation for being hard to access and I think it's a shame because I don't think it has to be: it's mostly that the Perl API is hard. What if you offload as much logic as possible to perl, and use XS only to expose functions to perl? That would be much easier for a casual XS writer who doesn't know anything about Perl's internals.

So, in this example I will write a simple XS module for a real-life API, in this case POSIX real-time semaphores. This allows you to synchronize data between different processes or threads. At its core the API is this:

sem_t* sem_open(const char* path, int open_flags, mode_t mode, unsigned int value);
int sem_close(sem_t *sem);
int sem_unlink(const char* path);

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);

What's the simplest way to expose that through XS? That would look something like this:

#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include <semaphore.h>
MODULE = POSIX::Sem                PACKAGE = POSIX::Sem
TYPEMAP: <<END
sem_t*    T_PTR
mode_t    T_IV
END
sem_t *sem_open(const char* path, int open_flags, mode_t mode, unsigned int value)
int sem_close(sem_t *sem)
int sem_unlink(const char* path)
int sem_wait(sem_t *sem)
int sem_trywait(sem_t *sem)
int sem_post(sem_t *sem)

Let me explain. An XS file always starts with a C section, followed by one or more XS sections declared using the MODULE keyword.

Our C section is fairly short and obvious, I'm first importing all the Perl bits that we need, this is a boilerplate that every XS module will start with. Then we include the API we want to wrap (semaphore.h).

Then we declare we want to declare the POSIX::Sem XS module. This contains a typemap and a list of functions.

The typemap declares which conversion template should be used for specific types; it will already be predefined for standard types such as int and char*, but we do need to tell it that sem_t* should be treated as a basic pointer (T_PTR), and mode_t as an integer (T_IV). Typemaps are really useful for making your XS code simpler, but sadly they're rather underdocumented.

After that, all we have to do is declare the functions in XS as they're declared in C. All the code needed to wrap the functions up and export them into Perl will be automatically generated. 

Now we just need some Perl code to wrap it up:

package POSIX::Sem;
our $VERSION = '0.001';
use 5.036;
use XSLoader;
XSLoader::load(__PACKAGE__, $VERSION);
use Errno 'EAGAIN';
sub open($class, $path, $open_flags, $mode = 0600, $value = 1) {
    my $sem_ptr = sem_open($path, $open_flags, $mode, $value);
    die "Could not open $path: $!" if not $sem_ptr;
    return bless \$sem_ptr, $class;
}
sub DESTROY($self) {
    sem_close($$self);
}
sub post($self) {
    die "Could not post: $!" if sem_post($$self) < 0;
}
sub wait($self) {
    die "Could not wait: $!" if sem_wait($$self) < 0;
}
sub trywait($self) {
    if (sem_trywait($$self) < 0) {
        return undef if $! == EAGAIN;
        die "Could not trywait: $!";
    }
    return 1;
}
sub unlink($class, $path) {
    die "Could not unlink: $!" if sem_unlink($path) < 0;
}
1;

Here we use XSLoader to load the XS module and import the functions from the library. I could have chosen to leave it at that, but I chose to wrap it up in  a nice object-oriented API by storing the pointer that we get from sem_open in a scalar reference so I can bless it.. Other than that all the Perl code does is turn any error into an exception. The resulting API can be used something like this:

use 5.036;
use POSIX::Sem;
use Fcntl 'O_CREAT';
my $sem = POSIX::Sem->open("foo", O_CREAT);
$sem->post;
$sem->wait;

That's all folks!

Leave a comment

About Leon Timmermans

user-pic