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 |
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:
! 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.
#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:
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.
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:
#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.
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 |
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.
