Wie der Titel schon sagt, handelt es sich um eine Geschichte der Implementierung von Ruby selbst in Ruby (und C) unter Verwendung von Rubys integriertem Programm (entlehnt von der aufrufenden Methode, da ich den offiziellen Namen nicht kenne).
Der Inhalt richtet sich an diejenigen, die an der Implementierung von Ruby selbst interessiert sind.
Builtin ist die Implementierung von Ruby selbst in Ruby (und C) (Gibt es bisher keinen formalen Namen?). Sie können Ruby einfacher mit Ruby und C implementieren, indem Sie "_builtin <in C definierter Funktionsname" aus Ruby-Code aufrufen, wie unten gezeigt.
Zum Beispiel wird "Hash # delete" in C wie folgt implementiert:
static VALUE
rb_hash_delete_m(VALUE hash, VALUE key)
{
VALUE val;
rb_hash_modify_check(hash);
val = rb_hash_delete_entry(hash, key);
if (val != Qundef) {
return val;
}
else {
if (rb_block_given_p()) {
return rb_yield(key);
}
else {
return Qnil;
}
}
}
Das erste Argument, "Hash", empfängt den Hash selbst als Argument, und das zweite Argument, "Schlüssel", empfängt den in "Hash # delete" übergebenen Schlüssel. Werte wie Variablen auf der Ruby-Seite werden übrigens als "VALUE" -Typ empfangen und von C-Funktionen verarbeitet.
rb_hash_modify_check(hash);
Die Funktion rb_hash_modify_check
führt intern die Funktion rb_check_frozen
aus, um zu überprüfen, ob der Hash eingefroren ist.
static void
rb_hash_modify_check(VALUE hash)
{
rb_check_frozen(hash); //Überprüfen Sie, ob das Objekt eingefroren ist
}
In val = rb_hash_delete_entry (Hash, Schlüssel);
wird der zu löschende Wert basierend auf dem im Argument empfangenen Schlüssel erfasst und gleichzeitig gelöscht. Wenn mit dem Schlüssel kein Wert gepaart ist, wird der in C verwendete undefinierte Wert namens "Qundef" eingegeben.
if (val != Qundef) {
return val;
}
else {
if (rb_block_given_p()) {
return rb_yield(key);
}
else {
return Qnil;
}
}
Verzweigt den Prozess mit dem Wert "val" und gibt den gelöschten Wert zurück, wenn er nicht "Qundef" ist (dh wenn der Wert mit dem Schlüssel abgerufen und gelöscht werden kann). Wenn es "Qundef" ist, gibt es "Qnil" ("nil" in Ruby) zurück. Wenn ein Block übergeben wird, führt er "rb_yield (key)" aus und gibt das Ergebnis zurück.
Auf diese Weise wird der normalerweise verwendete Ruby mit C implementiert.
Bei Verwendung der integrierten Funktion lautet der obige Code wie folgt.
class Hash
def delete(key)
value = __builtin_rb_hash_delete_m(key)
if value.nil?
if block_given?
yield key
else
nil
end
else
value
end
end
end
static VALUE
rb_hash_delete_m(rb_execution_context_t *ec, VALUE hash, VALUE key)
{
VALUE val;
rb_hash_modify_check(hash);
val = rb_hash_delete_entry(hash, key);
if (val != Qundef) {
return val;
}
else {
return Qnil;
}
}
Weil die Ausführung von Blöcken auf der Ruby-Seite verarbeitet wird. Ich denke, die Implementierung in C ist einfacher und leichter zu lesen. Ebenfalls
Wenn Sie die integrierte Funktion auf diese Weise verwenden, können Sie Ruby mit Ruby und ein wenig C-Code implementieren.
Es scheint auch Fälle zu geben, in denen sich die Leistung verbessert, wenn sie in Ruby und nicht in C implementiert werden. Für eine genauere Geschichte sprach Herr Sasada auf der RubyKaigi 2019, bitte beziehen Sie sich darauf.
Write a Ruby interpreter in Ruby for Ruby 3
Ich fand heraus, dass ich eine Methode mit Ruby-Code mithilfe von Builtin implementieren konnte, also habe ich es tatsächlich versucht.
Ich habe zunächst eine Ruby-Entwicklungsumgebung erstellt. Ich habe WSL + Ubuntu 18.04 als meine Umgebung verwendet und eine Entwicklungsumgebung erstellt. Als grundlegendes Verfahren habe ich mich auf (2) MRI-Quellcodestruktur der Ruby Hack Challenge bezogen.
Zunächst werden wir die zu verwendenden Bibliotheken installieren.
sudo apt install git ruby autoconf bison gcc make zlib1g-dev libffi-dev libreadline-dev libgdbm-dev libssl-dev
Erstellen Sie dann ein Arbeitsverzeichnis und wechseln Sie dorthin. ..
mkdir workdir
cd workdir
Klonen Sie nach dem Wechseln in das Arbeitsverzeichnis den Ruby-Quellcode. Es braucht viel Zeit, daher ist es eine gute Idee, in dieser Zeit Kaffee hinzuzufügen.
git clone https://github.com/ruby/ruby.git
Gehen Sie nach dem Klonen des Quellcodes in das Verzeichnis "ruby" und führen Sie "autoconf" aus. Dies dient zum Generieren eines Konfigurationsskripts, das später ausgeführt wird. Nach der Ausführung kehrt es zu workdir
zurück.
cd ruby
autoconf
cd ..
Erstellen Sie dann ein Verzeichnis für den Build und wechseln Sie zu diesem.
mkdir build
cd build
Führen Sie ../ruby/configure --prefix = $ PWD / ../ install --enable-shared
aus, um ein zu erstellendes Makefile zu erstellen. Außerdem gibt --prefix = $ PWD / ../ install
an, wo Ruby installiert werden soll.
../ruby/configure --prefix=$PWD/../install --enable-shared
Führen Sie dann make -j
aus, um zu erstellen. -j
ist eine Option, um die Kompilierung parallel durchzuführen. Wenn Sie es nicht eilig haben, ist nur "make" in Ordnung.
make -j
Wenn Sie schließlich "make install" ausführen, wird ein "install" -Verzeichnis im Verzeichnis "workdir" erstellt und Ruby installiert.
make install
Der neueste Ruby ist jetzt in workdir / install
installiert.
Übrigens, wenn Sie sich fragen, ob es wirklich installiert ist, versuchen Sie, ../install/bin/ruby -v
auszuführen. Wenn Sie "Ruby 2.8.0 Dev" und die Ruby-Version sehen, ist Ruby korrekt installiert.
Nachdem die Entwicklungsumgebung eingerichtet ist, werden wir die Methoden mithilfe von Builtin neu definieren. Wir werden den im vorherigen Beispiel erwähnten "Hash # delete" erneut implementieren.
Fügen Sie zunächst "common.mk" verschiedene Einstellungen hinzu, um den Ruby-Quellcode beim Erstellen zu verwenden.
Es gibt eine Beschreibung von "BUILTIN_RB_SRCS" um die 1000. Zeile von "common.mk". Fügen Sie eine Datei hinzu, die den Ruby-Code enthält, der von diesem BUILTIN_RB_SRCS
gelesen werden soll.
common.mk
BUILTIN_RB_SRCS = \
$(srcdir)/ast.rb \
$(srcdir)/gc.rb \
$(srcdir)/io.rb \
$(srcdir)/pack.rb \
$(srcdir)/trace_point.rb \
$(srcdir)/warning.rb \
$(srcdir)/array.rb \
$(srcdir)/prelude.rb \
$(srcdir)/gem_prelude.rb \
$(empty)
BUILTIN_RB_INCS = $(BUILTIN_RB_SRCS:.rb=.rbinc)
Fügen Sie dieses Mal "hash.rb" wie folgt hinzu, um Hash zu implementieren.
BUILTIN_RB_SRCS = \
$(srcdir)/ast.rb \
$(srcdir)/gc.rb \
$(srcdir)/io.rb \
$(srcdir)/pack.rb \
$(srcdir)/trace_point.rb \
$(srcdir)/warning.rb \
$(srcdir)/array.rb \
$(srcdir)/prelude.rb \
$(srcdir)/gem_prelude.rb \
+ $(srcdir)/hash.rb \
$(empty)
BUILTIN_RB_INCS = $(BUILTIN_RB_SRCS:.rb=.rbinc)
Ändern Sie als Nächstes den Teil, der die zu lesende Datei im Hash-Build um Zeile 2520 angibt. Auf diese Weise wird die zu lesende Datei wie "hash.c" angegeben.
common.mk
hash.$(OBJEXT): {$(VPATH)}hash.c
hash.$(OBJEXT): {$(VPATH)}id.h
hash.$(OBJEXT): {$(VPATH)}id_table.h
hash.$(OBJEXT): {$(VPATH)}intern.h
hash.$(OBJEXT): {$(VPATH)}internal.h
hash.$(OBJEXT): {$(VPATH)}missing.h
Fügen Sie hier "hash.rbinc" und "builtin.h" hinzu.
hash.$(OBJEXT): {$(VPATH)}hash.c
+hash.$(OBJEXT): {$(VPATH)}hash.rbinc
+hash.$(OBJEXT): {$(VPATH)}builtin.h
hash.$(OBJEXT): {$(VPATH)}id.h
hash.$(OBJEXT): {$(VPATH)}id_table.h
hash.$(OBJEXT): {$(VPATH)}intern.h
hash.$(OBJEXT): {$(VPATH)}internal.h
hash.$(OBJEXT): {$(VPATH)}missing.h
hash.rbinc
ist eine Datei, die automatisch generiert wird, wenn make
ausgeführt wird, und basierend auf dem Inhalt von__builtin_ <aufrufender C-Funktionsname>
in hash.rb
generiert wird. Außerdem ist builtin.h
eine Header-Datei mit Implementierungen für die Verwendung von Builtin.
Damit ist die Änderung in "common.mk" abgeschlossen.
Dann ändern Sie inits.c
. Es ist jedoch sehr einfach zu beheben.
inits.c
#define BUILTIN(n) CALL(builtin_##n)
BUILTIN(gc);
BUILTIN(io);
BUILTIN(ast);
BUILTIN(trace_point);
BUILTIN(pack);
BUILTIN(warning);
BUILTIN(array);
Init_builtin_prelude();
}
inits.c
fügt die Ruby-Quelldatei hinzu, die wie oben beschrieben eingebaut ist. Fügen Sie hier auf die gleiche Weise BUILTIN (Hash);
hinzu.
#define BUILTIN(n) CALL(builtin_##n)
BUILTIN(gc);
BUILTIN(io);
BUILTIN(ast);
BUILTIN(trace_point);
BUILTIN(pack);
BUILTIN(warning);
BUILTIN(array);
+ BUILTIN(hash);
Init_builtin_prelude();
Dies ist in Ordnung, um "inits.c" zu ändern.
Schließlich werden wir den Code in hash.c
ändern.
Fügen Sie zuerst "#include" builtin.h "" zum Header-Leseteil um die 40. Zeile hinzu.
#include "ruby/st.h"
#include "ruby/util.h"
#include "ruby_assert.h"
#include "symbol.h"
#include "transient_heap.h"
+ #include "builtin.h"
Jetzt können Sie die Strukturen usw. verwenden, die für die Integration von "hash.c" erforderlich sind.
Entfernen Sie als Nächstes den Teil, der "Hash # delete" definiert.
Ich denke, eine Funktion namens "Init_Hash (void)" ist am Ende von "hash.c" definiert.
void
Init_Hash(void)
{
///Der Implementierungscode von Hash usw. wird geschrieben.
}
Die Methoden jeder Klasse in Ruby werden in dieser Funktion wie folgt definiert.
rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);
Stellen Sie sich rb_define_method
als eine Methodendefinition in Ruby vor. Übergeben Sie den VALUE
der Klasse, die die Methode als erstes Argument definiert, und das zweite Argument ist der Methodenname.
Das dritte Argument ist die in C definierte Funktion (der von der Methode ausgeführte Prozess), und das vierte Argument ist die Anzahl der von der Methode empfangenen Argumente.
Wenn Sie eine Ruby-Methode mit integrierter Funktion definieren möchten, müssen Sie diesen Definitionsteil entfernen. Dieses Mal werden wir "Hash # delete" erneut implementieren. Löschen Sie also den Teil, in dem "delete" definiert ist.
rb_define_method(rb_cHash, "shift", rb_hash_shift, 0);
- rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);
rb_define_method(rb_cHash, "delete_if", rb_hash_delete_if, 0);
Ändern Sie das von rb_define_method aufgerufene rb_hash_delete_m
(rb_cHash," delete ", rb_hash_delete_m, 1);` das Sie zuvor gelöscht haben, damit es im eingebauten verwendet werden kann.
Es gibt eine Implementierung von rb_hash_delete_m
um Zeile 2380.
static VALUE
rb_hash_delete_m(VALUE hash, VALUE key)
{
VALUE val;
rb_hash_modify_check(hash);
val = rb_hash_delete_entry(hash, key);
if (val != Qundef) {
return val;
}
else {
if (rb_block_given_p()) {
return rb_yield(key);
}
else {
return Qnil;
}
}
}
Ändern Sie dies wie folgt.
static VALUE
rb_hash_delete_m(rb_execution_context_t *ec, VALUE hash, VALUE key)
{
VALUE val;
rb_hash_modify_check(hash);
val = rb_hash_delete_entry(hash, key);
if (val != Qundef)
{
return val;
}
else
{
return Qnil;
}
}
Der Implementierungspunkt ist, dass rb_execution_context_t * ec
als erstes geliehenes Argument zur Unterstützung von Builtin übergeben wird.
Jetzt können Sie die in C definierten Funktionen von Ruby aus aufrufen.
Laden Sie abschließend die automatisch generierte Datei "hash.rbinc". Fügen Sie "#include" hash.rbinc "" am Ende von "hash.c" hinzu.
#include "hash.rbinc"
Damit ist die Änderung auf der C-Codeseite abgeschlossen.
Lassen Sie uns nun "Hash # delete" in Ruby implementieren. Erstellen Sie "hash.rb" in derselben Hierarchie wie "hash.c". Fügen Sie nach dem Erstellen den folgenden Code hinzu.
class Hash
def delete(key)
puts "impl by Ruby(& C)!"
value = __builtin_rb_hash_delete_m(key)
if value.nil?
if block_given?
yield key
else
nil
end
else
value
end
end
end
Das empfangene Argument wird an "__builtin_rb_hash_delete_m" übergeben, das von builtin früher aufgerufen werden kann, und das Ergebnis wird "value" zugewiesen.
Danach ist der Wert von "Wert" "Null" oder der Prozess wird im selben Abschnitt verzweigt. Im Fall von "Null" Wenn ein Lichtblock übergeben wird, wird der Block mit "Schlüssel" als Argument ausgeführt.
setzt" impl von Ruby (& C)! "
Ist eine Nachricht, die überprüft werden muss, wann Sie es tatsächlich versuchen.
Damit ist die integrierte Implementierung abgeschlossen!
Erstellen wir es auf die gleiche Weise wie beim Erstellen der Entwicklungsumgebung.
make -j && make install
Wenn der Build erfolgreich ist, ist es OK! Wenn der Build fehlschlägt, prüfen Sie, ob Tippfehler usw. vorliegen.
Lassen Sie uns versuchen, "Hash # delete" in "irb" zu implementieren!
../install/bin/irb
Fügen wir nun den folgenden Code ein!
hash = {:key => "value"}
hash.delete(:k)
hash.delete(:key)
Wenn das Ergebnis wie folgt angezeigt wird, ist die Implementierung mit eingebautem Ergebnis abgeschlossen!
irb(main):001:0> hash = {:key => "value"}
irb(main):002:0> hash.delete(:k)
impl by Ruby(& C)!
=> nil
irb(main):003:0> hash.delete(:key)
impl by Ruby(& C)!
=> "value"
irb(main):004:0>
Da es von Ruby (& C) als "impl" angezeigt wird, können Sie sehen, dass das in Ruby definierte "Hash # delete" ausgeführt wird.
Sie haben Ruby jetzt in Ruby (und C) implementiert!
Wenn Sie Built auf diese Weise verwenden, können Sie Ruby selbst mithilfe von Ruby und (ein wenig C) Code implementieren. Daher denke ich, dass selbst Leute, die normalerweise Ruby schreiben, problemlos Patches wie Methodenänderungen senden können.
Ich bin froh, dass es überraschend einfach zu schreiben ist, da ich den Prozess nach dem Ausprobieren auf der Ruby-Seite schreiben kann.
Persönlich denke ich, dass es einfacher sein wird, Ruby-Erweiterungen in C / C ++ zu schreiben, wenn sie in Erweiterungen usw. verwendet werden können. Daher freue ich mich sehr auf die Zukunftsaussichten.
Write a Ruby interpreter in Ruby for Ruby 3
Vollständige Erklärung des Ruby-Quellcodes
Recommended Posts