Calling Fortran functions from Ruby scripts.


It may seem like an odd pairing of languages, especially since the interfacing is somewhat painful. I feel like Ruby is a great complement to Fortran, however, for a couple of reasons:

Ruby Fortran
Beautiful, elegant, fun Beautiful, elegant, but only when working with matrices
Slow runtime, quick development/debug Fast runtime, slow development/debug
Metaprogramming & Magic, active community Concurrency, numerical aptitude, entrenched historical codebase
Ruby, as everybody who has every tried it knows, is pretty awesome. Fortran, as everybody who has ever heard of it knows, is kind of rough in comparison, even today. But alas, the latter has fifty years of history behind it: StackOverflow Q/A is replaced largely by formal military/national lab documents describing algorithms and techniques.

The sheer quantity of code already written and tested is not something we should let wither away and die. Sure, it's written in extra-ugly fixed-format F77, but if you can stuff it through a compiler and get your output into something civilized like Ruby, you've saved yourself the time of writing the function AND testing it.
Pickaxe: C Extensions
Programming Ruby chapter on C extensions; This will explain most of the C/Ruby interaction, but doesn't cover all of the API.
Eqqon & Matz: C Extension Libraries
Describes in greater detail datatypes, data checking functions, and the rest of the API
GFortran: Mixed Language Programming
Make sure to read either this article or the relevant one for your Fortran compiler! Describes C/Fortran intrinsic functions and compatible datatypes.
Fortran/C Interoperability
Example code and compilation commands (For GFort and IFort both) of Fortran calling C and vice versa.

An essential part of working with more than one language is a build script. Here is the Ruby/C/Fortran "Bridge" build script that I wrote for my own use. I chose not to use the traditional extconf.rb makefile generation route so that I would maybe learn greater detail about C/Ruby interop, but also because it doesn't have any sort of support for Fortran compilation/linking.

In order to get that build script working, you may need to tailor a few things. I expect that the include string for C compilation will be the biggest issue: I actually did extconf.rb and generated a Makefile, enabled verbose mode, and copied the include string from that because I couldn't figure it out on my own. What a mess.


Lets check out some code:



Fortran / C Interoperation Example:


! ftest.f08
function testfort(testin)  bind(c, name="testfort") result(testoutput)
  use, intrinsic :: iso_c_binding
  implicit none
  integer(kind=c_int), intent(in) :: testin
  integer(kind=c_int) :: testoutput

  testoutput = testin + 10


end function testfort
              

This simple Fortran function demonstrates the bind(c) function and the iso_c_binding intrinsic module. bind(c) is "required" for interoperation according to GFortran, but is not strictly necessary for correct operation.

The iso_c_binding module is used to make sure that Fortran datatypes play nicely with C datatypes: Here we use c_int exclusively, but there are equivalents for most datatypes. Here is a list of the datatypes between C and Fortran and is a handy reference.

// ctest.c           
#include <stdio.h>

int testfort(int *); // Equivalent to int testfort_(int*);

int main() {
  int i = 10;
  int j;

  j = testfort(&i); // NOT equivalent to testfort_(&i) unless we omit bind(c)
  printf("hi %d\n", j);

  return 0;
}
              

The C program calling Fortran functions usually appends an underscore to function names, but when using bind(c, name="..."), this underscore is no longer used. The function prototype works with or without the underscore; (i.e. int testfort_(int *); has the same result.)

gcc -c ctest.c    # Compile C driver into ctest.o
gfortran -c ftest.f08    # Compile F function into ftest.o
gcc -o c_calls_f ctest.o ftest.o -lgfortran # Link together into ./c_calls_f
              


[cameron@ghast-l test_bindc]$ ./c_calls_f
hi 20
              

Testing this function yields our nonsensical output, verifying that C/Fortran are communicating.





Ruby / C Interoperation Example:


#include "ruby.h"
#include "stdio.h"

VALUE TestModule = Qnil;


VALUE method_called_by_ruby(VALUE self) {
  printf("Hello, Ruby. (From C)\n");
  return 0;
}

void Init_testmodule() {
  TestModule = rb_define_module("TestModule");
  rb_define_method(TestModule, "called_by_ruby", method_called_by_ruby, 0);
}
              

The C extension makes use of ruby.h, which means that you have to manually link against Ruby's headers. This is a task best outsourced to build scripts; See Ruby/C/Fortran "Bridge" build script above.

Notice a few things:

  • C uses VALUE to represent Ruby objects; Everything in Ruby is an object, so everything in the interfacing C code must work with VALUEs.
  • The function Init_testmodule() matches the shared object file name, testmodule.so; These must match or you'll get an undefined symbol error.
  • We declare a method, prefixed with method_, which is bundled into a module and exported for use with Ruby in the last two lines.
require './testmodule.so'
include TestModule
called_by_ruby()
              

The Ruby driver is blissfully simple: Simply require the shared object file generated from your C compiler, load the module defined using ruby.h constructs, and call the method in question.

$ rake rebuild # Depends on Ruby/C/Fortran "Bridge" build script
$ gcc -shared -o testmodule.so ctest.o -lruby # links into testmodule.so
              

Use your handy-dandy build script to take care of remembering the many compiler options required; See the build script section above for tips on getting everything set up for compilation. Then use your C compiler to link the C file into the shared object expected by Ruby against Ruby libraries.

[cameron@ghast-l test_cext]$ pry
[1] pry(main)> require './testmodule.so'
=> true
[2] pry(main)> include TestModule
=> Object
[3] pry(main)> called_by_ruby
Hello, Ruby. (From C)
=> false
[4] pry(main)> called_by_ruby()
Hello, Ruby. (From C)
=> false
              

The easiest way to determine whether your C extension is working is to boot up Pry (or, heaven forbid, irb) and walk through the steps manually.





Puting it all together -- Calling Fortran from Ruby:


subroutine sieve_of_eratosthenes(max_value, number_list) bind(c, name="sieve_of_eratosthenes")
  use, intrinsic :: iso_c_binding
  implicit none

  integer(kind=c_int), intent(in) :: max_value
  integer(kind=c_int), dimension(max_value), intent(out) :: number_list(max_value)
  integer(kind=c_int) :: outer_high_bound, inner_high_bound, i

  number_list = 1
  number_list(1) = 0

  outer_high_bound = int (sqrt (real (max_value)))
  inner_high_bound = max_value

  do i = 2, outer_high_bound
    if (number_list(i) == 1) number_list(i*i : max_value : i) = 0
  end do

end subroutine sieve_of_eratosthenes
              

Lets explore a slightly more interesting example where Ruby calls a Fortran function that calculates primes using the Sieve of Eratosthenes.

Note that:

  • C/Fortran interoperation disallows a function to return an array. This behavior forces the use of subroutines, as shown in this example. The output variable is declared and allocated in the C function and passed into the Fortran subroutine for modification.
  • The name attribute can be used to deobfuscate or shorten function names. I elected to keep it gloriously too long.
#include <stdio.h>
#include "ruby.h"

int sieve_of_eratosthenes(int *, int *);
VALUE SieveModule = Qnil;

VALUE method_invoke_sieve(VALUE self, VALUE iterations) {
  int output [iterations], ii;
  VALUE result;
  int c_iterations = NUM2INT(iterations);
  if (c_iterations) {
    sieve_of_eratosthenes(&c_iterations, output);
  }

  result = rb_ary_new();
  for(ii=0; ii<c_iterations; ii++) {
    int prime = ii + 1;
    rb_ary_push(result, INT2FIX(output[prime]));
  }
  return result;
}

void Init_sievemodule() {
  SieveModule = rb_define_module("SieveModule");
  rb_define_method(SieveModule, "invoke_sieve", method_invoke_sieve, 1);
}
              

This C interface is somewhat more complicated than the previous examples, since now we're juggling datatypes and conversion specifications for three languages at once. This program exposes a Ruby module and method that accepts data from Ruby and converts it into a form usable by C. We then push the data inside C datatypes to the Fortran function, which handles the conversion.

As mentioned before, in order to return an array from a Fortran procedure to C, you must use subroutines instead of functions. After this, the array data is loaded into a Ruby array using the functions in "ruby.h"

require './sievemodule.so'
include SieveModule


iterations = 1000
result = invoke_sieve(iterations)


puts "Primes under #{iterations}:"
result.each_with_index do |item, ii|;
  if result[ii] == 1 # Then value is a prime; Print it out.
    print ii + 2
    print " "
  else 
    # Value is composite.
  end
end
puts "\n"
              

Ruby script is as simple as ever, with a little snippet added to print out prime numbers. (Remember that what's getting passed back is an array of 1 and 0 integers indicating prime status rather than a list of prime numbers or a list of bools.)

[cameron@ghast-l test_bindfcr]$ rake rebuild
[cameron@ghast-l test_bindfcr]$ gcc -shared -o sievemodule.so ctest.o sieve_e.o -lgfortran -lm -lruby
[cameron@ghast-l test_bindfcr]$ ruby rtest.rb 
Primes under 1000:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199 211 223 227 229 233 239 241 251 257 263 269 271 277 281 283 293 307 311 313 317 331 337 347 349 353 359 367 373 379 383 389 397 401 409 419 421 431 433 439 443 449 457 461 463 467 479 487 491 499 503 509 521 523 541 547 557 563 569 571 577 587 593 599 601 607 613 617 619 631 641 643 647 653 659 661 673 677 683 691 701 709 719 727 733 739 743 751 757 761 769 773 787 797 809 811 821 823 827 829 839 853 857 859 863 877 881 883 887 907 911 919 929 937 941 947 953 967 971 977 983 991 997
              

This time, link everything into a shared object suitable for loading into Ruby. Don't forget to include Fortran, Ruby and any other relevant libraries.





Benchmarks & Conclusion:


Methodology:

I tested three different implementations of the Sieve of Eratosthenes: My own Ruby/C/Fortran stack (shown above,) a pure Fortran implementation using the same core algorithm, and a pure Ruby implementation.

Each program was tested against 10,000 iterations and 1,000,000 each. In addition, each run was repeated without I/O processing (printing of the prime numbers and determining which ones are prime.) For each circumstance, run time was measured with the Unix utility time 10 times and averaged (arithmetic mean.)


Implementation Run time (10,000) Run time (1,000,000) Description
Pure Ruby 0.1566 (seconds) 1.7979 s I/O included
Pure Ruby 0.0267 s 0.6064 s Computation only
Pure Fortran 0.0138 s 2.0481 s I/O included
Pure Fortran 0.003 s 0.0192 s Computation only
Full stack 0.0393 s 1.404 s I/O included
Full stack 0.0221 s 0.0592 s Computation only
Conclusion:

Here we see that I/O is a significant factor, with Fortran actually coming in as the slowest when I/O is included. Naturally, it is lightning-fast again sans output. Note that this particular implementation of the Sieve requires the I/O routine to check which data are primes, increasing the load on that part of the full task while lessening the work done by the "computation only" circumstances.

Pure Ruby is on par with its C/Fortran extension given a small task with such an efficient algorithm. But after boosting the work up to 1 million integers, pure Ruby is left in the dust.

I conclude that a binary extension is definitely worth the time spent for Ruby scripts that need to do any significant calculation. We all already knew that, of course. I'm not sure that writing any new functions in Fortran is a better idea than writing them in C, because every new datatype is another torrent of complexity when you have to convert it back and forth.

Further investigation is needed to determine whether Fortran extensions would be even remotely robust. In addition, I intend to explore interfacing with existing legacy code, which is probably a much more viable use of Ruby/Fortran.

Thanks for reading! Hope you're inspired to play further with Fortran or Ruby.





References:
[1] Pickaxe: C Extensions
The quinessential 'learning Ruby' book, available online, discussing C extensions.
[2] Eqqon & Matz: C Extension Libraries
An annotated version of an explanatory text that Matz, creator of Ruby, checked somewhere into the source.
[3] GFortran: Mixed Language Programming
GCC Fortran compiler documentation on C/Fortran interoperation and extensions.
[4] UCLA: C/Fortran Interoperation
Example code and compilation options for C calling Fortran code, as well as Fortran calling C code.
[5] GFortran: ISO_C_BINDING Intrinsic Module
Documentation for the Fortran module intended for C interoperability. Good datatype reference.
[5] Wikipedia: Sieve of Eratosthenes
Wiki explanation of Sieve of Eratosthenes history and algorithm


Ruby/C/Fortran Interoperation


Dog wearing a Fedora
Originally Written:
July 8, 2012
Updated on:
July 9, 2012


Related Reading:

Installing Ruby 1.9.3/RVM on Fedora
R/C/F Pattern Description