Aaron Fischer Ingenieur, Vater, Heimwerker, Problemlöser

08 Juni, 2010

Syscalls aus Ruby

Projekte aus dem Studium

Für unsere Thesis hatten wir uns gestern nochmal etwas mit dem FFI (Foreign Function Interface) unseres Interpreters beschäftigt. Es geht darum, Funktionsaufrufe aus anderen Programmiersprachen im eigenen Interpreter aufzurufen. Da wir Ruby als Hostsprache gewählt haben, wollten wir auch nativen Ruby-Code in unserer Skriptsprache verwenden.

Das Wort, welches einen TCP-Socket öffnet, sieht so aus: (Wort deswegen, da es sich um eine konkatenative Programmiersprache handelt. Alles was nach dem with kommt, ist nativer Ruby-Code. Mehr dazu, nachdem die Thesis abgegeben wurde :)

:open-tcp-socket $host $port => { $host $port { $id <<native>> } <<socket>> } with
  require "socket"
  socket = TCPSocket.new(bindings.get("$host"), bindings.get("$port").to_i)
  bindings.bind("$id", store_native(socket))
.

Wir haben uns natürlich andere Implementierungen angesehen. Factor beispielsweise hat sein FFI verdammt elegant gelöst. Dadurch, dass Factor in C++ geschrieben ist, ist hier der Weg zum Betriebssystem nicht so weit entfernt wie bei Ruby. Die Definition von der Systemfunktion rename sieht bei Factor so aus:

USING: alien.c-types alien.syntax ;
IN: unix.ffi
LIBRARY: libc FUNCTION: int rename
( c-string from, c-string to ) ;

So etwas wollten wir natürlich auch. Glücklicherweise gibt es ein total undokumentiertes Feature in Ruby, welches dem gewillten Programmierer erlaubt, Funktionen aus Shared Libs aufzurufen. Es nennt sich Ruby/DL. Dieses Feature lässt sich nun nutzen, um einen ähnlichen Komfort wie bei Factor zu erzeugen:

require "dl"
class FFI
  def initialize
    @libc = DL.dlopen("libc.so.6")
    @types = {String => "S", Fixnum => "I", Float => "F", nil => "0"}
  end

  def syscall(function, return_type, *parameters)
    params = [@types[return_type]] + parameters.map {|param| @types[param.class]}
    fun = @libc[function, params.to_s]
    fun.call(*parameters)
  end
end

Die Klasse macht im Prinzip nur das Type-Mapping, ruft die Funktion auf und gibt das Ergebnis zurück. Somit braucht es nur noch ein Wort (vorausgesetzt, man weiß welche Parameter die Funktion benötigt), um das FFI anzusprechen.

:syscall #function #return_type { @params } => { @result } with
  FFI.new.syscall(bindings.get("$function"), bindings.get("$return_type"), bindings.get("@params"))
.

:rename #from #to => rename <<integer>> { { #from <<string>> } { #to <<strong>> } } | syscall .

Das ganze Vorgehen ist natürlich sehr vereinfacht. Dinge wie Structs, Arrays oder Pointer müssen natürlich speziell behandelt werden. Auch verabschiedet sich Ruby regelmäßig mit einem SegFault, wenn man die Funktion mit falschen Parametern aufruft.