QVINTVS · SCRIBET

Einführung in mruby, Teil 2: C-Structs wrappen

Nach dem ersten Teil möchte ich mich nun damit beschäftigen, wie man C-Structs in der mruby-Umgebung zur Verfügung stellt. So ganz nebenbei werden wir dabei auch lernen, wie man Methoden und Klassen definiert.

Ausgangslage

Es liegt folgender Header vor, und wir wollen den mystruct-Struct in der mruby-Umgebung zur Verfügung stellen.

#ifndef FOO_H
#define FOO_H

struct mystruct {
  int first_value;
  int second_value;
};

#endif

Grundgerüst

Die einführenden Erklärungen zur Einbindung von mruby aus dem ersten Teil werde ich hier nur kurz wiederholen, der geneigte Leser möchte sich daher vielleicht zunächst jenen Teil durchlesen.

Die Datei foo.c beginnt wie üblich mit den Includes, die dieses mal etwas umfangreicher ausfallen.

#include <stdio.h> /* printf() und Anverwandte */
#include <stdlib.h> /* Wir brauchen malloc() */
#include <mruby.h> /* Haupt-Header von mruby */
#include <mruby/compile.h> /* Funktionen zur Übersetzung in Bytecode */
#include <mruby/data.h> /* Funktionen zum Struct-Wrapping */
#include <mruby/class.h> /* Funktionen zur Definition von Klassen und Methoden */
#include "foo.h" /* Unser foo.h-Header von oben */

Wer aufmerksam gelesen hat, hat schon im ersten Teil festgestellt, dass wir mruby/compile.h eingebunden haben. Dies war erforderlich, da die Funktion mrb_load_string() von diesem Header zur Verfügung gestellt wird; daneben finden sich dort auch weitere Funktionen, mit denen man die Bytecode-Kompilation und dessen Evaluation trennen oder auch externe Dateien laden kann. Insgesamt ist mruby sehr modular aufgebaut, sodass wirklich nur die Header eingebunden werden müssen, die erforderlich sind — was leider auch den Nachteil hat, dass man ständig suchen muss, in welchem Header man denn nun eine bestimmte Funktion findet, die mal wieder nicht da ist, wo man sie vermutet hätte. Die gesamte Liste der mruby-Header ist online verfügbar.

Als nächstes nehmen wir uns die main()-Funktion vor, die wie schon im ersten Teil nichts weiter tun soll als eine kleine Zeile Ruby-Code auszuführen. In diesem Falle allerdings werden wir die noch zu definierende Klasse MyStruct instanziieren und ein wenig mit ihr spielen.

/* Haupteinsprungpunkt */
int main(int argc, char* argv[])
{
  mrb_state* p_state = NULL;

  printf("-- Initialising mruby --\n");
  p_state = mrb_open();

  printf("-- Extending mruby --\n");
  setup_ruby(p_state); /* Kümmern wir uns gleich drum */

  printf("-- Executing Ruby code --\n");
  mrb_load_string(p_state, "m = MyStruct.new;m.first_value = 3;m.foo"); /* Code ausführen (ruft C-Funktionen auf!) */

  printf("-- Finishing mruby --\n");
  mrb_close(p_state);

  return 0;
}

Soweit nichts besonderes, die Funktion entspricht dem Muster, welches wir bereits im ersten Teil kennengelernt haben, lediglich führen wir ein wenig mehr Code aus. Was aber hat es mit diesem ominösen setup_ruby() auf sich? Nichts außergewöhnliches, mir fiel nur kein besserer Name ein. Diese Funktion definiert die Klasse MyStruct und fügt ihr all die Methoden hinzu, die wir zur Interaktion mit unserem Struct brauchen. Aussehen tut sie folgt:

void setup_ruby(mrb_state* p_state)
{
  /* Definiere die MyStruct-Klasse rubyseitig als Subklasse von
   * Object (dies muss auf der C-Seite explizit angegeben werden;
   * alle Core-Klassen und -Module sind unterhalb des mruby_state-
   * Structs auf ähnliche Art verfügbar). Der Makroaufruf an
   * MRB_SET_INSTANCE_TT() ist **essentiell**, um mruby klarzumachen,
   * dass es sich bei den Instanzen von MyStruct um spezielle Objekte
   * handelt, die einen externen Pointer mit sich herumtragen. Ohne
   * diesen Aufruf markiert mruby die Instanzen von MyStruct als
   * reine Ruby-Objekte (MRB_TT_OBJECT) und jegliche Versuche, den
   * Pointer auf den C-Struct wieder aus dem Objekt auszupacken,
   * führen zu schwer verständlichen Fehlern bis hin zu Segfaults. */
  struct RClass* p_MyStruct = mrb_define_class(p_state, "MyStruct", p_state->object_class); /* class MyStruct < Object */
  MRB_SET_INSTANCE_TT(p_MyStruct, MRB_TT_DATA);

  /* Definiere eine #initialize- und die #foo-Methode,
   * ganz wie in Ruby. Da wir jedoch keine direkte
   * Entsprechung zu attr_accessor in der C-API finden,
   * müssen wir auch Getter- und Setter-Methoden händisch
   * definieren.
   *
   * Die ARGS_*-Makros sind in mruby.h deklariert und sollten
   * von sich aus verständlich sein. */
  mrb_define_method(p_state, p_MyStruct, "initialize", mystruct_initialize, ARGS_NONE()); /* def initialize */
  mrb_define_method(p_state, p_MyStruct, "foo", mystruct_foo, ARGS_NONE()); /* def foo */
  mrb_define_method(p_state, p_MyStruct, "first_value", mystruct_get_first_value, ARGS_NONE()); /* def first_value */
  mrb_define_method(p_state, p_MyStruct, "first_value=", mystruct_set_first_value, ARGS_REQ(1)); /* def first_value= */
}

Der obige Code definiert die Klasse MyStruct und auf ihr die Methoden #initialize, #foo, #first_value und #first_value=. Beachten Sie unbedingt, das Makro MRB_SET_INSTANCE_TT wie gezeigt aufzurufen — andernfalls erzeugen Sie lediglich normale Ruby-Objekte, die keinen externen Pointer wrappen können.

Auch tut sich hier ein ziemlich großer Unterschied zur C-API des MRI auf, den ich nicht verschweigen möchte, da er Leute, die häufiger C-Extensions schreiben (so wie mich) sicherlich böse beißen wird.

Der Instanziierungsprozess in mruby contra dem im MRI

In mruby gibt es keinen Hook für die ::allocate-Methode. Genaugenommen wird Class#allocate nicht einmal in die Ruby-Umgebung weitergereicht, sodass es nicht wie im MRI möglich ist, #initialize durch den direkten Aufruf von ::allocate auszuhebeln. Von Rubys Sicht her existiert die Methode ::allocate gar nicht (→ ein Aufruf führt zu einem NoMethodError) — in C gibt es sie (mrb_obj_alloc() in gc.c). Daher hier kurz der Codeflow bei der Instanziierung einer Klasse in mruby:

  1. Aufruf von Class#new, z.B. MyStruct.new
  2. Diese Methode ist in C definiert: mrb_class_new_instance() in class.c.
  3. mrb_class_new_instance() ruft die nur in C verfügbare Funktion mrb_obj_alloc() (definiert in gc.c) auf, die ::allocate im MRI entspricht, aber in die nicht eingehakt werden kann.
  4. mrb_obj_alloc() allokiert den erforderlichen Speicher für ein mruby-Objekt und gibt ihn zurück. Keine Callbacks.
  5. mrb_class_new_instance() setzt die Klasseninformation des neuen Objekts.
  6. mrb_class_new_instance() ruft die #initialize-Methode auf dem Objekt auf.
  7. mrb_class_new_instance() gibt das fertig initialisierte Objekt zurück.
  8. Class#new gibt das Objekt zurück.

Durch dieses Prozedere ergibt sich ein entscheidender praktischer Unterschied im Code: Wir brauchen uns nicht darum kümmern, dass unser Objekt zumindest nicht segfaultet, wenn jemand direkt ::allocate statt ::new aufruft und #initialize überspringt. Oder konkret: Es ist in mruby nicht möglich1, #initialize zu übergehen. Daraus ergibt sich zum einen, dass die Funktion rb_define_alloc_func() aus dem MRI schlicht überflüssig ist, zum anderen aber kann das altbekannte Data_Wrap_Struct nicht so funktionieren, wie wir es vom MRI-C-API gewohnt sind, da Data_Wrap_Struct ein neues Objekt allokiert. In mruby haben wir aber nicht die Möglichkeit, ein eigenes Objekt zu allokieren — die erste Möglichkeit, Einfluss auf ein neugeborenes Objekt zu nehmen, ist in mruby #initialize, und zu diesem Zeitpunkt ist das Objekt bereits allokiert. In mruby ist es daher Usus, das zu tun, was im MRI als Hack angesehen wird: Den gewrappten Pointer nachträglich in das Objekt stecken2.

Es folgt nun die Implementation von MyStruct#initialize über die mystruct_initialize()-Funktion, wie in mrb_define_method() weiter oben angegeben.

static mrb_value mystruct_initialize(mrb_state* p_state, mrb_value self)
{
  /* Zunächst wird eine neue „Instanz“ des Structs erzeugt. Durch die
   * Benutzung von malloc() wird verhindert, dass das Objekt am Ende
   * der Funktion aus dem Scope fällt. */
  struct mystruct* p_struct = (struct mystruct*) malloc(sizeof(struct mystruct));

  /* Ein paar Werte auf dem C-Struct setzen, sodass wir
   * später etwas Interessantes aus dem Objekt holen können */
  printf("MyStruct#initialize\n");
  p_struct->first_value = 10;
  p_struct->second_value = 20;

  /* mruby hat kein ::allocate, also müssen wir den externen Pointer
   * hier in #initialize wrappen. Das Objekt ist aber bereits allokiert
   * — daher können wir Data_Wrap_Struct() nicht verwenden, was ein neues
   * Objekt allokieren würde. Stattdessen verwenden wir die DATA_PTR()-
   * und DATA_TYPE()-Makros, mit denen man auch nach der Allokierung auf
   * die gewrappten Daten zugreifen und diese verändern kann. mrubys
   * Time.new macht es übrigens genauso. */
  DATA_PTR(self) = p_struct;
  DATA_TYPE(self) = &sp_ruby_mystruct_type_info;

  return self;
}

Typinformationen für Pointer

Der obige Codeschnippsel verwendete unkommentiert sp_ruby_mystruct_type_info. Bei dieser Variablen handelt es sich um ein spezielles Typinfo-Objekt, das mruby einfordert, um später überprüfen zu können, ob der Pointer, den man aus einem Objekt unwrappt, tatsächlich von dem Typ ist, den man erwartet hat. Ist dies nicht der Fall, d.h. gibt man beim Unwrapping eine andere Typinformation als beim Wrappen an, wirft mruby einen TypeError, was ein praktischer Sicherheitsmechanismus und obskuren Segfaults vorzuziehen ist.

Diese Typinformation wird mithilfe eines speziellen Structs übermittelt, folgender Code am Anfang der Datei außerhalb jeder Funktion, aber nach den Includes und Prototypen:

static struct mrb_data_type sp_ruby_mystruct_type_info = {"MyStruct", mystruct_free};

Das macht nichts weiter, als mruby die Existenz eines neuen „Pointer-Typen“ namens „MyStruct“ (der Name ist frei wählbar, auch „FooBarBaz“ wäre möglich gewesen, es bietet sich nur an, den Namen des gewrappten Objekts zu wählen) mitzuteilen. Darüber hinaus definiert es die free-Funktion, die mruby aufrufen soll, wenn ein Objekt, das einen solchen Pointer enthält, vom GC eingesammelt wird — auch dies ein Unterschied zum C-API des MRI. Unsere free-Funktion sieht dabei einfach so aus:

static void mystruct_free(mrb_state* p_state, void* ptr)
{
  /* ptr ist unser gewrappter Pointer */
  struct mystruct* p_struct = (struct mystruct*)ptr;

  printf("MyStruct <free> callback\n");
  free(p_struct); /* Freigeben, was wir mit malloc() allokiert haben */
}

„Normale“ Methoden

Neben dem Spezialfall #initialize — der allein dadurch speziell ist, dass diese Methode von mruby automatisch aufgerufen wird — kann man natürlich auch noch „gewöhnliche“ Methoden definieren. Dies funktioniert im Groben so, wie man es vom C-API des MRI gewöhnt ist, mit ein paar kleineren Unterschieden. So nimmt Data_Get_Struct, das Makro zum Unwrappen eines externen Pointers, wie oben angedeutet noch die erwarteten „Typinformationen“ entgegen, um bei Bedarf eine Exception werfen zu können. Abgesehen davon sollte folgender Code, der die Methode MyStruct#foo implementiert, leicht verständlich sein:

static mrb_value mystruct_foo(mrb_state* p_state, mrb_value self)
{
  struct mystruct* p_struct = NULL;

  printf("mystruct_foo() in C.\n");

  /* Wie im MRI auch benutzt man Data_Get_Struct(), um den verpackten
   * Pointer wieder aus dem Objekt herauszubekommen. In mruby ruft dieses
   * Makro einfach nur die Funktion mrb_check_datatype() auf, die Sie direkt
   * aufrufen können, wenn Sie Makros feindlich gegenüberstehen. In jedem
   * Falle wirft mruby einen TypeError, wenn die übergebene Typinformation
   * nicht mit der im Objekt gespeicherten übereinstimmt. Wenn Sie in diesem
   * Falle keine Exception wollen, sondern lieber einen NULL-Pointer, können
   * Sie stattdessen die Funktion mrb_get_datatype() verwenden. */
  Data_Get_Struct(p_state, self, &sp_ruby_mystruct_type_info, p_struct);
  printf("The first value is: %d\n", p_struct->first_value);
  printf("The second value is: %d\n", p_struct->second_value);

  /* mrb_nil_value() ist nil in Ruby (entspricht Qnil im MRI-C-API) */
  return mrb_nil_value();
}

Getter/Setter-Methoden

Etwas interessanter sind jene Methoden, die Argumente entgegennehmen und/oder einen anderen Wert als nil zurückgeben.

Zunächst der Getter MyStruct#first_value:

static mrb_value mystruct_get_first_value(mrb_state* p_state, mrb_value self)
{
  struct mystruct* p_struct = NULL;

  /* Wie gehabt */
  Data_Get_Struct(p_state, self, &sp_ruby_mystruct_type_info, p_struct);
  /* mrb_fixnum_value() konvertiert einen C-Integer in ein mruby-Fixnum. */
  return mrb_fixnum_value(p_struct->first_value);
}

Das ist so einfach wie es aussieht :-). Die verschiedenen mrb_xxx_value()-Funktionen sind in value.h deklariert.

Der Setter ist etwas interessanter, weil wir Argumentparsing machen müssen:

static mrb_value mystruct_set_first_value(mrb_state* p_state, mrb_value self)
{
  struct mystruct* p_struct = NULL;
  mrb_int val;

  /* Wir haben ein erforderliches Argument, und das
   * ist ein Integer, der in `val' landen soll. */
  mrb_get_args(p_state, "i", &val);

  /* Wert setzen */
  Data_Get_Struct(p_state, self, &sp_ruby_mystruct_type_info, p_struct);
  p_struct->first_value = val;

  /* Es gehört zum guten Ton, bei Settern das Argument
   * zurückzugeben. */
  return mrb_fixnum_value(val);
}

mrb_get_args() kann noch eine ganze Menge mehr als nur Integers extrahieren. Da die Dokumentation spärlich (aber immerhin vorhanden, siehe die Definition von mrb_get_args() in class.c) ist, ist die beste Art, den korrekten Gebrauch dieser Funktion zu erlernen, einfach den Sourcecode von mruby nach Aufrufen dieser Funktion zu durchkämmen:

$ grep -nr mrb_get_args /path/to/mruby/sourcetree

Ein Tipp dazu: Soweit mir ersichtlich war, separiert | die erforderlichen von den optionalen Parametern. Nettigkeiten wie erforderliche Parameter hinter optionalen Parametern wie im MRI sind in mruby daher augenscheinlich nicht möglich.

Abschluss

Nun noch wie schon im ersten Teil kompilieren und dann testen:

% bin/foo 
-- Initialising mruby --
-- Extending mruby --
-- Executing Ruby code --
MyStruct#initialize
mystruct_foo() in C.
The first value is: 3
The second value is: 20
-- Finishing mruby --
MyStruct <free> callback

Wunderbar :-). Den gesamten Code einschließlich mruby-Sourcecode habe ich zum Download zur Verfügung gestellt. Zum Kompilieren einfach das Archiv entpacken, in den Ordner foo/ wechseln, einen Unterordner build/ anlegen, erneut da hinein wechseln und den Build via Rake anstoßen:

$ mkdir mruby
$ cd mruby
$ wget ftp://ftp.pegasus-alpha.eu/mruby/mruby-c-struct-example.tar.xz
$ tar -xvJf mruby-c-struct-example.tar.xz
$ cd foo
$ mkdir build
$ cd build
$ rake
$ bin/foo #### Ausführen

Valete.

1 Nun gut, wir sind in Ruby. Natürlich kann man von der Ruby-Seite aus einfach undef initialize benutzen oder die Methode überschreiben. Aber es gibt tatsächlich nicht den „semi-offiziellen“ Weg über ::allocate.

2 Ja, das geht auch im MRI. Noch nicht probiert? Wäre mal ein bisschen Spielerei wert ;-)