Eine Geschichte, die selbst ein Mann, der die C-Sprache nicht versteht, Ruby 2.6 um neue Funktionen erweitern könnte

Dieser Artikel ist der 9. Tag von ISer Adventskalender 2018.

Einführung

Ruby ist natürlich eine bekannte Programmiersprache. Es ist auf der ganzen Welt weit verbreitet und konzentriert sich auf Ruby on Rais, ein Webanwendungsframework. Normalerweise arbeite ich Teilzeit mit Ruby. Ich liebe Ruby, die Sprache, die ich bei der Arbeit benutze, und die Sprache, die mich dazu gebracht hat, Programme zu schreiben. Ich wollte schon immer irgendwie zu Ruby beitragen. Die naheliegendste Form ist, eigenen Code zu schreiben, um Ruby zu verbessern. Aber natürlich benötigen Sie eine andere Sprache als Ruby, um Ruby zu erstellen. Tatsächlich ist ein Großteil des Ruby-Quellcodes in C geschrieben. Außerhalb von Ruby hatte ich keine praktischen Erfahrungen und ich hatte das Gefühl, dass die Schwelle von C so hoch war, dass ich meinen Wunsch, Ruby zu verbessern, nicht in die Tat umsetzen konnte. Als ich neulich aus irgendeinem Grund einen Ruby-Patch schrieb und ihn verschickte, konnte ich ihn in Ruby integrieren. Ich werde diesen Artikel schreiben, damit Sie die Schwelle für eine Beteiligung an der Ruby-Entwicklung so niedrig wie möglich fühlen.

Auslösen

Während der Sommerferien fand ich eine Veranstaltung namens Cookpad Ruby Hack Challenge. Cookpad Ruby Hack Challenge # 5 [zwei Tage lang] Auf die Frage hin war es eine Veranstaltung "Lassen Sie uns gemeinsam einen Ruby-Interpreter entwickeln!" Ein langjähriger Wunsch kann hier erfüllt werden. Mit dieser Erwartung entschied ich mich zur Teilnahme.

Tatsächlich habe ich mich immer über die Ruby-Spezifikation gewundert. Die "Hash" -Klasse hat eine Methode namens "Merge", die die beiden Hashes zusammenführt.

hash1 = {a: 1, b: 2}
hash2 = {c: 3, d: 4}

hash1.merge(hash2)
# => {a: 1, b: 2, c: 3, d: 4}

Diese Methode kann jedoch nur ein Argument annehmen, sodass Sie nicht drei oder mehr Hashes gleichzeitig kombinieren können.

hash3 = {e: 5, f: 6}

hash1.merge(hash2, hash3)
# => ArgumentError (wrong number of arguments (given 2, expected 1))

Dies ist etwas unpraktisch. Es ist eine großartige Gelegenheit, also habe ich beschlossen, mein Bestes zu geben, um dies zu ändern.

Funktionsimplementierung

Zunächst werde ich die Pull-Anfrage veröffentlichen, die ich tatsächlich auf GitHub gestellt habe. Make the number of arguments of Hash#merge variable

Von hier aus werde ich schreiben, wie die Entwicklung verlaufen ist.

Um Ruby zu entwickeln, habe ich zunächst verschiedene Quellcodes heruntergeladen und die Entwicklungsumgebung vorbereitet. Dieser Bereich wird auf der folgenden Seite ausführlich zusammengefasst, die auch als Referenzmaterial für die Ruby Hack Challenge dient. Daher werde ich die Details weglassen.

ko1/rubyhackchallenge

Wie auch immer, wenn das Ruby-Repository mit der folgenden Verzeichnisstruktur in "workdir / ruby" geklont wird, denke ich, dass es für die erste Vorbereitung ausreicht.

workdir/
 ├ ruby/
 ├ build/
 └ install/

Von hier aus bearbeiten wir den Ruby-Quellcode workdir / ruby. Dieses Mal werden wir "ruby / hash.c" bearbeiten, eine Sammlung von Code, der sich auf die "Hash" -Klasse bezieht.

ruby/hash.c ↑ Bevor ich es bearbeitet habe, war es wie oben. Die Anzahl der Zeilen beträgt 4857. lange. Auf den ersten Blick bricht mein Herz. Aber nur ein Teil davon wird bearbeitet. Mach dir keine Sorgen. Gehen wir Schritt für Schritt. Am Ende des Codes sehen Sie, dass eine Reihe von Funktionen mit dem Namen "rb_define_method" aufgerufen werden.

hash.c


    rb_define_method(rb_cHash, "initialize", rb_hash_initialize, -1);
    rb_define_method(rb_cHash, "initialize_copy", rb_hash_initialize_copy, 1);
    rb_define_method(rb_cHash, "rehash", rb_hash_rehash, 0);

    rb_define_method(rb_cHash, "to_hash", rb_hash_to_hash, 0);
    rb_define_method(rb_cHash, "to_h", rb_hash_to_h, 0);
    rb_define_method(rb_cHash, "to_a", rb_hash_to_a, 0);
//・
//・
//・

Hier ist die Verarbeitung der im zweiten Argument übergebenen Ruby-Methode der im dritten Argument übergebenen C-Funktion zugeordnet. Als ich vorerst nach "Zusammenführen" suchte, war es ganz unten.

hash.c


    rb_define_method(rb_cHash, "merge!", rb_hash_update, 1);

    rb_define_method(rb_cHash, "merge", rb_hash_merge, 1);

merge! Ist eine destruktive Methode von merge (eine Methode, die den Empfänger selbst modifiziert), daher muss diese Implementierung zusammen mit merge geändert werden. Aus der Beschreibung von "rb_define_method" können wir erkennen, dass wir Änderungen an den beiden C-Funktionen "rb_hash_update" und "rb_hash_merge" vornehmen müssen, um dieses Ziel zu erreichen. Vorher sollten Sie sich das letzte Argument von rb_define_method genau ansehen. Dies zeigt die Anzahl der Argumente, die eine Ruby-Methode annehmen kann. Für variable Länge ist es -1. Da es sich bei dieser Änderung um eine Implementierung handelt, bei der die Anzahl der Variablen, die verwendet werden können, variabel ist, ist es natürlich erforderlich, diese beiden auf -1 zu ändern.

hash.c


    rb_define_method(rb_cHash, "merge!", rb_hash_update, -1);

    rb_define_method(rb_cHash, "merge", rb_hash_merge, -1);

Schauen wir uns von hier aus die tatsächliche Verarbeitung von "Hash # merge" an. Schauen wir uns zuerst rb_hash_merge an.

hash.c


static VALUE
rb_hash_merge(VALUE hash1, VALUE hash2)
{
    return rb_hash_update(rb_hash_dup(hash1), hash2);
}

Der hier geschriebene VALUE ist die Darstellung eines Ruby-Objekts im C-Code. Von hier aus scheint diese Funktion eine Funktion zu sein, die ein Ruby-Objekt zurückgibt. Wenn ich den Code irgendwie lese, habe ich das Gefühl, dass ich ein Duplikat von "hash1" zurückgebe und es mit "rb_hash_update" multipliziere (die C-Funktion, die Rubys "merge!" Entspricht). Ich möchte dieser Funktion vorerst erlauben, Argumente mit variabler Länge zu verwenden, aber ich habe keine Ahnung, was ich tun soll. Daher habe ich mich vorerst entschlossen, eine Methode zu implementieren, die andere Argumente variabler Länge verwendet.

hash.c


static VALUE
rb_hash_flatten(int argc, VALUE *argv, VALUE hash)
{
//・
//・
//・
}

Es scheint, dass Argumente variabler Länge realisiert werden können, indem der Empfänger selbst als letztes Argument "Hash", die Anzahl der Argumente als erstes "Argument" und das Array von Argumenten als zweites "Argument" empfangen wird. Geben Sie vorerst das Argument "rb_hash_merge" in diesem Format an, damit auch der interne Aufruf von "rb_hash_update" dieser Implementierung folgt.

hash.c


static VALUE
rb_hash_merge(int argc, VALUE *argv, VALUE self)
{
    return rb_hash_update(argc, argv, rb_hash_dup(self));
}

Alles was Sie tun müssen, ist rb_hash_update zu ändern.

hash.c


static VALUE
rb_hash_update(VALUE hash1, VALUE hash2)
{
    rb_hash_modify(hash1);
    hash2 = to_hash(hash2);
    if (rb_block_given_p()) {
	rb_hash_foreach(hash2, rb_hash_update_block_i, hash1);
    }
    else {
	rb_hash_foreach(hash2, rb_hash_update_i, hash1);
    }
    return hash1;
}

Es sieht so aus, als ob der empfängerseitige Hash der Methode und der zusammengeführte Hash unterschiedliche Aktionen ausführen, je nachdem, ob der Block an die Methode übergeben wird oder nicht. Diese Änderung ermöglicht nur, dass der ursprüngliche "Zusammenführungs" -Prozess mehrmals gleichzeitig ausgeführt wird. Es erscheint daher sinnvoll, diesen Prozess mehrmals mit der for-Anweisung aufzurufen.

Ich habe überprüft, wie ein C für Anweisung geschrieben wird, und es hinzugefügt.

hash.c


static VALUE
rb_hash_update(int argc, VALUE *argv, VALUE self)
{
    rb_hash_modify(self);
    for(int i = 0; i < argc; i++){
      VALUE hash = to_hash(argv[i]);
      if (rb_block_given_p()) {
    rb_hash_foreach(hash, rb_hash_update_block_i, self);
      }
      else {
    rb_hash_foreach(hash, rb_hash_update_i, self);
      }
    }
    return self;
}

Ich habe gerade eine for-Anweisung hinzugefügt, und es sollte funktionieren.

Testen und optimieren

Erstellen Sie "workdir / ruby / test.rb" und schreiben Sie ein Ruby-Skript, das die Funktionen verwendet, die Sie dieses Mal implementiert haben.

test.rb


hash1 = {a: 1, b: 2}
hash2 = {c: 3, d: 4}
hash3 = {e: 5, f: 6}

puts hash1.merge(hash2, hash3)

Wenn die Umgebung gemäß dem zu Beginn von "Implementierung von Funktionen" genannten Material eingerichtet ist, können Sie den Befehl "make run" in "workdir / build" eingeben. Das Ruby-Skript lautet __Der Ruby, den Sie gerade bearbeitet haben. Wird in __ ausgeführt. (Einige Funktionen sind eingeschränkt, z. B. kann die Erweiterungsbibliothek nicht verwendet werden.)

$ make run
compiling ../ruby/hash.c
linking miniruby
../ruby/tool/ifchange "--timestamp=.rbconfig.time" rbconfig.rb rbconfig.tmp
rbconfig.rb unchanged
creating verconf.h
verconf.h updated
compiling ../ruby/loadpath.c
linking static-library libruby.2.6-static.a
linking shared-library libruby.2.6.dylib
linking ruby
./miniruby -I../ruby/lib -I. -I.ext/common  ../ruby/tool/runruby.rb --extout=.ext  -- --disable-gems ../ruby/test.rb
{a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}

Die Ausgabe ist wie vorgesehen eine Kombination der drei Hashes! Die Implementierung scheint vorerst erfolgreich zu sein.

Von hier aus schreibe ich Testcode, um zu bestätigen, dass diese Funktion ordnungsgemäß funktioniert. Es gibt einen Testcode für "hash.c" in "workdir / ruby / test / ruby / test_hash.rb". Bearbeiten Sie ihn also. Es ist im Format "Minitest" geschrieben, einem wichtigen Testframework in Ruby. Daher sollte es für diejenigen, die es gewohnt sind, in Ruby zu entwickeln, nicht so schwierig sein.

test_hash.rb


  def test_merge
    h1 = @cls[1=>2, 3=>4]
    h2 = {1=>3, 5=>7}
    h3 = {1=>1, 2=>4}
    assert_equal({1=>3, 3=>4, 5=>7}, h1.merge(h2))
    assert_equal({1=>6, 3=>4, 5=>7}, h1.merge(h2) {|k, v1, v2| k + v1 + v2 })
    assert_equal({1=>1, 2=>4, 3=>4, 5=>7}, h1.merge(h2, h3))
    assert_equal({1=>8, 2=>4, 3=>4, 5=>7}, h1.merge(h2, h3) {|k, v1, v2| k + v1 + v2 })
  end

Im obigen Testcode haben wir die Funktionsweise dieser Methode auf vier Arten bestätigt: Wenn es ein oder zwei Argumente für "Hash # merge" gibt, wenn ein Block übergeben wird und wenn er nicht übergeben wird. Übrigens können Sie mit @ cls die Funktionsweise sowohl des Hash-Objekts als auch seiner untergeordneten Klassenobjekte überprüfen.

Lassen Sie uns den Test ausführen. Sie können den gesamten Testcode ausführen, indem Sie den Befehl make test-all in workdir / build ausführen.

$ make test-all
・
・
・
optparse.rb:810:in `update':can't modify frozen -702099568038204768 (FrozenError)
make: *** [verconf.h] Error 1

Das? Ich habe einen Fehler bekommen. Bei näherer Betrachtung wird die Methode "update" auch "rb_hash_update" genannt.

rb_define_method(rb_cHash, "update", rb_hash_update, 1);

Da das letzte Argument 1 ist, wird beim Aufruf von "update" "rb_hash_update" aufgerufen, als ob es nur ein Argument gäbe. Da ich jedoch das Argument in der Definition von "rb_hash_update" bereits in die Variable mit der Länge eins geändert habe, wird bei jedem Aufruf von "update" eine Fehlermeldung angezeigt. Diese Methode wurde von der Bibliothek "optparse" verwendet, die Befehlszeilenargumente verwaltet. Daher scheint es, dass der Befehl nicht richtig aufgerufen werden konnte und ein Fehler aufgetreten ist. Ändern Sie die Zeile von rb_define_method so, dass sie Argumente mit variabler Länge annehmen kann, selbst wenn sie von update aufgerufen wird.

rb_define_method(rb_cHash, "update", rb_hash_update, -1);

Fügen Sie auch minitest für update hinzu.

test_hash.rb


  def test_update2
    h1 = @cls[1=>2, 3=>4]
    h2 = {1=>3, 5=>7}
    h3 = {1=>1, 2=>4}
    h1.update(h2, h3) {|k, v1, v2| k + v1 + v2 }
    assert_equal({1=>8, 2=>4, 3=>4, 5=>7}, h1)
  end
$ make test-all
・
・
・
Finished tests in 979.934312s, 19.6534 tests/s, 2362.7614 assertions/s.
19259 tests, 2315351 assertions, 0 failures, 0 errors, 51 skips

Der Test ist bestanden.

Bis zum Zusammenführen

Von nun an werde ich darüber schreiben, wie die oben genannten Änderungen in das Repository von Ruby selbst übernommen werden. Ich werde den Ablauf erklären, bis der Ruby-Patch vorerst importiert wird, und das folgende Dokument erneut zitieren. ko1/rubyhackchallenge Ruby wird von Subversion versioniert, nicht von Git. Daher ist es nicht möglich, die Änderungen einfach durch Senden einer Pull-Anfrage an GitHub zu übernehmen. Diskussionen über Ruby finden täglich auf Redmine, dem OSS für das Projektmanagement, statt. Wenn Sie also Änderungen vornehmen möchten, geben Sie zuerst die Änderungen bekannt, die Sie vornehmen möchten (dies wird als Ticket bezeichnet). .. Ruby Issue Tracking System Tickets sind in "Feature Request" und "Bug Report" unterteilt. Diese Änderung wird als erstere kategorisiert. In diesem Fall sollte das Ticket den folgenden Inhalt enthalten.

--Abstrakt (kurze Zusammenfassung der Vorschläge)

Vor allem in Bezug auf die Funktionsanforderung ist es wichtig, dass die Person, die die Funktion tatsächlich verwendet, tatsächlich verwendet wird. Wenn Sie also einen bestimmten Anwendungsfall usw. im Abschnitt Hintergrund schreiben, scheint sich die Möglichkeit einer Einbeziehung zu erhöhen. Wenn Ihnen dies gefällt, werden Personen mit Commit-Berechtigungen für Subversion (ganz zu schweigen vom Ruby Committer!) Die Änderungen in Rubys Subversion übernehmen.

Dies ist das Ticket, das ich dieses Mal gemacht habe. Make the number of arguments of Hash#merge variable Für die Implementierung habe ich eine separate GitHub-Pull-Anfrage gestellt und den Link angehängt. Für den Hintergrund gibt es Beispiele für Stapelüberlauf und Qiita, bei denen Sie nervigen Code schreiben müssen, da drei Hashes nicht mit "Hash # merge" zusammengeführt werden können. Ich habe danach gesucht und eingefügt.

Bisher ist die Ruby Hack Challenge vorbei. Schließlich hatte ich die Gelegenheit, meine Implementierung vor allen Mitarbeitern von Ruby Committer bekannt zu geben, und ich erhielt eine positive Antwort von allen, und es wurde beschlossen, diesen Code sofort in das Ruby-Repository aufzunehmen. Ich tat. Ich war damals wirklich glücklich ...

Danach habe ich auf die GitHub-Pull-Anfrage die Teile geändert, auf die verschiedene Personen hingewiesen haben, z. B. falsche Notation, Einrückung und unvollständige Dokumentation. (Manchmal haben Sie die Änderungen selbst vorgenommen ... Danke)

Wiederholen Sie das für eine Woche ... endlich ... スクリーンショット 2018-12-04 17.53.57.png Der Code, den ich geschrieben habe, wurde in Ruby! Ich war wirklich glücklich.

Schließlich

Dieses Mal war ich wirklich froh, durch die Kombination verschiedener Dinge zu meinem Lieblings-Ruby beitragen zu können. Vielen Dank an die Leute, die an der Ausrichtung der Ruby Hack Challenge beteiligt waren, an die Ruby Commiters, die sie angenommen haben, und an die verschiedenen Leute, die uns die Code-Bewertungen gegeben haben. Ich bin dir wirklich dankbar. Ich hoffe, dieser Artikel verringert die Hürden für viele Menschen, um zu Ruby beizutragen. Sie dort, die noch schüchtern sind. Ich denke, Ruby ist die einzige wichtige Sprache in Japan, die eine so starke Community von Kernentwicklern hat. Es ist eine Verschwendung, davon keinen Gebrauch zu machen!

Recommended Posts

Eine Geschichte, die selbst ein Mann, der die C-Sprache nicht versteht, Ruby 2.6 um neue Funktionen erweitern könnte
Nachdem ich das Montyhall-Problem mit Ruby überprüft hatte, war es eine Geschichte, die ich gut verstehen konnte und die ich nicht gut verstand
MockMVC gibt 200 zurück, auch wenn ich eine Anfrage an einen Pfad stelle, der nicht existiert
[JQuery] Ein Typ, den selbst Anfänger gut verstehen konnten
Wenn in Ruby Hash [: a] [: b] [: c] = 0 ist, möchten wir, dass Sie rekursiv erweitern, auch wenn der Schlüssel nicht vorhanden ist
Entspricht "Fehler, dass die Basisauthentifizierung nicht bestanden wird" im Testcode "Die Geschichte, die nicht gemacht werden konnte".
Dateiübertragung in eine virtuelle Umgebung, die auch nach einem Tag nicht gelöst werden konnte: Memorandum
So interagieren Sie mit einem Server, der die App nicht zum Absturz bringt
Eine Geschichte, die unter einem Raum litt, der nicht verschwindet, selbst wenn er mit Java beschnitten ist