Ich habe versucht, Ruby mit Ruby (und C) zu implementieren (ich habe mit Builtin gespielt)

Einführung

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.

Was ist eingebaut?

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 versuchte es

Ich fand heraus, dass ich eine Methode mit Ruby-Code mithilfe von Builtin implementieren konnte, also habe ich es tatsächlich versucht.

Bau der Entwicklungsumgebung

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.

Versuchen Sie, die Methode mit Builtin neu zu definieren

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.

Fix common.mk

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.

Änderung von inits.c

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.

Ändern Sie hash.c

Schließlich werden wir den Code in hash.c ändern.

Laden Sie builtin.h

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.

Löschen Sie die Definition von Hash # delete

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);

Problem, bei dem rb_hash_delete_m verfügbar war - gefixt

Ä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 hash.rbinc

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.

Hash.rb erstellen

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!

Versuche zu bauen

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.

Eigentlich mit irb versuchen

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!

Am Ende

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.

Referenz

Write a Ruby interpreter in Ruby for Ruby 3

Vollständige Erklärung des Ruby-Quellcodes

Wie Ruby funktioniert

Recommended Posts

Ich habe versucht, Ruby mit Ruby (und C) zu implementieren (ich habe mit Builtin gespielt)
Ich habe auch Web Assembly mit Nim und C ausprobiert
Lösen mit Ruby, Perl und Java AtCoder ABC 128 C.
[Ruby] Ich habe einen Crawler mit Anemone und Nokogiri gemacht.
Ich habe DI mit Ruby versucht
Ruby C Erweiterung und flüchtig
Secret Note 104 von Mathematical Girl, implementiert in Ruby und C.
Lösen mit Ruby, Perl und Java AtCoder ABC 129 C (Teil 1)
Hallo Welt mit Docker und C Sprache
Mit Java verschlüsseln und mit C # entschlüsseln
Ich habe versucht, Ruby's Float (arg, Ausnahme: true) mit Builtin neu zu implementieren
Mit Rubin ● × Game und Othello (Grundlegende Bewertung)
Verknüpfen Sie Java- und C ++ - Code mit SWIG
Ich habe mit Ruby einen riskanten Würfel gemacht
Lösen mit Ruby, Perl und Java AtCoder ABC 129 C (Teil 2) Dynamische Planungsmethode
Konvertieren Sie mit Ruby von JSON nach TSV und von TSV nach JSON
Lesen und Schreiben Sie zeilenweise aus dem Puffer mit TCP-Kommunikation zwischen C und Ruby
AtCoder Anfängerwettbewerb 169 A, B, C mit Rubin
[Ruby] Schlüsselwörter mit Schlüsselwörtern und Standardwerten von Argumenten
Versuchen Sie, Ruby und Java in Dapr zu integrieren
Ich habe mit Ruby einen Blackjack gemacht (ich habe versucht, Minitest zu verwenden)
Ich habe eine Ruby-Erweiterungsbibliothek in C erstellt
Erstellen Sie mit Docker ein Jupyter-Notizbuch und führen Sie Ruby aus
Ich habe die Anzahl der Taxis mit Ruby überprüft
Ich habe mit Ruby On Rails ein Portfolio erstellt
Rubin und Edelstein
[Ruby] Ich habe über den Unterschied zwischen each_with_index und each.with_index nachgedacht
Fühlen Sie den Grundtyp und Referenztyp leicht mit Rubin
Ich habe versucht, CSV mit Outsystems zu lesen und auszugeben
AtCoder ABC129 D 2D-Array In Ruby und Java gelöst
[Ruby] Schließen Sie bestimmte Muster aus und ersetzen Sie sie durch reguläre Ausdrücke
Ich habe MySQL 5.7 mit Docker-Compose gestartet und versucht, eine Verbindung herzustellen
Fühlen Sie den Grundtyp und Referenztyp leicht mit Rubin 2
Ich möchte Bildschirmübergänge mit Kotlin und Java machen!
Installieren Sie rbenv mit apt auf Ubuntu und setzen Sie Ruby
Ich habe versucht, C # zu kauen (Dateien lesen und schreiben)
[Tutorial] [Ruby] Erstellen und Debuggen von C-nativen Erweiterungsedelsteinen