Syscalls aus Ruby
Projekte aus dem StudiumFü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.