Une histoire que même un homme qui ne comprend pas le langage C pourrait ajouter de nouvelles fonctions à Ruby 2.6

Cet article est le 9ème jour du Calendrier de l'Avent ISer 2018.

introduction

Inutile de dire que Ruby est un langage de programmation bien connu. Il est largement utilisé dans le monde entier, centré sur Ruby on Rais, qui est un framework d'application Web. Je travaille généralement à temps partiel avec Ruby. J'adore Ruby, la langue que j'utilise au travail et la langue qui m'a fait aimer écrire des programmes. J'ai toujours voulu contribuer à Ruby d'une manière ou d'une autre. La forme la plus évidente est d'écrire votre propre code pour améliorer Ruby. Mais, bien sûr, vous avez besoin d'un langage autre que Ruby pour créer Ruby. En fait, une grande partie du code source de Ruby est écrite en C. Je n'avais aucune expérience pratique en dehors de Ruby et je sentais que le seuil de C était si élevé que je ne pouvais pas mettre en œuvre mon désir d'améliorer Ruby. Cependant, l'autre jour, j'ai écrit un patch Ruby pour une raison quelconque et je l'ai envoyé, et j'ai pu l'introduire dans Ruby. J'écrirai cet article afin que vous puissiez sentir le seuil pour être impliqué dans le développement de Ruby aussi bas que possible.

Déclencheur

Pendant les vacances d'été, j'ai trouvé un événement appelé Cookpad Ruby Hack Challenge. Cookpad Ruby Hack Challenge # 5 [tenu pendant deux jours] Lorsqu'on lui a demandé, c'était un événement "Développons ensemble un interpréteur Ruby!" Un désir de longue date peut être satisfait ici. Avec cette attente, j'ai décidé de participer.

En fait, je me suis toujours posé des questions sur la spécification Ruby. La classe Hash a une méthode appelée merge qui fusionne les deux hachages.

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

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

Cependant, cette méthode ne peut prendre qu'un seul argument, vous ne pouvez donc pas combiner trois hachages ou plus en même temps.

hash3 = {e: 5, f: 6}

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

C'est un peu gênant. C'est une belle opportunité, j'ai donc décidé de faire de mon mieux pour changer cela.

Implémentation des fonctionnalités

Tout d'abord, je publierai la pull request que j'ai réellement faite sur GitHub. Make the number of arguments of Hash#merge variable

De là, j'écrirai comment le développement s'est déroulé.

Tout d'abord, pour développer Ruby, j'ai téléchargé divers codes sources et préparé l'environnement de développement. Cette zone est résumée en détail sur la page suivante, qui est également un matériel de référence pour le Ruby Hack Challenge, donc je vais omettre les détails.

ko1/rubyhackchallenge

Quoi qu'il en soit, si le référentiel ruby est cloné dans workdir / ruby avec la structure de répertoire suivante, je pense que c'est suffisant pour la première préparation.

workdir/
 ├ ruby/
 ├ build/
 └ install/

De là, nous éditerons le code source de Ruby workdir / ruby. Cette fois, nous éditerons ruby / hash.c, qui est une collection de code lié à la classe Hash.

ruby/hash.c ↑ Avant de l'éditer, c'était comme ci-dessus. Le nombre de lignes est de 4857. longue. À première vue, mon cœur est sur le point de se briser. Mais seule une partie est éditée. Ne t'inquiète pas. Allons-y étape par étape. Au bas du code, vous pouvez voir qu'un certain nombre de fonctions appelées rb_define_method sont appelées.

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

Ici, le traitement de la méthode Ruby passée dans le deuxième argument est associé à la fonction C passée dans le troisième argument. Pour le moment, quand j'ai cherché «fusionner» à partir de ceci, c'était en bas.

hash.c


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

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

merge! Est une méthode destructive de merge (une méthode qui modifie le récepteur lui-même), donc cette implémentation doit être modifiée avec merge. À partir de la description de rb_define_method, nous pouvons voir que nous devons apporter des modifications aux deux fonctions C, rb_hash_update et rb_hash_merge, afin d'atteindre cet objectif. Avant cela, vous devriez regarder attentivement le dernier argument de rb_define_method. Cela montre le nombre d'arguments qu'une méthode Ruby peut prendre. Pour une longueur variable, il vaut -1. Puisque ce changement est une implémentation qui rend le nombre de variables pouvant être prises de longueur variable, il est naturellement nécessaire de changer ces deux en -1.

hash.c


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

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

À partir de là, jetons un coup d'œil au traitement réel de Hash # merge. Regardons d'abord rb_hash_merge.

hash.c


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

La «VALUE» écrite ici est la représentation d'un objet Ruby dans le code C. À partir de là, cette fonction semble être une fonction qui renvoie un objet Ruby. Pour autant que je lis le code d'une manière ou d'une autre, j'ai l'impression de renvoyer un double de «hash1» et de le multiplier par «rb_hash_update» (la fonction C qui correspond à «merge!» De Ruby). J'aimerais autoriser cette fonction à prendre des arguments de longueur variable pour le moment, mais je n'ai aucune idée de ce qu'il faut faire. Donc, pour le moment, j'ai décidé d'essayer d'implémenter une méthode qui prend d'autres arguments de longueur variable.

hash.c


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

Il semble que des arguments de longueur variable peuvent être réalisés en recevant le récepteur lui-même comme dernier argument «hash», le nombre d'arguments comme premier «argc» et le tableau d'arguments comme second «argv». Pour le moment, faites l'argument de rb_hash_merge dans ce format afin que l'appel interne à rb_hash_update suive également cette implémentation.

hash.c


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

Tout ce que vous avez à faire est de modifier rb_hash_update.

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

Il semble que le hachage côté récepteur de la méthode et le hachage fusionné font des choses différentes, selon que le bloc est passé à la méthode ou non. Ce changement ne permet que le processus original de «fusion» d'être exécuté plusieurs fois à la fois, il semble donc bon d'appeler ce processus plusieurs fois avec l'instruction for.

J'ai vérifié comment écrire une instruction C for et l'ai ajoutée.

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

Je viens d'ajouter une instruction for, et cela devrait fonctionner.

Test et peaufinage

Créez workdir / ruby / test.rb et écrivez un script Ruby qui utilise les fonctionnalités que vous avez implémentées cette fois.

test.rb


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

puts hash1.merge(hash2, hash3)

Si l'environnement est configuré selon le matériel répertorié au début de "Implémentation des fonctions", vous pouvez taper la commande make run dans workdir / build et le script Ruby sera __Le Ruby que vous venez de modifier Sera exécuté dans __. (Certaines fonctions sont limitées, telles que la bibliothèque d'extension ne peut pas être utilisée)

$ 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}

Comme prévu, la sortie est une combinaison des trois hachages! Pour l'instant, la mise en œuvre semble réussie.

À partir de là, j'écrirai un code de test pour confirmer que cette fonction fonctionne correctement. Il y a un code de test pour hash.c dans workdir / ruby / test / ruby / test_hash.rb, alors modifiez-le. Il est écrit au format minitest, qui est un framework de test majeur dans Ruby, donc cela ne devrait pas être si difficile pour ceux qui sont habitués à développer en Ruby.

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

Dans le code de test ci-dessus, nous avons confirmé le fonctionnement de cette méthode de quatre manières: quand il y a un ou deux arguments pour Hash # merge, quand un bloc est passé et quand il n'est pas passé. Au fait, en utilisant @ cls, vous pouvez vérifier le fonctionnement de l'objet Hash et de ses objets de classe enfants.

Lançons le test. Vous pouvez exécuter tout le code de test en exécutant la commande make test-all dans workdir / build.

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

cette? J'ai une erreur. En y regardant de plus près, la méthode ʻupdate a également appelé rb_hash_update`.

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

Puisque le dernier argument est 1, quand ʻupdateest appelé,rb_hash_updatesera appelé comme s'il n'y avait qu'un seul argument. Cependant, comme j'ai déjà changé l'argument en une longueur variable dans la définition derb_hash_update, j'obtiens une erreur chaque fois que j'appelle ʻupdate. Cette méthode a été utilisée dans la bibliothèque ʻoptparsequi gère les arguments de ligne de commande. Par conséquent, il semble que la commande n'a pas pu être appelée correctement et qu'une erreur s'est produite. Modifiez la lignerb_define_method pour qu'elle puisse prendre des arguments de longueur variable lorsqu'elle est appelée depuis ʻupdate.

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

Ajoutez également minitest pour ʻupdate`.

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

Le test a réussi.

Jusqu'à la fusion

À partir de maintenant, j'écrirai sur la manière dont les modifications ci-dessus sont fusionnées dans le référentiel de Ruby lui-même. J'expliquerai le déroulement jusqu'à ce que le patch Ruby soit importé pour le moment, en citant à nouveau le document suivant. ko1/rubyhackchallenge Ruby est une version gérée par Subversion, pas par Git. Par conséquent, il n'est pas possible d'incorporer les modifications en soumettant simplement une demande d'extraction à GitHub. Des discussions sur Ruby ont lieu quotidiennement sur Redmine, l'OSS pour la gestion de projet, donc si vous souhaitez apporter des modifications, annoncez d'abord les modifications que vous souhaitez apporter (cela s'appelle un ticket). .. Ruby Issue Tracking System Les tickets sont divisés en «Demande de fonctionnalité» et «Rapport de bogue». Ce changement est classé comme le premier. Dans ce cas, il semble que le ticket devrait inclure le contenu suivant.

--Résumé (bref résumé des suggestions)

Surtout, en ce qui concerne la demande de fonctionnalité, il est important que la personne qui utilise réellement la fonction soit réellement utilisée, donc si vous écrivez un cas d'utilisation spécifique, etc. dans la section Contexte, il semble que la possibilité d'être incorporée augmentera. Si vous aimez ce contenu, les personnes ayant l'autorité de commettre sur Subversion (sans parler du Ruby Committer!) Incorporeront les changements dans Ruby Subversion.

C'est le ticket que j'ai fait cette fois. Make the number of arguments of Hash#merge variable Pour l'implémentation, j'ai fait une demande d'extraction séparée pour GitHub et j'ai joint le lien. Pour l'arrière-plan, il existe des exemples de débordement de pile et de Qiita où vous devez écrire du code ennuyeux en raison de l'impossibilité de fusionner trois hachages avec Hash # merge. Je l'ai recherché et collé.

Jusqu'à présent, le Ruby Hack Challenge est terminé. Enfin, j'ai eu l'occasion d'annoncer mon implémentation devant tout le monde chez Ruby Committer, et j'ai eu une réponse positive de tout le monde, et il a été décidé que ce code serait incorporé dans le référentiel Ruby sur place. J'ai fait. J'étais vraiment heureux à ce moment-là ...

Après cela, sur la demande d'extraction GitHub, j'ai changé les parties signalées par diverses personnes, telles que la notation incorrecte, l'indentation et la documentation incomplète. (Parfois, vous avez fait les changements vous-même ... Merci)

Répétez cela pendant une semaine ... enfin ... スクリーンショット 2018-12-04 17.53.57.png Le code que j'ai écrit a été incorporé dans Ruby! J'étais vraiment content.

finalement

Cette fois, j'étais vraiment heureux de pouvoir contribuer à mon Ruby préféré en combinant diverses choses. Merci aux personnes impliquées dans l'organisation du Ruby Hack Challenge, aux Ruby Commiters qui l'ont accepté et aux différentes personnes qui nous ont donné les révisions de code. Je vous en suis vraiment reconnaissant. J'espère que cet article réduira les obstacles pour de nombreuses personnes à contribuer à Ruby. Toi qui es encore timide. Je pense que Ruby est la seule langue majeure au Japon qui a une communauté aussi forte de développeurs de base. C'est un gaspillage de ne pas s'en servir!

Recommended Posts

Une histoire que même un homme qui ne comprend pas le langage C pourrait ajouter de nouvelles fonctions à Ruby 2.6
Après avoir vérifié le problème de Montyhall avec Ruby, c'était une histoire que je pouvais bien comprendre et que je ne comprenais pas bien
MockMVC renvoie 200 même si je fais une demande vers un chemin qui n'existe pas
[JQuery] Un gars que même les débutants pourraient bien comprendre
Si hash [: a] [: b] [: c] = 0 dans Ruby, je veux que vous étendiez récursivement même si la clé n'existe pas
Correspondant à "erreur que l'authentification de base ne réussit pas" dans le code de test "L'histoire qui n'a pas pu être faite"
Transfert de fichiers vers un environnement virtuel qui n'a pas pu être résolu même après avoir essayé pendant une journée: Mémorandum
Comment interagir avec un serveur qui ne plante pas l'application
Une histoire qui a souffert d'un espace qui ne disparaît pas même s'il est taillé avec Java La cause est BOM