I am a big fan of Ruby and I am also a big fan of the less well known D programming language. Lately I had been wondering how hard it would be to get them both to work together. As we will find out - it is not actually all that hard to accomplish! This is the first entry in a three part series discussing Ruby and D interop.

Getting started

If you are following along at home this post assumes:

  • You have D installed and in your path.
  • You have Ruby installed and in your path.

Interop options

There are a few immediate options available to us.

We do not always need the overhead and complexity of building against the devkit and I did not find the Fiddle documentation too useful.

For this project I opted to go with the RubyFFI method of interop. Add ffi to your gemfile or type gem install ffi at the terminal to install for your Ruby installation.

D ABI

A really useful thing about D is it has ABI compatibility with C! This means that RubyFFI will have no issues interacting with functions defined in the shared libraries.

The simple case

To get started let us examine the simple case of making a function available to Ruby from D.

//simple.d
extern(C) int add(int a, int b) {
    return a + b;
}

We have defined a simple function that we want to call from C code.
Type in dmd -shared simple.d at the terminal to compile the library.

On the Ruby end we want to create the binding that tells RubyFFI how to interact with the D library.

#!/usr/bin/ruby
#simple.rb
require 'ffi'
#Ruby-D binding
module SIMPLE
    extend FFI::Library
    #Change to .dll for Windows
    ffi_lib "#{Dir.pwd}/simple.so"
    #int add(int a, int b)
    attach_function :add, [:int, :int], :int
end
#Invocation of D function
p SIMPLE.add(1,2)

If all goes well you should see the output of 3 in your terminal after running simple.rb.

Windows notes:

  • You will need to place the export keyword before extern(C) or this will not work.
  • Rename the produced .exe to .dll or .so and adjust the binding code. It is not required by RubyFFI but keeps things consistent.

The interesting case

You may have noticed the above D code is identical to C code and that is not terribly interesting. Fear not for we can take full advantage of D!

//beep.d
import std.stdio : writeln;
extern(C) void beep(int count) {
    foreach(i; 0 .. count)  {
        "Beep!".writeln;
    }
}

Aha, now there we go, this code is much more in the D style.

#!/usr/bin/ruby
#beep.rb
require 'ffi'
module BEEP
    extend FFI::Library
    #Change to .dll for Windows
    ffi_lib "#{Dir.pwd}/beep.so"
    #void beep(int count)
    attach_function :beep, [:int], :void
end
BEEP.beep(3) #Beep! three times.

So that is pretty awesome, we can leverage the D standard library, all of the standard functionality and RubyFFI is perfectly fine with the binary generated by it.
Unfortunately this will not work on Windows out of the box which is something we will get to shortly.

The D Runtime

So far our code on the D side has not performed any memory allocations. If we want to access this functionality amongst other things we will need to initialise the D Runtime.

Let us look at a perhaps contrived FizzBuzz program.

//fizz.d
import std.stdio : writeln;
import std.conv : to;
export extern(C) void fizzBuzz(int num) {
    auto result =
    num % 15 == 0 ? "FizzBuzz" :
    num % 5  == 0 ? "Buzz"     :
    num % 3  == 0 ? "Fizz"     :
                    "Boring"   ;
    //The below line performs a memory allocation.
    (num.to!string ~ " is " ~ result).writeln;
}

To compile this we now want to use the command dmd -shared -defaultlib=libphobos2.so fizz.d.

#!/usr/bin/ruby
#fizz.rb
require 'ffi'
module FIZZ
    extend FFI::Library
    ffi_lib "#{Dir.pwd}/fizz.so"
    attach_function :rt_init, [], :void
    attach_function :rt_term, [], :void
    #void fizzBuzz(int num)
    attach_function :fizzBuzz, [:int], :void
end

FIZZ::rt_init

at_exit do
    FIZZ::rt_term
end

FIZZ.fizzBuzz(10) #10 is Buzz
FIZZ.fizzBuzz(15) #15 is FizzBuzz
FIZZ.fizzBuzz(9)  #9 is Fizz

We are now binding to two functions from the D Standard Library (Phobos) which is why we needed the -defaultlib switch. This gives us the rt_init and rt_term functions which are for initialising and terminating the D Runtime respectively.

If the rt_init function is not called by your Ruby script the called library code may hang or crash.

Unlike with C libraries thanks to the D Runtime we have a garbage collector working away in the background much like in Ruby. It is possible to disable the D garbage collector and do manual memory management if further performance is required.

What about Windows?

import core.sys.windows.windows;
import core.sys.windows.dll;
HINSTANCE g_hInst;
extern (Windows) BOOL DllMain(HINSTANCE hInstance, ULONG ulReason, LPVOID pvReserved) {
    switch (ulReason) {
    case DLL_PROCESS_ATTACH:
        g_hInst = hInstance;
        dll_process_attach( hInstance, true );
        break;

    case DLL_PROCESS_DETACH:
        dll_process_detach( hInstance, true );
        break;

    case DLL_THREAD_ATTACH:
        dll_thread_attach( true, true );
        break;

    case DLL_THREAD_DETACH:
        dll_thread_detach( true, true );
        break;

        default:
    }
    return true;
}

Add this snippet to the fizz.d example from earlier. Compile with dmd -shared fizz.d -of="fizz.dll"

#!/usr/bin/ruby
#fizz.rb
require 'ffi'
module FIZZ
    extend FFI::Library
    ffi_lib "#{Dir.pwd}/fizz.dll"
    #void fizzBuzz(int num)
    attach_function :fizzBuzz, [:int], :void
end

FIZZ.fizzBuzz(10) #10 is Buzz
FIZZ.fizzBuzz(15) #15 is FizzBuzz
FIZZ.fizzBuzz(9)  #9 is Fizz

In the Windows case we do not need rt_init and rt_term. When we attach to the DLL the runtime is automatically initialised for us.

Conclusion

Phew! That was a lot to go through. Hopefully by this point you have a good idea how to build basic RubyFFI compatible libraries with the D compiler.

In the next part in the series I will be talking about some D code I have written to automatically generate the correct RubyFFI bindings directly from our D code.

Resources

  1. Call D from Ruby using FFI
  2. Win32 DLLs in D
  3. RubyFFI Binding Guide