J'ai essayé d'implémenter Ruby avec Ruby (et C) (j'ai joué avec intégré)

introduction

Comme le titre l'indique, il s'agit d'une histoire d'implémentation de Ruby lui-même dans Ruby (et C) en utilisant la fonction intégrée de Ruby (empruntée à la méthode d'appel car je ne connais pas le nom officiel).

Le contenu est destiné à ceux qui souhaitent implémenter Ruby lui-même.

Qu'est-ce qui est intégré?

Builtin est d'implémenter Ruby lui-même dans Ruby (et C) (N'y a-t-il pas un nom formel jusqu'à présent?). Vous pouvez implémenter Ruby plus facilement en utilisant Ruby et C en appelant __builtin_ <nom de la fonction défini en C> à partir du code Ruby comme indiqué ci-dessous.

Par exemple, Hash # delete est implémenté en C comme suit:

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

Le premier argument, hash, reçoit le hachage lui-même comme argument, et le second argument, key, reçoit la clé passée dans Hash # delete. En passant, les valeurs telles que les variables du côté Ruby sont reçues en tant que type «VALUE» et traitées par les fonctions C.

    rb_hash_modify_check(hash);

La fonction rb_hash_modify_check exécute en interne la fonction rb_check_frozen pour voir si le hachage est figé.

static void
rb_hash_modify_check(VALUE hash)
{
    rb_check_frozen(hash); //Vérifiez si l'objet est gelé
}

Dans val = rb_hash_delete_entry (hash, key);, la valeur à supprimer est acquise en fonction de la clé reçue dans l'argument, et la suppression est effectuée en même temps. S'il n'y a pas de valeur associée à la clé, la valeur non définie utilisée en C appelée Qundef sera entrée.

    if (val != Qundef) {
	return val;
    }
    else {
	if (rb_block_given_p()) {
	    return rb_yield(key);
	}
	else {
	    return Qnil;
	}
    }

Branche le processus avec la valeur «val», et renvoie la valeur supprimée si ce n'est pas «Qundef» (c'est-à-dire si la valeur peut être obtenue et supprimée à l'aide de la clé). Si c'est «Qundef», il renvoie «Qnil» («nil» en Ruby). Si un bloc est passé, il exécute rb_yield (key) et renvoie le résultat.

De cette façon, le Ruby que vous utilisez habituellement est implémenté à l'aide de C.

En utilisant la fonction intégrée, le code ci-dessus sera le suivant.

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

Parce que l'exécution des blocs est traitée côté Ruby. Je pense que l'implémentation en C est plus simple et plus facile à lire. Aussi

En utilisant la fonction intégrée de cette manière, vous pouvez implémenter Ruby avec Ruby et un peu de code C.

De plus, il semble qu'il y ait des cas où les performances s'améliorent lorsqu'elles sont implémentées dans Ruby plutôt que dans C. Pour une histoire plus spécifique, M. Sasada a parlé à RubyKaigi 2019, alors veuillez vous y référer.

Write a Ruby interpreter in Ruby for Ruby 3

Je l'ai essayé

J'ai trouvé que je pouvais implémenter une méthode utilisant le code Ruby en utilisant intégré, donc je l'ai essayé.

Construction d'environnement de développement

J'ai commencé par créer un environnement de développement Ruby. J'ai utilisé WSL + Ubuntu 18.04 comme environnement et créé un environnement de développement. Comme procédure de base, j'ai procédé en faisant référence à la (2) structure du code source IRM du Ruby Hack Challenge.

Tout d'abord, nous installerons les bibliothèques à utiliser.

sudo apt install git ruby autoconf bison gcc make zlib1g-dev libffi-dev libreadline-dev libgdbm-dev libssl-dev

Créez ensuite un répertoire de travail et accédez-y. ..

mkdir workdir
cd workdir

Après avoir accédé au répertoire de travail, clonez le code source Ruby. Cela prend beaucoup de temps, c'est donc une bonne idée d'ajouter du café pendant cette période.

git clone https://github.com/ruby/ruby.git

Après avoir cloné le code source, allez dans le répertoire ruby et exécutez ʻautoconf. Il s'agit de générer un script configurequi sera exécuté plus tard. Après exécution, il retournera àworkdir`.

cd ruby
autoconf
cd ..

Créez ensuite un répertoire pour la construction et accédez-y.

mkdir build
cd build

Exécutez ../ruby/configure --prefix = $ PWD / ../ install --enable-shared pour créer un Makefile à construire. De plus, --prefix = $ PWD / ../ install spécifie où installer Ruby.

../ruby/configure --prefix=$PWD/../install --enable-shared

Puis lancez make -j pour construire. -j est une option pour effectuer une compilation en parallèle. Si vous n'êtes pas pressé, il suffit de «faire».

make -j

Enfin, lancer make install crée un répertoire ʻinstall dans le répertoire workdir` et installe Ruby.

make install

Le dernier Ruby est maintenant installé dans workdir / install.

Au fait, si vous vous demandez s'il est vraiment installé, essayez d'exécuter ../install/bin/ruby -v. Si vous voyez ruby 2.8.0 dev et la version Ruby, Ruby est correctement installé.

Essayez de redéfinir la méthode avec builtin

Maintenant que l'environnement de développement est en place, nous allons utiliser builtin pour redéfinir les méthodes. Nous ré-implémenterons le Hash # delete mentionné dans l'exemple ci-dessus.

Correction de common.mk

Tout d'abord, ajoutez divers paramètres à common.mk pour utiliser le code source Ruby lors de la construction. Il y a une description de BUILTIN_RB_SRCS autour de la 1000ème ligne de common.mk. Ajoutez un fichier contenant le code Ruby à lire par ce BUILTIN_RB_SRCS.

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)

Cette fois, ajoutez hash.rb comme suit pour implémenter Hash.

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)

Ensuite, modifiez la partie qui spécifie le fichier à lire dans la construction Hash autour de la ligne 2520. De cette manière, le fichier à lire tel que «hash.c» est spécifié.

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

Ajoutez ici hash.rbinc et builtin.h.

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 est un fichier qui est automatiquement généré lorsque make est exécuté, et est généré en fonction du contenu de__builtin_ <appelant le nom de la fonction C>dans hash.rb. De plus, builtin.h est un fichier d'en-tête avec des implémentations pour l'utilisation de builtin.

Ceci termine la modification dans common.mk.

Modification de inits.c

Puis modifiez ʻinits.c`. Cependant, il est très facile à réparer.

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.cajoute le fichier source Ruby qui utilise intégré comme décrit ci-dessus. AjoutezBUILTIN (hash);` ici de la même manière.

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

C'est OK pour modifier ʻinits.c`.

Modifier hash.c

Enfin, nous modifierons le code dans hash.c.

Charger builtin.h

Tout d'abord, ajoutez #include" builtin.h " à la partie de lecture de l'en-tête autour de la 40e ligne.

  #include "ruby/st.h"
  #include "ruby/util.h"
  #include "ruby_assert.h"
  #include "symbol.h"
  #include "transient_heap.h"
+ #include "builtin.h"

Vous pouvez maintenant utiliser les structures etc. requises pour la fonction intégrée dans hash.c.

Supprimer la définition de Hash # delete

Ensuite, supprimez la partie qui définit Hash # delete.

Je pense qu'une fonction appelée ʻInit_Hash (void) est définie au bas de hash.c`.

void
Init_Hash(void)
{
 ///Le code d'implémentation de Hash etc. est écrit.
}

Les méthodes de chaque classe dans Ruby sont définies dans cette fonction comme suit.

rb_define_method(rb_cHash, "delete", rb_hash_delete_m, 1);

Pensez à rb_define_method comme à une définition de méthode dans Ruby. Passez la «VALUE» de la classe qui définit la méthode comme premier argument, et le deuxième argument est le nom de la méthode. Le troisième argument est la fonction définie en C (le processus exécuté par la méthode), et le quatrième argument est le nombre d'arguments reçus par la méthode.

Si vous souhaitez définir une méthode Ruby avec builtin, vous devez supprimer cette partie de définition. Cette fois, nous allons réimplémenter Hash # delete, donc supprimez la partie où delete est défini.

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

Correction de rb_hash_delete_m pour être disponible à partir de Builtin

Modifiez le rb_hash_delete_m appelé par rb_define_method (rb_cHash," delete ", rb_hash_delete_m, 1); que vous avez supprimé plus tôt afin qu'il puisse être utilisé dans builtin.

Il y a une implémentation de rb_hash_delete_m autour de la ligne 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;
	}
    }
}

Modifiez ceci comme suit.

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

Le point d'implémentation est que rb_execution_context_t * ec est passé comme premier argument emprunté pour prendre en charge builtin.

Vous pouvez maintenant appeler les fonctions définies en C depuis Ruby.

Charger hash.rbinc

Enfin, chargez le hash.rbinc généré automatiquement. Ajoutez #include" hash.rbinc " au bas de hash.c.

#include "hash.rbinc"

Ceci termine la modification côté code C.

Création de hash.rb

Maintenant, implémentons Hash # delete dans Ruby. Créez hash.rb dans la même hiérarchie que hash.c. Après la création, ajoutez le code comme ci-dessous.

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

Nous passons l'argument reçu à __builtin_rb_hash_delete_m, que nous avons rendu appelable avec builtin plus tôt, et affectons le résultat à value.

Après cela, la valeur de «valeur» est «nil» ou le processus est ramifié dans la même section. Dans le cas de nil Si un bloc de lumière est passé, le bloc est exécuté avec clé comme argument.

met" impl by Ruby (& C)! " Est un message à vérifier lorsque vous l'essayez réellement.

Ceci termine l'implémentation intégrée!

Essayez de construire

Construisons-le de la même manière que lorsque nous avons construit l'environnement de développement.

make -j && make install

Si la construction est réussie, c'est OK! Si la construction échoue, vérifiez les fautes de frappe, etc.

En fait, essayez avec irb

Essayons Hash # delete implémenté en interne en utilisant ʻirb`!

../install/bin/irb

Maintenant, collons le code ci-dessous!

hash = {:key => "value"}
hash.delete(:k)
hash.delete(:key)

Si le résultat est affiché comme ci-dessous, l'implémentation avec builtin est terminée!

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>

Puisqu'il est affiché comme ʻimpl par Ruby (& C)! , Vous pouvez voir que le Hash # delete` défini dans Ruby est en cours d'exécution.

Vous avez maintenant implémenté Ruby en Ruby (et C)!

À la fin

En utilisant intégré de cette manière, vous pouvez implémenter Ruby lui-même en utilisant Ruby et (un peu de code C). Par conséquent, je pense que même les personnes qui écrivent habituellement Ruby pourront facilement envoyer des correctifs tels que des modifications de méthodes.

Je suis heureux que ce soit étonnamment facile à écrire car je peux écrire le processus du côté Ruby après l'avoir essayé.

Personnellement, je pense qu'il sera plus facile d'écrire des extensions Ruby en C / C ++ si elles peuvent être utilisées en Extension etc., donc j'attends avec impatience les perspectives d'avenir.

référence

Write a Ruby interpreter in Ruby for Ruby 3

Explication complète du code source de Ruby

Comment fonctionne Ruby

Recommended Posts

J'ai essayé d'implémenter Ruby avec Ruby (et C) (j'ai joué avec intégré)
J'ai aussi essayé Web Assembly avec Nim et C
Résolution avec Ruby, Perl et Java AtCoder ABC 128 C
[Ruby] J'ai fait un robot avec de l'anémone et du nokogiri.
J'ai essayé DI avec Ruby
Extension Ruby C et volatile
Note secrète de Mathematical Girl 104e implémentée en Ruby et C
Résolution avec Ruby, Perl et Java AtCoder ABC 129 C (Partie 1)
Hello World avec Docker et langage C
Crypter avec Java et décrypter avec C #
J'ai essayé de réimplémenter Ruby's Float (arg, exception: true) avec builtin
Avec ruby ● × Game et Othello (examen de base)
Lier le code Java et C ++ avec SWIG
J'ai fait une mort risquée avec Ruby
Résolution avec Ruby, Perl et Java AtCoder ABC 129 C (Partie 2) Méthode de planification dynamique
Conversion de JSON en TSV et TSV en JSON avec Ruby
Lecture et écriture ligne par ligne à partir du tampon avec communication TCP entre C et Ruby
AtCoder Beginner Contest 169 A, B, C avec rubis
[Ruby] Mots clés avec mots clés et valeurs par défaut des arguments
Essayez d'intégrer Ruby et Java avec Dapr
J'ai fait un blackjack avec Ruby (j'ai essayé d'utiliser minitest)
J'ai créé une bibliothèque d'extension Ruby en C
Créez un notebook Jupyter avec Docker et exécutez ruby
J'ai vérifié le nombre de taxis avec Ruby
J'ai fait un portfolio avec Ruby On Rails
Rubis et gemme
[Ruby] J'ai réfléchi à la différence entre each_with_index et each.with_index
Ressentez facilement le type de base et le type de référence avec ruby
J'ai essayé de lire et de sortir CSV avec Outsystems
Tableau 2D AtCoder ABC129 D résolu en Ruby et Java
[Ruby] Exclure et remplacer des modèles spécifiques par des expressions régulières
J'ai démarré MySQL 5.7 avec docker-compose et j'ai essayé de me connecter
Ressentez facilement le type de base et le type de référence avec ruby 2
Je veux faire des transitions d'écran avec kotlin et java!
Installez rbenv avec apt sur ubuntu et mettez ruby
J'ai essayé de mâcher C # (lire et écrire des fichiers)
[Tutoriel] [Ruby] Création et débogage de gemmes d'extension native C