mruby-Bytecode einbinden
Marvin Gülker · 11.05.2015
MRuby-Code kann mithilfe von mrbc derart verarbeitet werden, dass man die Ruby-Quellcodedateien nicht mehr mitliefern muss.
Kategorien: Cpp, C, Ruby, Software
mruby ist und bleibt ein interessantes Thema.
Nachdem sich der gestrige Artikel mit der
Integration in CMake
auseinandergesetzt hat, geht es heute um die Frage, wie der
mruby-Kern es eigentlich anstellt, die ganzen .rb
-Dateien, die er
enthält, nicht mitliefern zu müssen. Leider ist dies einmal mehr nicht
dokumentiert.
Im Internet auffindbare Quellen sind leider oft nicht mehr auf dem aktuellen Stand der Technik. 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 dokumentiert dieser Artikel. 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 wird ein kurzes, praktisches Beispiel dieser Theorie vorgestellt.
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/
. Zurzeit tritt der 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)>
auf, 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. An dieser Stelle wird nicht weiter auf die Technik zur
Initialisierung des mruby-Interpreters eingegangen, sondern nur der
Umgang mit dem Bytecode thematisiert. 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 aufgeteilt ergibt sich folgendes:
#include <mruby.h> #include <mruby/irep.h> #include <mruby/dump.h> #include <mruby/proc.h>
Dies sind die Header, derer man zwecks Aufruf diverser mruby-Funktionen bedarf.
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
enstammt
der von mrbc
generierten C-Datei; 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 besonders. 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.