Open Source im professionellen Einsatz
Linux-Magazin 03/2016
© Szilagyi Palko Pal, 123RF

© Szilagyi Palko Pal, 123RF

Die Ruby-artige Programmiersprache Crystal

Der dunkle Kristall

Das Open-Source-Projekt Crystal will das Beste zweier Welten vereinen: Die Einfachheit einer weitgehend zu Ruby ähnlichen Sprachsyntax mit der Geschwindigkeit und den Möglichkeiten der LLVM-Plattform.

1429

Im Herbst 2012 implementierte der Argentinier Ary Borenszweig seine Programmiersprache Crystal [1] als "Programmiersprache für Menschen und Computer". Dieser Satz drückt wahrscheinlich am besten aus, was diese Sprache verbinden möchte: Einfachheit und Eleganz einer Ruby-artigen Sprachsyntax mit der Effizienz und den Geschwindigkeitsvorteilen kompilierter Sprachen wie C. Maßgeblich trieb in den letzten Jahren die argentinische Software-Consulting-Firma Manas [2] die Entwicklung von Crystal voran. Dort ist auch Borenszweig angestellt.

Aktuell entwickeln rund 100 Freiwillige das Projekt auf Github [3] weiter. Alle Funktionen der Sprache lassen sich in einem Gitbook [4] nachschlagen. Erste Schritte macht ein Neuling ohne jede Installation, einen einfachen Web-basierten Crystal-Editor inklusive Compiler findet er unter [5].

Installation

Aktuell wartet Crystal noch nicht in den offiziellen Repositories der großen Distributionen. Für Debian und Ubuntu lässt sich das Crystal-Repository über

curl http://dist.crystal-lang.org/apt/ setup.sh | sudo bash

und für Red Hat und Centos über

curl http://dist.crystal-lang.org/rpm/setup.sh | sudo bash

anzapfen. Danach reicht unter Debian oder Ubuntu ein

sudo apt-get install crystal

aus, für Red Hat und Centos ein:

sudo yum install crystal

Darüber hinaus benötigt Crystal eine Reihe weiterer Bibliotheken zur Ausführungszeit [9]. Unter Debian und Ubuntu installiert der Anwender diese über

sudo apt-get install libbsd-dev  libedit-dev libevent-core-2.0-5  libevent-dev libevent-extra-2.0-5 libevent-openssl-2.0-5  libevent-pthreads-2.0-5 libgc-dev  libgmp-dev libgmpxx4ldbl libpcl1-dev libssl-dev libxml2-dev libyaml-dev  libreadline-dev

auf seinem System, wobei einige Pakete andere automatisch nach sich ziehen.

Das Skript aus Abbildung 1 führt den Ruby-Code aus Listing 1 als einfachen Benchmark aus. Es umfasst eine Schleife mit einer darin enthaltenen Multiplikation. Auf Basis der Zeitmessung am Anfang und am Ende des Programms lässt sich mit einer Subtraktion die Ausführungszeit ermitteln. Die liegt mit Ruby 1.9.3 bei rund 6 Sekunden. Mit J-Ruby braucht das Skript immerhin noch rund 4 Sekunden. Auf der Webseite [5] benötigt das gleiche Skript nur noch rund 0,3 Sekunden (Abbildung 2).

Listing 1

Benchmark

01 time1 = Time.now
02 (0..100000000).each do |i|
03 i*8
04 end
05 time2 = Time.now - time1
06 puts time2.to_f
Abbildung 1: Über die Webseite play.crystal-lang.org probiert der Einsteiger einfache Crystal-Skripte aus.
Abbildung 2: LLVM hilft dabei, das Skript zu übersetzen und auszuführen.

Installiert der Entwickler Crystal auf dem Rechner (siehe Kasten "Installation"), erwartet ihn danach das Kommandozeilen-Werkzeug »crystal« . Um das bestehende Ruby-Skript aus Listing 1 auszuführen, genügt es bereits, wenn er ein einfaches

$ crystal bench1.rb

eingibt. Auf dem beschriebenen Testsystem benötigte das Benchmark-Programm lediglich 0,23 Sekunden, um den Programmcode auszuführen, und arbeitete damit rund 30-mal schneller als die reine Ruby-Version.

Crystal interpretiert das Ruby-Skript nicht, sondern kompiliert es erst mit der LLVM-Infrastruktur (siehe Kasten "Siegeszug der LLVM-Compiler-Infrastruktur") in nativen Code. Dabei erzeugt Crystal reinen C-Code, der im Gegensatz zum Ruby-Code streng typisiert ist.

Siegeszug der LLVM-Compiler-Infrastruktur

An der Universität von Illinois starteten die Entwickler Vikran Adve und Chris Lattner im Jahre 2000 ein Studienprojekt, um eine neue Compiler-Architektur zu implementieren. Ihr Projekt Low Level Virtual Machine, kurz LLVM [6], sollte alle modernen Prinzipien für den Bau von Compilern berücksichtigen. Im Gegensatz zu dem altbekannten Aufbau eines Compiler aus Frontend, Optimizer und Backend ist der LLVM allerdings von vornherein mit der Absicht der beiden Entwickler entstanden, mehr als nur eine Sprache zu übersetzen und zu optimieren. Sie schufen aus diesem Grund eine interne Repräsentation des Programmcodes nach dem Kompilierprozess, die als LLVM Intermediate Repräsentation (IR) bekannt ist.

Auch der mittlerweile sehr populäre Clang-Compiler [7], der C, C++, Objectiv C und Objectiv C++ übersetzt, entstammt diesem Projekt. Mit seiner Hilfe erzeugt LLVM nicht nur ausführbaren Code auf den bekannten Desktopsystemen der x86-Prozessorfamilie, sondern auch auf ARM-basierten Systemen, wie sie die meisten mobilen Endgeräte nutzen.

Ein Ruby-Interpreter entscheidet situativ zur Laufzeit des Programms, welchen Typ eine Variable besitzt. Dave Thomas, Autor der ersten englischsprachigen Sprachreferenz für Ruby [8], prägte dafür den Begriff Duck Typing. Der Begriff selbst geht auf das Gedicht "Little Orphant Annie" des US-amerikanischen Dichters James Whitecomb Riley zurück, in dem es heißt: "See a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck." Den Typ der Variablen »a« ermittelt Ruby in Listing 2 zur Laufzeit.

Listing 2

Typ-Ermittlung in Ruby

01 a = 1
02 a = "Hallo Crystal Welt!"
03 puts a
04 a = a + 1
05 puts a

Anders als Ruby identifiziert Crystal den Typ von »a« bereits zur Übersetzungszeit. So wundert es nicht, dass Listing 2 in Ruby und Crystal grundverschiedene Fehlermeldungen nach sich zieht. Ruby führt den ersten »puts« -Befehl (Zeile 3) noch brav aus, weigert sich aber, den String mit einer Integerzahl zu addieren (Abbildung 3).

Abbildung 3: Ruby führt den ersten Teil des Skripts noch aus, streicht beim zweiten allerdings die Segel, um nicht einen String mit einer Integerzahl zu addieren.

Der Compiler von Crystal stößt ebenfalls auf dieses Problem, führt aber das Programm, im Gegensatz zu Ruby, nicht einmal bis zu der Zeile mit dem ersten »puts« aus (Abbildung 4). Er erkennt nicht nur die richtigen Typen für einfache Literale wie »nil« , »false« , 1, 3.14159265, »'a'« , »:crystal« und »"crystal"« selbstständig, sondern ist auch in der Lage, Zuweisungen wie »a = 1« sowie Funktionsargumente richtig zu typisieren.

Abbildung 4: Listing 2 lässt sich in Crystal nicht einmal kompilieren. Der Compiler weigert sich das Skript auszuführen.

Beispielsweise erlaubt Rubys Sprachsyntax die Definition einer einfachen Subtraktionsfunktion mit:

def sub(a,b)
  a - b
end

Die Anwendung kann diese Funktion aber unterschiedlich nutzen:

c = sub(2,1)

Oder wahlweise:

c = sub(2.5,1.6)

Auch diese Argumenttypisierung erlaubt Crystal über alternative Funktionen (einmal für Int32, einmal für Float64).

Doch Crystal kann noch mehr. Mit Hilfe des Kommandos »build« beeinflusst es die Codeproduktion in mehrfacher Hinsicht. So erzeugt der Aufruf

$ crystal build --release   bench1.cr

eine in der Größe optimierte ausführbare Datei, die ohne Crystal lauffähig ist. Das Kommando »tool« wiederum stellt den Quellcode einer Crystal-Datei dar, wodurch der Entwickler diese untersuchen kann. Das Kommando

$ crystal tool types bench1.cr

beispielsweise zeigt die unterschiedlichen Variablentypen an, die Crystal dem Programm während des Übersetzungsprozesses zuweist.

Sprachfunktionalität

Einiges löst Crystal aber anders als Ruby. Unions bilden ein wichtiges Sprachkonstrukt, um den Code bei statischer Typisierung flexibler zu gestalten. Der Aufruf »alias Int32OrString = Int32 | String« erzeugt den Typ »Int32OrString« , der wahlweise Integer oder String ist.

Eine weitere interessante Funktion stellen die so genannten Procs bereit. Ein Proc ist ein Funktionspointer mit einem optionalen Kontext. Mit ihm realisiert Crystal anonyme Funktionen folgender Art:

a = ->(x : Int32, y : Int32) { x - y }
a.call(42, 23) #=> 19

Wer in Crystal-Programmen mehrere Prozesse verwalten möchte, greift dafür auf das von Go und Erlang bekannte Konstrukt der Fibers und Channels zurück. Listing 3 zeigt einen Socketserver, der zehn Prozesse anlegt, um entsprechend viele Clients zu bedienen.

Listing 3

Channel, Spawn

01 require "socket"
02
03 ch = Channel(TCPSocket).new
04
05 10.times do
06   spawn do
07     loop do
08       socket = ch.receive
09       socket.puts "Hi!"
10       socket.close
11     end
12   end
13 end
14
15 server = TCPServer.new(1234)
16 loop do
17   socket = server.accept
18   ch.send socket
19 end

Auch eine Anbindung von C-Bibliotheken liegt nicht fern. Mit der Deklaration aus Listing 4 nutzt ein Entwickler etwa die mathematischen Funktionen der Standard-C-Library. Mit den von C bekannten Structs lässt sich zudem deren Deklaration wie in Listing 5 umsetzen.

Listing 4

Mathe mit Libc

01 @[Link("m")] #Math Library
02 lib LibC
03   fun sin(value : Float64) : Float64
04 end
05 y = LibC.sin(2.5)
06 puts y

Listing 5

Structs

01 lib LibC
02   struct Point
03       x, y : Int32
04   end
05 end
06 point = LibC::Point.new
07 point.x = 320
08 point.y = 200

Geschwindigkeitsvorteil

Um den von Crystal behaupteten Geschwindigkeitsvorteil zu bewerten, sollten die im Vergleich herangezogenen Anwendungen mehr können als nur bloße Schleifenoptimierungen. Tabelle 1 zeigt einen Geschwindigkeitsvergleich zwischen Ruby, J-Ruby und Crystal bei der Multiplikation zweier 100 mal 100 großer Matrizen [10].

Tabelle 1

100x100-Matritzen berechnen

Sprache

Zeit

Crystal

0,005 s

Ruby

0,136 s

J-Ruby

3,874 s

»jrubyc«

5,207 s

Um den Vergleich fair zu gestalten, misst er lediglich die Ausführungszeit. Nicht eingeschlossen ist der Wert, den Crystal zum Kompilieren benötigt. Bei J-Ruby wurden zwei Werte ermittelt. Der erste verzeichnet die direkte Ausführungszeit, der zweite die Java-Class-Datei, die mit Hilfe von »jrubyc« erzeugt wurde, um sie dann innerhalb der Java Virtual Machine auszuführen. Ein ähnliches Bild kristallisiert sich beim Berechnen einer Fibonacci-Folge heraus. Tabelle 2 zeigt die Resultate.

Tabelle 2

Fibonacci-Folge

Sprache

Zeit

Crystal

0,206 s

Ruby

0,250 s

J-Ruby

3,849 s

»jrubyc«

5,311 s

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 4 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Linux-Magazin kaufen

Einzelne Ausgabe
 
Abonnements
 
TABLET & SMARTPHONE APPS
Bald erhältlich
Get it on Google Play

Deutschland

Ähnliche Artikel

comments powered by Disqus

Ausgabe 10/2017

Digitale Ausgabe: Preis € 6,40
(inkl. 19% MwSt.)

Artikelserien und interessante Workshops aus dem Magazin können Sie hier als Bundle erwerben.