mruby-Bytecode einbinden
MRuby-Code kann mithilfe von mrbc derart verarbeitet werden, das man die Ruby-Quellcodedateien nicht mehr mitliefern muss.
mruby ist und bleibt ein interessantes Thema. Nachdem ich mich gestern
mit der Integration in CMake auseinandergsetzt habe, bin ich heute
der Frage nachgegangen, wie der mruby-Kern es eigentlich anstellt, die
ganzen .rb
-Dateien, die er enthält, nicht mitliefern zu müssen. Wie
so oft ist dies undokumentiert, und der Code des mruby-Buildsystems
ist recht abenteuerlich, sodass er nicht wirklich als lesbare Quelle
für Informationen herhalten kann.
Bei meiner Recherche habe ich im Internet ausschließlich Quellen gefunden, die nicht mehr auf dem aktuellen Stand der Technik sind. Anhand zweier Quellen jedoch kann in Verbindung mit dem aktuellen Quellcode von mruby das zur vollständigen Einkompilation des Ruby-Codes in eine C/C++-Executable erforderliche Verfahren hergeleitet werden. Die erforderlichen Schritte dokumentiere ich hier, damit ich sie nicht vergesse und auch andere etwas davon haben. Die Anweisungen beziehen sich auf mruby-Version 1.1.0.
MRuby enthält das Programm mrbc
(nachdem man mruby einmal gebaut
hat, liegt es im bin/
-Verzeichnis), das der zentrale Dreh- und
Angelpunkt des Verfahrens ist. mrbc
ist in der Lage, Ruby-Quellcode
in Bytecode für die RITE-VM zu übersetzen. Außerdem bietet es eine
Option -B
, die denselben Bytecode in Form eines für C verständlichen
Arrays von Integers ausgibt, dessen Name der Wert für die Option -B
ist, d. h. -Bfoobar
wird eine globale Variable foobar
definieren,
deren Wert der Bytecode als Array von Integers ist. Die Option -C
,
die von einigen Artikeln im Internet erwähnt wird und die eine ganze
C-Funktion erstellt hat, ist mittlerweile entfernt worden, es kann nur
noch mit -B
gearbeitet werden.
Die Theorie sieht nun also so aus:
- Eine Datei mit Ruby-Code anlegen.
- Die Datei mit
mrbc
parsen und in RITE-Bytecode übersetzen. - Den Bytecode in eine mruby-Instanz laden
- Den Bytecode ausführen.
Man beachte Schritt 4, den man, wenn man vorwiegend mit einer kompilierenden Sprache arbeitet, durchaus auch einmal vergisst. In Ruby werden Klassen und anderes aber erst durch die Ausführung definiert, das bloße Laden des geparsten Codes genügt nicht.
Im Folgenden stelle ich ein kurzes, praktisches Beispiel dieser Theorie vor.
Schritt 0: Vorbereitung
Zunächst muss ein neues Verzeichnis für das Projekt angelegt werden. Das sollte schließlich so aussehen:
mrbtest/
mruby/
mrblib/
src/
Das Verzeichnis mruby/
sollte mruby in der Version 1.1.0
enthalten. Das kann im einfachsten Falle durch Verwendung eines
Release-Tarballs von mruby.org geschehen oder durch
Git-Checkout.
Da für die weiteren Schritte die mruby-Bibliotheken benötigt werden, sollte man nun mruby kompilieren. Für die Zwecke dieses Tutorials genügt die Standardkonfiguration.
$ cd mruby
$ ruby minirake
Danach befindet sich mrbc
im Verzeichnis build/host/bin
sowie die zwei
mruby-Bibliotheken in Form statischer Bibliotheken im Verzeichnis
build/host/lib/
. Ich erhalte bei der Kompilation zZt. den Fehler
LD build/host/bin/mirb /usr/bin/ld: cannot find -ltermcap collect2: Fehler: ld gab 1 als Ende-Status zurück /usr/bin/ld: cannot find -ltermcap collect2: Fehler: ld gab 1 als Ende-Status zurück rake aborted! Command Failed: [gcc -o "/tmp/foo/mruby/build/host/bin/mirb" "/tmp/foo/mruby/build/host/mrbgems/mruby-bin-mirb/tools/mirb/mirb.o" "/tmp/foo/mruby/build/host/lib/libmruby.a" -lm -ltermcap -lreadline ] Rakefile:67:in `block (4 levels) in <top (required)>
, was aber wohl unschädlich ist. Jedenfalls werden die Executable
mrbc
und die mruby-Bibliotheken trotzdem erstellt. Wen es stört, der
entfernt mirb
aus der mruby-Build-Konfiguration.
Schritt 1: Ruby-Code
Dies ist der wohl einfachste Schritt. Erstellt wird eine Datei
mrblib/test.rb
unterhalb des Hauptprojektverzeichnisses mit
folgendem Inhalt:
module Kernel def tester puts "Hello, World!" end end
Schritt 2: In RITE-Bytecode übersetzen
Jetzt kommt der interessante Teil. Mithilfe von mrbc
wird die Datei
test.rb
in RITE-Bytecode übersetzt. Die Option -B
weist wie
beschrieben mrbc
an, den Bytecode in Form eines C-Arrays
auszugeben. Das Ergebnis ist dann eine Datei, die einfach wie jede
andere C-Datei kompiliert werden kann.
$ cd ..
$ ./mruby/build/host/bin/mrbc -Bmruby_bytecode \
-o src/rite_bytecode.c mrblib/test.rb
Dieser Befehl wird eine Datei src/rite_bytecode.c
erstellen, deren
Inhalt nichts weiter ist als ein C-Array mit dem Namen
mruby_bytecode
, welcher den kompilierten RITE-Bytecode enthält, der
durch Verarbeitung der Datei mrblib/test.rb
entstanden ist. Ihr
Inhalt sieht so aus:
#include <stdint.h> const uint8_t #if defined __GNUC__ __attribute__((aligned(4))) #elif defined _MSC_VER __declspec(align(4)) #endif mruby_bytecode[] = { 0x45,0x54,0x49,0x52,0x30,0x30,0x30,0x33,0xcd,0x78,0x00,0x00,0x00,0xdf,0x4d,0x41, 0x54,0x5a,0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x00,0xb1,0x30,0x30, 0x30,0x30,0x00,0x00,0x00,0x33,0x00,0x01,0x00,0x02,0x00,0x01,0x00,0x00,0x00,0x04, 0x05,0x00,0x80,0x00,0x44,0x00,0x80,0x00,0x45,0x00,0x80,0x00,0x4a,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x06,0x4b,0x65,0x72,0x6e,0x65,0x6c, 0x00,0x00,0x00,0x00,0x37,0x00,0x01,0x00,0x03,0x00,0x01,0x00,0x00,0x00,0x05,0x00, 0x48,0x00,0x80,0x00,0xc0,0x00,0x00,0x01,0x46,0x00,0x80,0x00,0x04,0x00,0x80,0x00, 0x29,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x06,0x74,0x65, 0x73,0x74,0x65,0x72,0x00,0x00,0x00,0x00,0x45,0x00,0x02,0x00,0x05,0x00,0x00,0x00, 0x00,0x00,0x05,0x00,0x26,0x00,0x00,0x00,0x06,0x00,0x00,0x01,0x3d,0x00,0x80,0x01, 0xa0,0x00,0x00,0x01,0x29,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x00,0x00,0x0d,0x48, 0x65,0x6c,0x6c,0x6f,0x2c,0x20,0x57,0x6f,0x72,0x6c,0x64,0x21,0x00,0x00,0x00,0x01, 0x00,0x04,0x70,0x75,0x74,0x73,0x00,0x4c,0x56,0x41,0x52,0x00,0x00,0x00,0x10,0x00, 0x00,0x00,0x00,0xff,0xff,0x00,0x00,0x45,0x4e,0x44,0x00,0x00,0x00,0x00,0x08, };
Man erkennt, wie die globale Variable mruby_bytecode
definiert wird.
Schritt 3 und 4: Bytecode laden und ausführen
Angelegt wird nun eine Datei src/main.c
, die den eigentlichen
Hauptcode enthalten und anschließend den vorhin erzeugten Bytecode
laden wird. Ich gehe an dieser Stelle nicht weiter auf die Technik zur
Initialisierung des mruby-Interpreters ein, sondern konzentriere mich
auf den Umgang mit dem Bytecode. Zunächst der Inhalt der Datei
src/main.c
:
#include <stdio.h> #include <stdint.h> #include <mruby.h> #include <mruby/irep.h> #include <mruby/dump.h> #include <mruby/proc.h> extern uint8_t mruby_bytecode[]; int main(int argc, char* argv[]) { printf("Initialisiere Interpreter.\n"); mrb_state* p_state = mrb_open(); printf("Lade Bytecode\n"); mrb_irep* bytecode = mrb_read_irep(p_state, mruby_bytecode); printf("Führe Bytecode aus.\n"); mrb_run(p_state, mrb_proc_new(p_state, bytecode), mrb_top_self(p_state)); printf("Gewöhnlicher mruby-Code:\n"); mrb_load_string(p_state, "tester"); printf("Beende Interpreter.\n"); mrb_close(p_state); return 0; }
In Einzelteile aufgebröselt ergibt sich folgendes:
#include <mruby.h> #include <mruby/irep.h> #include <mruby/dump.h> #include <mruby/proc.h>
Dies sind die Header, derer wir zwecks Aufruf diverser mruby-Funktionen bedürfen.
extern uint8_t mruby_bytecode[];
Wie erwähnt erzeugt die von mrbc
erzeugte C-Datei die Implementation
des Arrays mruby_bytecode
. Um aber darauf zugreifen zu können, ist
es erforderlich, ihn in dieser anderen Datei zu deklarieren. Natürlich
wäre es auch möglich gewesen, den gesamten Inhalt von
rite_bytecode.c
in die Datei main.c
zu übernehmen, dann wäre die
Deklaration entbehrlich. Bei wachsendem Bytecode ist das aber auf die
Dauer sehr unübersichtlich, folglich sollte man es schon von Anfang an
ordentlich machen und den Bytecode in seiner eigenen Datei
abspeichern. Den Typ uint8_t
habe ich der von mrbc
generierten
C-Datei entnommen; der Beitrag von Daniel Bovensiepen schlägt
dagegen noch char
vor, was mittlerweile wohl überholt ist.
mrb_irep* bytecode = mrb_read_irep(p_state, mruby_bytecode);
/* ... */
mrb_run(p_state, mrb_proc_new(p_state, bytecode), mrb_top_self(p_state));
Dieser Teil ist es, der im Web nicht mehr der aktuellen API von mruby
entspricht, weil mruby zwischenzeitlich den Typ mrb_irep
eingeführt
und das API von mrb_read_irep()
entsprechend verändert hat —
Beiträge, die hier noch mit einem int
arbeiten, sind veraltet. Diese
Funktion liest den Bytecode ein und gibt ein entsprechendes Handle
zurück, ohne ihn jedoch auszuführen. Dies besorgt mrb_run()
. Das
Handle ist jedoch selbst nicht ausführbar, vielmehr muss man es erst
in einen Proc verpacken, den man dann anschließend ausführt
(mrb_proc_new()
). mrb_top_self
gibt das main
-Objekt zurück,
welches als Binding dienen soll.
Der Rest des Programms besteht lediglich in einem Aufruf an die nun
neu definierte Methode tester
.
Schritt 5: Kompilation
Die Kompilation ist nicht weiter aufregend. Durch die entsprechende
Verwendung von mrbc
ist es nicht notwendig, auf die Ruby-Quelldatei
test.rb
zuzugreifen. Die erzeugten C-Quellen sind ausreichend.
$ gcc -Wall -Imruby/include src/main.c src/rite_bytecode.c \
mruby/build/host/lib/libmruby_core.a mruby/build/host/lib/libmruby.a \
-lm -o mrbtest
Beachtenswert an dieser Stelle ist insbesondere das Linking der Math-Library. Ansonsten handelt es sich um ein ziemlich selbsterklärendes Kompilationskommando.
Die Executable mrbtest
ist jetzt standalone, der Ruby-Quellcode
einkompiliert.
./mrbtest
Initialisiere Interpreter.
Lade Bytecode
Führe Bytecode aus.
Gewöhnlicher mruby-Code:
Hello, World! # <<<<<<<
Beende Interpreter.
Das ist alles. Es sei zuletzt noch darauf hingewiesen, dass das
Kommando mrbc
auch mehrere Ruby-Dateien als Eingabe nimmt und dafür
dann nur einen einzigen Bytecode herausgibt. Das kann die Einbindung
in ein Build-System einfacher machen, allerdings gilt für den Bytecode
dasselbe wie für gewöhnlichen Ruby-Code auch: Die Ausführreihenfolge
ist entscheidend. Ruft früherer Bytecode eine Methode auf, die erst
späterer Bytecode erzeugt, führt das, ganz wie auch in Ruby, zu einem
NameError. Diesen Punkt sollte man bei der Arbeit mit mehreren Dateien
unbedingt im Kopf behalten.
Gesamter Quellcode
Der gesamte Quellcode steht als Tarball zum Herunterladen zur Verfügung. MD5-Prüfsumme: aa75a02622c4d28631edfe36e0587390
Valete.