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.
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
J'ai trouvé que je pouvais implémenter une méthode utilisant le code Ruby en utilisant intégré, donc je l'ai essayé.
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é.
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.
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
.
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. Ajoutez
BUILTIN (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`.
Enfin, nous modifierons le code dans hash.c
.
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
.
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);
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.
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.
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!
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.
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)!
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.
Write a Ruby interpreter in Ruby for Ruby 3
Explication complète du code source de Ruby
Recommended Posts