Day 15 – Calling native libraries from Perl 6

If you’ve used Perl 5 for any length of time, you’ve probably encountered a package with “::XS” in the name. Maybe you’ve even written one, in which case you can skip this paragraph. XS is Perl 5’s way of calling into native code (C libraries), and it works great for what it does. Writing such modules takes effort though; there’s C code and compilers involved, and it’s not much fun if you just want to get something working with a minimum of effort.

Perl 6 puts a lot of effort into letting you be lazy. That goes for using foreign code too. Let’s tell it we’re going to do that:

#!/usr/bin/env perl6
use v6;
use NativeCall;

That’s it. NativeCall does the hard work of talking to C libraries; you don’t need to manually compile anything. For Rakudo, you just need to install zavolaj.

Let’s have some fun with it. For demonstration, I’ll go and port part 1 of XMMS2’s C client tutorial to Perl 6.

First off, NativeCall needs to know how to talk to the C library. At present it knows how to translate to/from strings, numbers, and a thing called OpaquePointer. That last one is for things like database connection handles that you should only poke with a 10-foot pole.

We’re dealing with weird structs and a socket connection in this code, so let’s pretend those are subclasses of OpaquePointer. These don’t have any effect on the code, but it’ll be easier to follow:

class xmmsc_connection_t is OpaquePointer;
class xmmsc_result_t is OpaquePointer;
class xmmsv_t is OpaquePointer;

Now the C functions we’re using, in alphabetical order. One of these is slightly different from the rest – the original C code expects a **char to be passed in, or in other words a place in memory to write a string. We give it a Str is rw, and it just works.

sub xmmsc_connect(xmmsc_connection_t, Str $path) returns Int is native('libxmmsclient') { ... }
sub xmmsc_get_last_error(xmmsc_connection_t) returns Str is native('libxmmsclient') { ... }
sub xmmsc_init(Str $clientname) returns xmmsc_connection_t is native('libxmmsclient') { ... }
sub xmmsc_playback_start(xmmsc_connection_t) returns xmmsc_result_t is native('libxmmsclient') { ... }
sub xmmsc_result_get_value(xmmsc_result_t) returns xmmsv_t is native('libxmmsclient') { ... }
sub xmmsc_result_unref(xmmsc_result_t) is native('libxmmsclient') { ... }
sub xmmsc_result_wait(xmmsc_result_t) is native('libxmmsclient') { ... }
sub xmmsc_unref(xmmsc_connection_t) is native('libxmmsclient') { ... }
sub xmmsv_get_error(xmmsv_t, Str $error is rw) returns Int is native('libxmmsclient') { ... }
sub xmmsv_is_error(xmmsv_t) returns Int is native('libxmmsclient') { ... }

And because the point of this is to *avoid* writing C code, let’s make a nice wrapper object:

class XMMS2::Client {
    has xmmsc_connection_t $!connection;
    method new($client_name = 'perl6', $path = %*ENV) {
        self.bless(*, :$client_name, :$path);
    }
    method play returns Bool {
        my $result = xmmsc_playback_start($!connection);
        xmmsc_result_wait($result);
        return True if self.check-result($result);
        warn "Playback start failed!"; return False;
    }
    method !check-result(xmmsc_result_t $result) returns Bool {
        my $return_value = xmmsc_result_get_value($result);
        my Bool $failed = xmmsv_is_error($return_value);
        if $failed {
            xmmsv_get_error($return_value, my $error-str) and warn $error-str;
        }
        xmmsc_result_unref($result);
        return not $failed;
    }
    submethod BUILD($client_name, $path) {
        $!connection = xmmsc_init($client_name);
        xmmsc_connect($!connection, $path) or die "Connection failed with error: {xmmsc_get_last_error($!connection)}";
    }
    submethod DESTROY {
        xmmsc_unref($!connection);
    }
};

And here’s the magic part:

XMMS2::Client.new.play;

It connects! Then it spits out a Null PMC error. Looks like some work still needs to be done in NativeCall for passing null pointers around, but it’s still a work in progress.

NativeCall has been put to good use already though: check out
MiniDBI, which gives you a (working!) MySQL driver, among others. :)

(Late edit: I forgot the xmmsc_result_wait(). The above code should actually start playback now, though you’ll still get the Null PMC access error.)