Outils pour utilisateurs

Outils du site


langages:ruby:etendre-ruby-en-c

Étendre Ruby en C

Je vous propose une initiation, la plus large possible, à la création d'une extension en C pour le langage Ruby. Que ce soit pour la recherche des meilleures performances possibles ou plus simplement pour interfacer une librairie écrite initialement en C, vous aurez besoin d'une part de savoir comment s'organise ce développement et d'autre part de vous faire une idée de la structure de cette API et des fonctions qu'elle propose.

TODO:

  • Les classes : accesseur/mutateur, variable d'instance, variable de classe, méthode singleton, …
  • Data_[Make|Wrap|Get]_Struct [Trouver un exemple valable]
  • Utiliser le Garbage Collector (mark and sweep)
  • Les exceptions : il y a quelques trous dans les descriptions
  • Les types spéciaux comme les symboles (SYMBOL et ID, cf chapitre 2.2.2 de README.EXT)
  • Les structures de données (au moins Array et String) [String prévue # Array pas encore]
  • Déboguer : récupérer différentes informations sur un objet (rb_obj_id, rb_obj_classname)
  • Créer un bloc (rb_proc_new ou <b>rb_iterate</b> ?)
  • 2013 : VALUE ⇒ char * : StringValueCStr
  • 2013 : ID/VALUE, explication de type
  • Les threads ?
  • Les IO ?
  • Les regexp ?

Présentation

Introduction

Pourquoi étendre Ruby en développant une nouvelle extension ? On peut avoir à écrire une extension premièrement parce qu'il n'est pas possible de faire autrement comme par exemple pour pouvoir exploiter les spécificités de son système. Second cas, on veut interfacer une bibliothèque écrite en langage C ce qui présente divers avantages : elle est maintenue et éprouvée, votre extension peut évoluer de façon plus ou moins indépendante par rapport à cette librairie. Enfin, pour des raisons de performances : une extension écrite en C sera bien plus rapide qu'un équivalent élaboré en Ruby pur.

Cependant, il faut prendre conscience qu'une extension écrite en langage C est potentiellement bien plus menaçante : des bugs ou des défauts dans son écriture auront bien plus d'impacts et un comportement indéfini dans de tels cas. Ce problème ne se pose normalement pas avec un script Ruby, qui dans le pire des cas provoquera une exception ou mettra fin à l'exécution de l'interpréteur.

Entrons à présent dans le vif du sujet : comparé à d'autres langages scripts, l'écriture d'une extension en C pour Ruby s'avère être plutôt aisée du fait que l'on ne manipule la plupart du temps qu'un seul type de données en C pour Ruby. Il s'agit d'un type nommé VALUE et qui correspond aux références sur les objets Ruby.

Note : Les exemples donnés tout au long de cet article n'ont qu'une valeur pédagogique, d'autant plus que quelques codes C ne sont valables uniquement sur des environnements Linux voir Unix (BSD).

Conventions, styles et typographie

Les macros C, que l'on reconnaît facilement à leurs noms en majuscules, ont été volontairement prototypées lorsqu'elles correspondent d'une manière ou d'une autre à un appel à une vraie fonction. Ce choix a été motivé par le fait que ces dernières n'attendent pas n'importe quoi comme paramètres et ne renvoient pas non plus une quelconque valeur (dans certains cas aucune).

Les objets Ruby qui peuvent avoir une taille variable (comme les Array, String, Hash, etc) sont gérés au mieux en interne pour éviter de sans cesse adapter la quantité de mémoire aux besoins de l'objet manipulé. Les réallocations de mémoire s'avèrent en effet être coûteuses. La technique consiste d'une part à utiliser une capacité minimale par défaut et à l'augmenter de manière appropriée (en la doublant par exemple). C'est pourquoi je parlerais de <b>longueur</b> pour désigner la partie utilisée par la représentation de l'objet en mémoire et de <b>capacité</b> pour faire référence au nombre d'éléments total alloués, qu'ils soient utilisés ou non. Illustration avec une chaîne de caractères :

Ici il n'est question que d'une illustration, la capacité a été choisie arbitrairement et le caractère nul marquant la fin d'une chaîne en C n'y figure pas, bien qu'en Ruby on ne s'en préoccupe pas.

La capacité est automatiquement augmentée lorsque les besoins deviennent égaux ou supérieurs à la capacité actuelle interne de l'objet.

Ce fonctionnement est identique pour une table de hachage :

Info : Ruby recommande aux développeurs d'utiliser le style K & R pour la déclaration de leurs fonctions. Afin d'alléger l'écriture de celles-ci dans les exemples j'ai sciemment ignoré cette convention en faveur d'un style plus usuel (ANSI).

Constitution d'une extension

Une extension est généralement constituée de deux parties :

  1. Un code C, qui une fois compilé donnera une librairie dynamique.
  2. Un script Ruby, utilisant la librairie mkmf, pour générer aisément un Makefile qui vous permettra de compiler ces sources C.

Votre librairie sera chargée par Ruby lors de l'appel à l'instruction require. Pour ce faire, votre code C doit nécessairement comporter un point d'entrée particulier. Etant en C, celui-ci correspond à une fonction spécifique dont le nom suivra le modèle suivant : void Init_<nom de votre extension>(). Elle sera par conséquent chargée de procéder à tout ce qui est initialisation dont notamment aux définitions de vos différents modules, classes, méthodes, etc (que nous verrons plus tard). Voici un code minimal pour créer une telle extension :

#include <ruby.h>
 
void Init_<extension>()
{
    rb_warn("Extension chargée");
}

La partie <extension>, que vous retrouverez par la suite, est à remplacer par le nom que vous avez choisi.

Créez un script extconf.rb (son nom par convention). Le minimum pour générer le Makefile étant :

require 'mkmf'
 
create_makefile('<extension>')

Nous exécutons ensuite le script extconf.rb pour produire ce Makefile :

ruby extconf.rb

Puis, lançons la compilation :

make

Vous pouvez installer cette extension avec les autres pour permettre à tout le monde de l'utiliser via la cible install du Makefile. Nous ne l'utiliserons pas puisque l'extension ici ne présente qu'un but pédagogique et est à réserver aux extensions "stables". Pour information, il aurait suffit d'enchaîner par cette commande :

make install

Testez à présent l'extension par un script ou une session IRB en commençant par la charger :

require '<extension>.so'

Info : Lorsque aucune ambiguïté n'est possible (avec un script Ruby du même nom par exemple), l'extension .so (ou autres - .dll) peut être omise.

Vous trouverez ci-dessous un exemple un peu plus étoffé. Seule la structure des différentes sources abordée ici (l'inclusion du fichier d'entête ruby.h, la fonction Init_intro, le script extconf.rb) nous intéresse pour le moment.

#include <ruby.h>
 
static ID id_rand;
 
/*
 * Générer un nombre aléatoire de 0 à max - 1 (inclus).
 * On fait appel à la fonction Ruby de manière à être portable.
 */
static int rand_int(int max)
{
    return FIX2INT(rb_funcall(rb_mKernel, id_rand, 1, INT2FIX(max)));
}
 
/*
 *  call-seq:
 *     str.shuffle!  => str
 *
 *  Mélanger, sur place, aléatoirement les caractères d'une chaîne.
 *
 *     s = "coucou"
 *     s.shuffle!  #=> "ucuooc"
 */
VALUE rb_str_shuffle_bang(VALUE str)
{
    if (RSTRING(str)->len > 1) {
        int ridx, i;
        char *p, tmp;
        const char *pend;
 
        rb_str_modify(str);
        i = 0;
        p = RSTRING(str)->ptr;
        pend = p + RSTRING(str)->len;
        while (pend > p) {
            ridx = rand_int(pend - p) + i++;
            tmp = RSTRING(str)->ptr[ridx];
            RSTRING(str)->ptr[ridx] = *p;
            *p++ = tmp;
        }
    }
 
    return str;
}
 
/*
 *  call-seq:
 *     str.shuffle!  => new_str
 *
 *  Mélanger aléatoirement les caractères d'une chaîne.
 *
 *     'coucou'.shuffle  #=> "ouccou"
 */
VALUE rb_str_shuffle(VALUE str)
{
    return rb_str_shuffle_bang(rb_str_dup(str));
}
 
/*
 *  call-seq:
 *     ary.shuffle!  => ary
 *
 *  Mélanger, sur place, aléatoirement les éléments d'un tableau.
 *
 *     a = (0..9).to_a
 *     a.shuffle!  #=> [3, 9, 5, 2, 1, 6, 0, 4, 7, 8]
 */
VALUE rb_ary_shuffle_bang(VALUE ary)
{
    if (RARRAY(ary)->len > 1) {
        long ridx, i;
        VALUE tmp;
 
        for (i = 0; i < RARRAY(ary)->len; i++) {
            ridx = rand_int(RARRAY(ary)->len - i) + i;
            tmp = RARRAY(ary)->ptr[ridx];
            RARRAY(ary)->ptr[ridx] = RARRAY(ary)->ptr[i];
            RARRAY(ary)->ptr[i] = tmp;
        }
    }
 
    return ary;
}
 
/*
 *  call-seq:
 *     ary.shuffle!  => new_ary
 *
 *  Mélanger, aléatoirement les éléments d'un tableau.
 *
 *     (0..9).to_a.shuffle  #=> [2, 5, 4, 0, 3, 9, 6, 8, 1, 7]
 */
VALUE rb_ary_shuffle(VALUE ary)
{
    return rb_ary_shuffle_bang(rb_ary_dup(ary));
}
 
void Init_intro()
{
    id_rand = rb_intern("rand");
 
    rb_define_method(rb_cString, "shuffle!", rb_str_shuffle_bang, 0);
    rb_define_method(rb_cString, "shuffle", rb_str_shuffle, 0);
 
    rb_define_method(rb_cArray, "shuffle!", rb_ary_shuffle_bang, 0);
    rb_define_method(rb_cArray, "shuffle", rb_ary_shuffle, 0);
}

Pour terminer, un exemple de script mettant en oeuvre cette extension :

require 'intro.so'
require 'pp'
 
puts 'coucou'.shuffle
pp (1..9).to_a.shuffle

Utilisation des types de base Ruby

Les contantes Ruby

Les constantes incontournables Ruby telles true ou nil sont définies en C de la manière suivante :

Nom en Ruby Équivalent en C Valeur en C Classe associée
false Qfalse 0 rb_cFalseClass
true Qtrue 1 rb_cTrueClass
nil Qnil 4 rb_cNilClass
undef Qundef 6 -

Les types numériques

De Ruby vers C

Sont recensées ci-dessous toutes les fonctions de conversion de tout type d'objet numérique Ruby vers un type C proche (celles en majuscules sont en réalité des macros, conformément aux conventions) :

Prototype de la "fonction" Type de départ côté Ruby Type obtenu en C
int FIX2INT(VALUE) Fixnum int
unsigned int FIX2UINT(VALUE) Fixnum unsigned int
int NUM2INT(VALUE) Numeric int
unsigned int NUM2UINT(VALUE) Numeric unsigned int
long FIX2LONG(VALUE) Fixnum long
unsigned long FIX2ULONG(VALUE) Fixnum unsigned long
long NUM2LONG(VALUE) Numeric long
long rb_num2long(VALUE) Numeric long
unsigned long NUM2ULONG(VALUE) Numeric unsigned long
unsigned long rb_num2ulong(VALUE) Numeric unsigned long
long long NUM2LL(VALUE) Numeric long long
long long rb_num2ll(VALUE) Numeric long long
unsigned long long NUM2ULL(VALUE) Numeric unsigned long long
unsigned long long rb_num2ull(VALUE) Numeric unsigned long long
double NUM2DBL(VALUE) Numeric double
double rb_num2dbl(VALUE) Numeric double

De C vers Ruby

Vous aurez très vite la nécessité de faire l'opération inverse, convertir votre variable C, d'un certain type (double, long, etc) vers un objet Ruby numérique adéquat. Voici la liste des fonctions disponibles à cette fin :

Prototype de la "fonction" Type de départ en C Type obtenu en Ruby
VALUE INT2FIX(int) int Fixnum
VALUE LONG2FIX(long) long Fixnum
VALUE INT2NUM(int) int Numeric
VALUE UINT2NUM(unsigned int) unsigned int Numeric
VALUE LONG2NUM(long) long Numeric
VALUE rb_int2inum(long) long Numeric
VALUE ULONG2NUM(unsigned long) unsigned long Numeric
VALUE rb_uint2inum(unsigned long) unsigned long Numeric
VALUE LL2NUM(long long) long long Numeric
VALUE rb_ll2inum(long long) long long Numeric
VALUE ULL2NUM(unsigned long long) unsigned long long Numeric
VALUE rb_ull2inum(unsigned long long) unsigned long long Numeric
double RFLOAT(VALUE)→value double Float

Les chaînes de caractères

Fonctions de manipulation

La liste des fonctions permettant d'agir sur les objets de la classe String est la suivante :

Prototypes de la fonction Description
VALUE rb_str_append(VALUE str1, VALUE str2) Concaténer la chaîne str2 à str1, qui est renvoyée.
VALUE rb_str_buf_append(VALUE str1, VALUE str2) Concaténer la chaîne str2 à str1, qui est renvoyée.
VALUE rb_str_buf_cat(VALUE str, const char *ptr, long len) Ajouter len> caractères de la chaîne C ptr à l'objet str. Ce dernier étant renvoyé.
VALUE rb_str_buf_cat2(VALUE str, const char *ptr) Concaténer la chaîne C ptr à l'objet str, qui est renvoyé.
VALUE rb_str_buf_new(long capa) Crée un buffer de longueur capa (la capacité minimale sera de 128 octets).
VALUE rb_str_buf_new2(const char *ptr) Crée un buffer à partir de la chaîne C ptr.
VALUE rb_str_cat(VALUE str, const char *ptr, long len) Ajouter à la chaîne str, len caractères de la chaîne C ptr.
VALUE rb_str_cat2(VALUE str, const char *ptr) Ajouter la chaîne C ptr à str.
int rb_str_cmp(VALUE str1, VALUE str2) Comparer deux objets String. Elle renvoie 0 si elles sont égales, 1 si str1 est "plus grande" que str2 et -1 si str1 est "plus petite" que str2.
VALUE rb_str_concat(VALUE str1, VALUE str2) Concaténer la chaîne str2 à str1, qui est renvoyée.
VALUE rb_str_dump(VALUE str) Retourne une version de la chaîne str où les caractères non imprimables sont remplacés par leur représentation (\n, \a, …) ou par leur code octal (\Oxxx)
VALUE rb_str_dup(VALUE str) Renvoie une copie de la chaîne str.
VALUE rb_str_dup_frozen(VALUE str) Renvoie une copie figée (= non modifiable) de la chaîne str
VALUE rb_str_freeze(VALUE str) Figer la chaîne str et vous est ensuite retournée
int rb_str_hash(VALUE str) Obtenir la valeur de hachage de la chaîne str
VALUE rb_str_inspect(VALUE str) Fonction retournant une version imprimable de str, semblable à rb_str_dump, mais l'entoure en plus de double quotes.
VALUE rb_str_intern(VALUE str) Renvoie un symbole correspondant à la chaîne str
VALUE rb_str_locktmp(VALUE str) Poser un verrou, le temps de faire des opérations, contre la modification la chaîne str
void rb_str_modify(VALUE str) S'assure que la chaîne peut être modifiée : une erreur sera levée si la chaîne est gelée ou semble souillée (dépendant du niveau de sécurité). Elle assure également que les éléments partagés (comme la chaîne vide) sont alloués et copiés au lieu de les pointer
VALUE rb_str_new(const char *ptr, long len) Crée un objet String à partir des <i>len</i> premiers caractères de la chaîne C ptr
VALUE rb_str_new2(const char *ptr) Crée un objet String à partir de la chaîne C désignée par ptr
VALUE rb_str_plus(VALUE str1, VALUE str2) Retourne une nouvelle chaîne résultant de la concaténation de str2 à str1
VALUE rb_str_resize(VALUE str, long len) Redimensionne l'objet String str (si la chaîne devient plus petite, elle sera tronquée)
void rb_str_setter(VALUE val, ID id, VALUE *var) Fonction utilisée comme intermédiaire par rb_define_hooked_variable (comme quatrième paramètre) afin de modifier une variable globale attendant une valeur de type chaîne uniquement
VALUE rb_str_split(VALUE str, const char *sep) Renvoie un tableau des différentes parties de la chaîne str conformément au séparateur sep
VALUE rb_str_substr(VALUE str, long beg, long len) Crée un nouvel objet String correspondant à la partie de <i>len</i> caractères à partir de la position beg de la String str
VALUE rb_str_times(VALUE str, VALUE times) Crée un nouvel objet String qui est la répétition de times occurences de str
VALUE rb_str_unlocktmp(VALUE str) Relâcher le verrou précédemment posé sur la chaîne str dans le but d'empêcher toute manipulation sur celle-ci
void rb_str_update(VALUE str, long beg, long len, VALUE val) Remplacer la partie de la chaîne str à partir de l'indice beg (le premier étant 0) et sur la longueur len par val
VALUE rb_str_upto(VALUE beg, VALUE end, int excl) Appeler un bloc transmettant les différentes valeurs séparant beg à end. Le paramètre excl avec une valeur non nulle permet d'exclure le dernier élément (end)
VALUE rb_string_value(volatile VALUE *ptr) Convertir l'objet pointé par ptr en une chaîne. S'il ne s'agit pas d'une String, une chaîne vide vous sera renvoyée

Info : Les fonctions rb_str_buf_* sont recommandées et à privélégier dans la mesure du possible pour des concaténations multiples puisque de la mémoire est allouée à l'avance d'où, en général, de meilleures performances.

Voyons à présent les fonctions spécialisées dans la conversion d'un objet vers un objet String :

Prototypes de la fonction Type de départ Description
VALUE rb_fix2str(VALUE x, int base) Fixnum Convertir l'objet Fixnum x en un objet String le représentant en base base
VALUE rb_big2str(VALUE x, int base) Bignum Convertir l'objet Bignum x en un objet String le représentant en base base
VALUE rb_big2str0(VALUE x, int base, int trim) Bignum Convertir l'objet Bignum x en un objet String le représentant en base base. Le paramètre trim indique si les zéro de poids fort doivent être supprimés ou non

Il en existe d'autres, effectuant le travail inverse, c'est à dire, de conversion d'un objet String en un autre :

Prototypes de la fonction Type obtenu Description
double rb_str_to_dbl(VALUE str, int badcheck) double Convertir l'objet String str en un double. Le paramètre badcheck sera permissif avec une valeur nulle (aucune erreur ne sera levée)
VALUE rb_str2inum(VALUE str, int base) Numeric Convertir l'objet String str représentant un nombre en base base en un objet Numeric. Si le paramètre base a une valeur inférieure ou égale à zéro, elle tentera de le déterminer à partir du préfixe (X12 pour 18 en hexadécimal, o23 pour 17 en octal, b1011 pour 11 en binaire, …)
VALUE rb_str_to_inum(VALUE str, int base, int badcheck) Numeric Strictement identique à rb_str2inum. Elle présente seulement un paramètre supplémentaire, badcheck, qui permet de choisir le comportement de la fonction en cas de rencontre d'un caractère non désiré : avec la valeur nulle, une valeur numérique sera renvoyée même si la chaîne n'a pas pu être intégralement interprétée et avec une valeur non nulle, une exception (TypeError) sera provoquée si un tel cas se présente
VALUE rb_str_to_str(VALUE str) String Tente de convertir un objet str en un objet String. Si s'en est un, c'est lui-même qui sera retourné sinon la méthode to_str sera appelée sur l'objet pour procéder à la conversion (si une telle méthode n'est pas disponible une exception TypeError sera provoquée)

Et enfin, de conversion d'une chaîne C vers un objet numérique :

Prototypes de la fonction Type obtenu Description
VALUE rb_cstr2inum(const char *ptr, int base) Numeric Convertir la chaîne C ptr représentant un nombre en base base en un objet Numeric. Si le paramètre base a une valeur inférieure ou égale à zéro, elle tentera de le déterminer à partir du préfixe (x23 pour 35 en hexadécimal, o42 pour 34 en octal, b100001 pour 33 en binaire, …)
double rb_cstr_to_dbl(const char *ptr, int badcheck) double Convertir la chaîne C désignée par ptr en un double. Le paramètre badcheck avec une valeur non nulle provoquera une erreur de type ArgumentError si la chaîne est constituée de caractères qu'il n'est pas possible d'interpréter
VALUE rb_cstr_to_inum(const char *ptr, int base, int badcheck) Numeric Convertir la chaîne C ptr représentant un nombre en base base en un objet Numeric (Fixnum ou Bignum suivant la capacité requise). Le paramètre badcheck permet, si sa valeur est nulle, d'obtenir quoiqu'il arrive une représentation numérique de la chaîne

De Ruby vers C

Bien sûr vous aurez vite besoin de faire l'opération inverse : obtenir une chaîne C représentant votre objet Ruby (qu'il soit ou non lui-même un objet de type String).

Un objet String est défini comme suit, ce qui vous permet de vous faire une idée de comment il est constitué et comment obtenir certaines informations bien que des fonctions (ou des macros) vous permettent d'y faire quelque peu abstraction :

struct RString {
    struct RBasic basic;
    long len;      /* Sa longueur */
    char *ptr;     /* Un pointeur sur la chaîne */
    union {
        long capa; /* Sa capacité */
        VALUE shared;
    } aux;
};

Pour la longueur de la chaîne (type long), il est plus simple d'utiliser la macro prévue à cet effet :

#ifndef RSTRING_LEN
# define RSTRING_LEN(s) (RSTRING(s)->len)
#endif /* !RSTRING_LEN */
 
/* Exemple : */
void ma_fonction(VALUE str)
{
    long len;
 
    Check_Type(T_STRING);
    len = RSTRING_LEN(str);
 
    /* ... */
}

Les fonctions pour obtenir une chaîne C à partir d'un objet Ruby (String comme autres) sont les suivantes :

Prototype Description
char *StringValueCStr(VALUE v) et char *rb_string_value_cstr(volatile VALUE *ptr) Obtenir un pointeur sur la chaîne C correspondant à l'objet v ou pointé par ptr Cette fonction est plus "sûre" car effectue des contrôles supplémentaires pouvant donner suite à une erreur s'ils ne sont pas vérifiés (pointeur interne non NULL et vérification de la présence de caractères nuls dans celle-ci)
char *StringValuePtr(VALUE v) et char *rb_string_value_ptr(volatile VALUE *ptr) Obtenir un pointeur sur la chaîne C correspondant à l'objet v ou pointé par ptr
char *rb_str2cstr(VALUE str, long *len) Obtenir un pointeur sur la chaîne C correspondant à l'objet str. Si len n'est pas NULL alors vous obtiendrez la longueur de cette chaîne

Une macro, RSTRING_PTR, est présente pour retourner directement la chaîne C encapsulée dans un objet String :

#ifndef RSTRING_PTR
# define RSTRING_PTR(s) (RSTRING(s)->ptr)
#endif /* !RSTRING_PTR */
 
/* Exemple : */
void ma_fonction(VALUE str)
{
    char *c_string;
 
    Check_Type(T_STRING);
    c_string = RSTRING_PTR(str);
 
    /* ... */
}

Note : seule StringValueCStr vous assure une chaîne C terminée par un caractère nul. Ce n'est pas le cas des autres car Ruby ne maintient pas \0 en fin de ses chaînes.

Adaptation des objets Ruby

Il existe, pour quelques uns des types de base de Ruby, des routines de conversion prédéfinies présentées ci-dessous avec une explication sur les objets sur lesquels il est possible d'appeler une telle fonction et le procédé de conversion :

Prototype de la fonction C Type de l'objet Ruby désiré Types possibles de l'objet Ruby de départ
VALUE rb_Float(VALUE val) Float Fixnum, Float, Bignum, String
VALUE rb_Integer(VALUE val) Integer Tous (tentative de conversion par appel de la méthode to_int pour les objets non standards)
VALUE rb_Array(VALUE val) Array Tous (appel de la méthode to_a, sinon crée un tableau avec pour seul élément l'objet désigné par val)
VALUE rb_String(VALUE val) String Tous (appel de la méthode to_s pour obtenir une représentation de l'objet sous la forme d'une chaîne)

Encapsuler des données C

TODO

VALUE Data_Wrap_Struct(VALUE klass, void (*dmark)(void *ptr), void (*dfree)(void *ptr), void *sval)
void Data_Make_Struct(VALUE klass, type, void (*dmark)(void *ptr), void (*dfree)(void *ptr), void *sval)
void Data_Get_Struct(VALUE obj, type, void *sval)

Lien avec le ramasse-miettes (Garbage Collector)

TODO

Les tableaux

Description des fonctions

Commençons par détailler toutes les fonctions relatives aux tableaux, nous verrons ensuite quelques exemples d'application :

Prototypes de la fonction Description
VALUE rb_ary_aref(int argc, VALUE *argv, VALUE array) Retourne un une partie ou un élément du tableau array. Cela dépend de ses paramètres selon qu'il soit sous forme d'un simple indice (tab[index]) ou d'une partie (tab[index, longueur] ou tab[début..fin])
VALUE rb_ary_assoc(VALUE array, VALUE key) Retourne le premier sous-tableau du tableau array qui comporte pour premier élément key. Si aucune correspondance n'est établit Qnil sera renvoyé
VALUE rb_ary_clear(VALUE array) Vide le tableau array, obtenant ainsi un tableau vide ([])
VALUE rb_ary_cmp(VALUE array1, VALUE array2) Retourne un Fixnum permettant de comparer les tableaux array1 et array2 (-1 : array1 est plus petit que array2, 0 : les tableaux sont égaux, 1 : array1 est plus grand que array2)
VALUE rb_ary_concat(VALUE obj1, VALUE obj2) Ajoute les éléments de obj2 à obj1 (ce dernier étant renvoyé)
VALUE rb_ary_delete(VALUE array, VALUE item) Supprime toutes les occurences égales à l'objet item dans le tableau array La valeur retournée est le tableau lui-même ou Qnil
VALUE rb_ary_delete_at(VALUE array, long position) Supprime l'élément situé à l'indice position du tableau array (la position peut être négative pour que l'indexation soit faite à partir de sa fin)
VALUE rb_ary_dup(VALUE array) Retourne une copie du tableau array
VALUE rb_ary_each(VALUE array) Appelle un bloc sur chaque élément du tableau array (sa valeur retournée)
VALUE rb_ary_entry(VALUE array, long offset) Retourne l'élément à la position offset du tableau array
VALUE rb_ary_freeze(VALUE array) Figer le tableau array
VALUE rb_ary_includes(VALUE array, VALUE item) Renvoie Qtrue si l'élément tem est présent dans le tableau array sinon Qfase
VALUE rb_ary_join(VALUE array, VALUE separator) Retourne une chaîne (String) résultant de la concaténation des différents éléments du tableau array. Ces éléments seront séparés par separator (la valeur Qnil vous permet de ne pas en utiliser). Voir la fonction rb_ary_to_s qui utilise le séparateur par défaut ($,)
VALUE rb_ary_new(void) Crée (et retourne) un nouveau tableau vide avec des paramètres d'initialisation par défaut (capacité initiale de 16 éléments)
VALUE rb_ary_new2(long len) Crée (et retourne) un nouveau tableau vide avec une capacité initiale de len éléments
VALUE rb_ary_new3(long n, …) Crée (et retourne) un nouveau tableau comportant les n objets donnés (des VALUE) (sa capacité initiale sera de n)
VALUE rb_ary_new4(long n, const VALUE *elts) Crée un tableau à partir des n premiers éléments du tableau C elts
VALUE rb_ary_plus(VALUE obj1, VALUE obj2) Crée et retourne un nouveau tableau résultant de la concaténation des deux objets, obj1 et obj2
VALUE rb_ary_pop(VALUE array) Supprime et retourne le dernier élément du tableau array ou Qnil si celui-ci est vide
VALUE rb_ary_push(VALUE array, VALUE item) Ajoute item à la fin du tableau array. Le tableau est lui-même retourné ce qui permet sa réutilisation
VALUE rb_ary_rassoc(VALUE array, VALUE val) Retourne le premier sous-tableau du tableau array comportant l'élément val sinon Qnil
VALUE rb_ary_reverse(VALUE array) Modifie le tableau array pour que ses éléments apparaissent dans l'ordre inversé. Ce même tableau est retourné après modification
VALUE rb_ary_shift(VALUE array) Supprime et retourne le premier élément du tableau array ou Qnil si celui-ci est vide
VALUE rb_ary_sort(VALUE array) Renvoie une copie triée du tableau array (un bloc peut être employé pour définir la manière dont doit être effectué le tri)
VALUE rb_ary_sort_bang(VALUE array) Trie le tableau array (lui-même renvoyé), en le modifiant (un bloc peut être utilisé pour modifier l'algorithme de tri par défaut)
void rb_ary_store(VALUE array, long index, VALUE val) Ajoute un nouvel élément, val, au tableau array à l'index fourni
VALUE rb_ary_to_ary(VALUE obj) Permet de récupérer une représentation sous forme de tableau d'un objet quelconque (appel de la méthode to_ary de l'objet et si celle-ci n'est pas diponible un nouveau tableau sera constitué contenant celui-ci)
VALUE rb_ary_to_s(VALUE array) Retourne une String représentant les différents éléments du tableau array, séparés par la valeur de la variable prédéfinie "$,"
VALUE rb_ary_unshift(VALUE array, VALUE item) Ajoute en tête du tableau array (qui est ensuite retourné), l'objet item

Exemple

En guise d'exemple, nous allons ajouter directement à la classe Array quelques nouvelles méthodes, à savoir le calcul de la somme de ses éléments, la suppression des éléments correspondants à une expression régulière, la conversion d'un tableau bidimensionnel en un Hash et la combinaison de deux tableaux pour obtenir un Hash :

#include <ruby.h>
 
/*
 *  call-seq:
 *     array.sum  -> float
 *
 *  Calcul la somme des éléments de <i>self</i>.
 *
 *     a = ["0.2", 3, 0.3 * 2]
 *     a.sum  #=> 3.8
 */
static VALUE rb_ary_sum(VALUE ary)
{
    long i;
    double dval;
    VALUE ptr;
 
    dval = 0.0;
    for (i = 0; i < RARRAY(ary)->len; i++) {
        ptr = RARRAY(ary)->ptr[i];
        if (TYPE(ptr) == T_FLOAT) {
            dval += RFLOAT(ptr)->value;
        } else {
            dval += RFLOAT(rb_Float(ptr))->value;
        }
    }
 
    return rb_float_new(dval);
}
 
/*
 *  call-seq:
 *     array.grep_extract!(regexp)  -> array
 *
 *  Extrait de <i>self</i> tous les éléments qui correspondent
 *  à l'expression régulière <i>regexp</i> et les retourne dans
 *  un nouveau tableau.
 *
 *     a = ("a".."h").to_a
 *     a.grep_extract!(/[aeiou]/)  #=> ["a", "e"]
 *     a                           #=> ["b", "c", "d", "f", "g", "h"]
 */
static VALUE rb_ary_grep_extract_m(VALUE ary, VALUE re)
{
    long i;
    VALUE matches, match;
 
    matches = rb_ary_new();
    for (i = 0; i < RARRAY(ary)->len; ) {
        match = rb_reg_match(re, RARRAY(ary)->ptr[i]);
        if (NIL_P(match)) {
            i++;
        } else {
            rb_ary_push(matches, rb_ary_delete_at(ary, i));
        }
    }
 
    return matches;
}
 
/*
 *  call-seq:
 *     array.to_h  -> hash
 *
 *  Crée à partir de <i>self</i>, un tableau bidimensionnel,
 *  en un hash.
 *
 *     a = [[1, "un"], [2, "deux"]]
 *     a.to_h  #=> {1 => "un", 2 => "deux"}
 */
static VALUE rb_ary_to_h(VALUE ary)
{
    long i;
    VALUE h, v;
 
    h = rb_hash_new();
    for (i = 0; i < RARRAY(ary)->len; i++) {
        v = RARRAY(ary)->ptr[i];
        if (TYPE(v) == T_ARRAY && RARRAY(v)->len > 1) {
            rb_hash_aset(h, RARRAY(v)->ptr[0], RARRAY(v)->ptr[RARRAY(v)->len - 1]);
        }
    }
 
    return h;
}
 
/*
 *  call-seq:
 *     array.combine(other_array)  -> hash
 *
 *  Associe les valeurs de deux tableaux. Les éléments de <i>self</i>,
 *  devenant les clés et ceux de <i>other_array</i>, les valeurs.
 *
 *     cles = [1, 2, 3, 4, 5]
 *     valeurs = ["un", "deux", nil, "quatre"]
 *     cles.combine(valeurs)  #=> {1 => "un", 2 => "deux", 3 => nil, 4 => "quatre", 5 => nil}
 */
static VALUE rb_ary_combine(VALUE ary1, VALUE ary2)
{
    long i, len2;
    VALUE h;
 
    ary2 = rb_Array(ary2);
    h = rb_hash_new();
    len2 = RARRAY(ary2)->len;
    for (i = 0; i < RARRAY(ary1)->len; i++) {
        rb_hash_aset(h, RARRAY(ary1)->ptr[i], i >= len2 ? Qnil : RARRAY(ary2)->ptr[i]);
    }
 
    return h;
}
 
void Init_array()
{
    rb_define_method(rb_cArray, "grep_extract!", rb_ary_grep_extract_m, 1);
    rb_define_method(rb_cArray, "to_h", rb_ary_to_h, 0);
    rb_define_method(rb_cArray, "sum", rb_ary_sum, 0);
    rb_define_method(rb_cArray, "combine", rb_ary_combine, 1);
}

Les hachages

Description des fonctions

Prototypes de la fonction Description
VALUE rb_hash(VALUE obj) Retourne le hash de l'objet obj
VALUE rb_hash_aref(VALUE hash, VALUE key) Retourne l'objet situé dans le hachage hash sous la clé key
VALUE rb_hash_aset(VALUE hash, VALUE key, VALUE val) Place l'objet val dans le hachage hash à la clé <i>key</i>. Si cette clé est déjà utilisée, l'objet à cet endroit sera écrasé. La valeur val est retournée
VALUE rb_hash_delete(VALUE hash, VALUE key) Supprime du hachage hash l'élément à la clé key et retourne sa valeur
VALUE rb_hash_delete_if(VALUE hash) Supprime les éléments qui valident le bloc pour le hachage hash, qui est ensuite renvoyé
void rb_hash_foreach(VALUE hash, int (*func)(ANYARGS), VALUE farg) Appeler la fonction <i>func</i> sur tous les éléments du hachage hash avec le paramètre farg
VALUE rb_hash_freeze(VALUE hash) Figer le hachage hash
VALUE rb_hash_new(void) Crée, initialise un nouveau hachage qui vous est retourné
VALUE rb_hash_reject_bang(VALUE hash) Supprime les éléments du hachage hash pour lesquels le bloc renvoie une valeur vraie. Ce même hash est renvoyé (modification sur place), si en revanche aucune suppression n'a été apporté, elle renverra Qnil
VALUE rb_hash_select(int argc, VALUE *argv, VALUE hash) Retourne un tableau constitué des paires clé/valeur du hachage hash pour lesquelles le bloc appelé renvoie une valeur vraie (Qnil et Qfalse exclues)
VALUE rb_hash_values_at(int argc, VALUE *argv, VALUE hash) Crée un tableau de toutes les valeurs dont les clés sont dans la liste

Un objet Ruby de type Hash se définit en C comme une structure de la forme suivante :

struct RHash {
    struct RBasic basic;
    struct st_table *tbl; /* La table interne de hachage */
    int iter_lev;         /* Incrémenté à chaque parcours du hachage (et décrémenté à la fin de celui-ci */
    VALUE ifnone;         /* La valeur ou le bloc à exécuter pour attribuer une valeur par défaut à la clé
                             (lorsqu'aucune n'est fournie) */
};

La table interne de hachage de type st_table est définie de la sorte :

struct st_table {
     struct st_hash_type *type;    /* Définit le type des éléments qui composent le hachage : de pouvoir les comparer
                                      et de calculer leur hash (voir le tableau ci-dessous) */
     int num_bins;                 /* La capacité du hachage (nombre d'éléments alloués) */
     int num_entries;              /* La taille du hachage (nombre d'éléments utilisés) */
     struct st_table_entry **bins; /* Un tableau de pointeurs sur les différents éléments */
};

Vous pourriez donc avoir besoin à un moment ou à un autre avoir besoin d'utiliser les fonctions bas niveau st_*, dont certainement l'indispensable st_lookup :

Prototypes de la fonction Description
void st_add_direct(st_table *table, st_data_t key, st_data_t value) Insérer rapidement la clé key (qui ne doit pas déjà exister) et sa valeur associée value à la table table
void st_cleanup_safe(st_table *table, st_data_t never) Supprimer de la table table tous les éléménts ayant pour valeur égale au paramètre never (souvent Qundef)
st_table *st_copy(st_table *old_table) Créer une copie (valeur renvoyée) de la table old_table
int st_delete(st_table *table, st_data_t *key, st_data_t *value) Supprimer de la table table, l'élément correspondant à la clé key. Si le paramètre <i>value</i> a une valeur non nulle alors vous pourrez récupérer via ce pointeur la valeur associée à cette clé avant la suppression. Cette fonction renvoie 1 si une suppression a été opérée et 0 dans le cas inverse
int st_delete_safe(st_table *table, st_data_t *key, st_data_t *value, st_data_t never) Une version similaire mais plus sûre (permettant de ne pas interférer avec des parcours concurrents sur le hash) à la fonction st_delete puisqu'elle marque l'élément à supprimer en remplaçant la clé et la valeur associée par la valeur donnée par le paramètre never (généralement Qundef)
int st_foreach(st_table *table, int (*func)(), st_data_t arg) Appliquer la fonction func avec le paramètre arg sur les éléments de table. Elle renverra 0 ou 1 si celle-ci a été modifiée pendant son parcours. Son emploi est expliqué ci-dessous
void st_foreach_safe(st_table *table, int (*func)(), st_data_t arg) Appliquer la fonction func avec le paramètre arg sur les éléments de table. Il s'agit d'une version intermédiaire à st_foreach car elle lèvera automatiquement une exception RuntimeError si la table du hash a été modifiée pendant son parcours
void st_free_table(st_table *table) Libérer la mémoire utilisée par la table table
st_table *st_init_numtable(void) Crée et retourne une table prévue pour accueillir des nombres. La capacité initiale sera celle par défaut (11)
st_table *st_init_numtable_with_size(int size) Crée et retourne une table prévue pour accueillir des nombres, la capacité initiale sera définie à size
st_table *st_init_strtable(void) Crée et retourne une table prévue pour accueillir des chaînes. La capacité initiale sera celle par défaut (11)
st_table *st_init_strtable_with_size(int size) Crée et retourne une table prévue pour accueillir des chaînes, la capacité initiale sera de size éléments
st_table *st_init_table(struct st_hash_type *type) Crée et retourne une table qui utilisera des fonctions spécifiques de comparaison et de hachage pour ses éléments. Elles correspondent à la définition d'une structure st_hash_type :
struct st_hash_type {
    /* Fonction de comparaison de deux éléments */
    int (*compare)();
    /* Fonction de hachage d'un élément */
    int (*hash)();
};
st_table *st_init_table_with_size(struct st_hash_type *type, int size) Semblable à st_init_table mais permet en plus de spécifier la capacité initiale de la table de hachage via le paramètre size
int st_insert(st_table *table, st_data_t key, st_data_t value) Insérer la clé key ou modifier sa valeur associée value à la table table
int st_lookup(st_table *table, st_data_t key, st_data_t *value) Savoir si la clé key est présente dans la table table (valeur 0 si inexistante et 1 dans le cas contraire). Si le paramètre value a une valeur différente de 0, alors vous pourrez obtenir la valeur correspondant à la clé (si celle-ci existe)

La fonction fournit à et appelée sur chaque élément par st_foreach doit retourner une valeur correspondant à une des constantes suivantes suivant l'action à effectuer :

  • ST_CONTINUE : poursuivre le parcours des éléments
  • ST_STOP : mettre fin au parcours du hachage
  • ST_DELETE : supprimer le dernier élément et poursuivre le parcours de la table
  • ST_CHECK : vérifier que l'élément n'a pas été modifié

Exemple

Voici un exemple ajoutant une méthode safe_invert à la classe Hash. Elle ressemble fortement à la méthode invert qu'implémente de base la classe Hash, mais elle s'assure en cas de doublons parmi les valeurs de créer un tableau des clés correspondantes au lieu de les écraser. Cette source met en oeuvre le parcours de la table via la fonction st_lookup et sa fonction auxiliaire :

#include <ruby.h>
#include <st.h>
 
static int safe_invert_i(VALUE key, VALUE val, VALUE hash)
{
    if (key != Qundef) {
        VALUE v;
 
        if (st_lookup(RHASH(hash)->tbl, val, &v)) {
            if (TYPE(v) == T_ARRAY) {
                rb_ary_push(v, key);
            } else {
                VALUE ary = rb_ary_new3(2, v, key);
                rb_hash_aset(hash, val, ary);
            }
        } else {
            rb_hash_aset(hash, val, key);
        }
    }
 
    return ST_CONTINUE;
}
 
/*
 *  call-seq:
 *     hash.safe_invert  -> an_other_hash
 *
 *  Inverse les clés et les valeurs (les valeurs deviennent
 *  les clés du hash et les clés, les valeurs). S'il existe
 *  plusieurs valeurs identiques pour des clés différentes,
 *  les clés seront regroupées au sein d'un tableau.
 *
 *     h = { "n" => 100, "m" => 100, "y" => 300, "d" => 200, "a" => 0 }
 *     h.safe_invert   #=> { 0 => "a", 100 => ["n", "m"], 200 => "d", 300 => "y" }
 */
static VALUE rb_hash_safe_invert(VALUE hash)
{
    VALUE inv;
 
    inv = rb_hash_new();
    rb_hash_foreach(hash, safe_invert_i, (st_data_t) inv);
 
    return inv;
}
 
void Init_hash()
{
    rb_define_method(rb_cHash, "safe_invert", rb_hash_safe_invert, 0);
}

Tester le type d'un objet

Les types de base

À tous les types de base en Ruby sont associés une constante qui le définit. La liste des ces constantes est la suivante :

Constante Classe correspondante Ruby Classe correspondante C (variable globale externe)
T_ARRAY Array rb_cArray
T_BIGNUM Bignum rb_cBignum
T_CLASS Class rb_cClass
T_FALSE False rb_cFalseClass
T_FILE File rb_cFile
T_FIXNUM Fixnum rb_cFixnum
T_FLOAT Float rb_cFloat
T_HASH Hash rb_cHash
T_MODULE Module rb_cModule
T_NIL nil rb_cNilClass
T_OBJECT Object rb_cObject
T_REGEXP Regexp rb_cRegexp
T_STRING String rb_cString
T_STRUCT Struct rb_cStruct
T_SYMBOL Symbol rb_cSymbol
T_TRUE True rb_cTrueClass

On peut ainsi effectuer des contrôles sur le ou les types des différents objets qu'on attend. La fonction rb_check_type, propose ce genre de vérification et provoque une exception TypeError si l'objet testé ne se conforme pas au type donné :

void Check_Type(VALUE obj, int type); /* Alias pour rb_check_type sous forme de macro */
void rb_check_type(VALUE obj, int type);

type, est l'une des constantes donné dans le tableau ci-dessus. Cette fonction ne correspond pas toujours à nos besoins, tout d'abord parce qu'elle lève d'emblais une erreur et d'autre part elle ne permet pas de couvrir plusieurs types, cas où on attendrait un objet numérique (Fixnum ou Float au choix), par exemple. Dans ce cas, pour vous adapter au type d'un objet vous pouvez utiliser la macro TYPE :

int TYPE(VALUE obj);

On peut ainsi écrire une fonction similaire à rb_check_type qui serait un peu plus souple :

#include <stdarg.h>
 
int rb_check_types(VALUE obj, long n, ...)
{
    va_list ar;
    long i;
    int type, ret;
 
    ret = 0;
    type = TYPE(obj);
    va_start(ar, n);
    for (i = 0; i < n && !ret; i++) {
        if (type == va_arg(ar, int)) {
            ret = 1;
        }
    }
    va_end(ar);
 
    return ret;
}

Il existe quelques macros pour tester plus rapidement si un objet est nil, un Fixnum ou un Symbol. Elles sont donc limitées au nombre de trois :

int NIL_P(VALUE obj);
int FIXNUM_P(VALUE obj);
int SYMBOL_P(VALUE obj);

Comparaisons niveau objet et lien de parenté (is_a?)

Le recours à la fonction rb_obj_is_kind_of nous permet d'affirmer qu'un objet est d'une classe précise ou éventuellement de l'une de ses filles.

VALUE rb_obj_is_kind_of(VALUE obj, VALUE class);

Illustration par l'exemple :

rb_obj_is_kind_of(rb_ary_new2(0), rb_cHash);   /* => Qfalse : un tableau n'est pas un Hash */
rb_obj_is_kind_of(rb_ary_new2(0), rb_cArray);  /* => Qtrue : ça se passe de commentaire */
rb_obj_is_kind_of(rb_ary_new2(0), rb_cObject); /* => Qtrue : un tableau est un objet */

Au lieu de tester un objet par rapport à une classe, il est possible via rb_class_inherited_p, de savoir si deux classes (objets Class) ou modules (classes Module) sont apparentés (marqué par la valeur retournée, Qtrue et Qfalse ou Qnil).

VALUE rb_class_inherited_p(VALUE mod, VALUE arg);

Voici quelques exemples :

rb_class_inherited_p(rb_eEOFError, rb_eIOError);       /* => Qtrue : IOError est la classe mère de EOFError */
rb_class_inherited_p(rb_eRangeError, rb_eScriptError); /* => Qnil : ces deux classes n'ont aucun lien de parenté */
rb_class_inherited_p(rb_eScriptError, rb_eLoadError);  /* => Qfalse : LoadError est une classe fille et non mère de ScriptError */
rb_class_inherited_p(rb_eException, rb_cObject);       /* => Qtrue : une Exception est un Object */
rb_class_inherited_p(rb_mKernel, rb_mKernel);          /* => Qtrue : nécessairement vrai */

Tester si un objet fournit une méthode (duck typing, respond_to?)

Pour savoir si un objet (sa classe plutôt) implémente une méthode bien définie, on fait ce test via la fonction rb_respond_to, que voici :

int rb_respond_to(VALUE obj, ID id);

Il s'agit du principe de duck typing. Remarquez que cette fonction attend un deuxième paramètre de type ID. Celui-ci correspond au symbole faisant référence à la méthode. Pour convertir le nom d'une méthode (chaîne C) en une variable de type ID on utilise la fonction rb_intern :

/* Nom de la méthode => ID */
ID rb_intern(const char *name);

Ci-dessous une application consistant à ajouter une méthode globale nommée concatener_successeur qui vous retourne une chaîne résultant de la concaténation, après convertion, de l'objet qui est passé en argument et de son successeur, que l'on obtient grâce à la méthode succ si celle-ci est disponible :

/*
 *  call-seq:
 *     concatener_successeur(object)  -> string
 *
 *  Retourne une chaîne qui est la concaténation de l'objet
 *  donné en paramètre et de son successeur. Une exception
 *  de type ArgumentError sera provoquée si cet objet ne
 *  dispose pas d'une méthode succ.
 *
 *     concatener_successeur(4)         #=> "45"
 *     concatener_successeur(Exception) #=> ArgumentError
 *     concatener_successeur("az")      #=> "azba"
 *     concatener_successeur(Math::PI)  #=> ArgumentError
 */
static VALUE rb_concatener_successeur(VALUE obj, VALUE v)
{
    ID id_succ;
    VALUE ret;
 
    (void) obj;
    ret = Qnil;
    id_succ = rb_intern("succ");
    if (rb_respond_to(v, id_succ)) {
        ret = rb_str_plus(rb_String(v), rb_String(rb_funcall(v, id_succ, 0)));
    } else {
        rb_raise(rb_eArgError, "L'objet passé en argument (de classe '%s') n'implémente pas de méthode succ", rb_obj_classname(v));
    }
 
    return ret;
}
 
void Init_test() {
    rb_define_global_function("concatener_successeur", rb_concatener_successeur, 1);
}

Puisque nous avons utilisé lors de l'exemple la fonction rb_funcall afin d'appeler une méthode sur un objet, le moment est venu de voir comment appeler une fonction ou une méthode Ruby en C. Vous avez trois fonctions plus ou moins identiques à votre disposition pour cela, à savoir :

VALUE rb_funcall(VALUE recv, ID mid, int n, ...);
VALUE rb_funcall2(VALUE recv, ID mid, int argc, const VALUE *argv);
VALUE rb_funcall3(VALUE recv, ID mid, int argc, const VALUE *argv);

Où recv est l'objet ou le module sur lequel sera appelée la méthode ou la fonction désignée par mid (il faudra faire la conversion du nom vers un ID à l'aide de rb_intern, présentée plus haut). Les autres paramètres sont liés aux arguments de la fonction ou méthode à appeler.

Définir de nouveaux éléments

Les variables globales

Débutons par la récupération de la valeur d'une variable globale Ruby à laquelle il est possible de répondre par la fonction rb_gv_get :

VALUE rb_gv_get(const char *name)

Son seul paramètre, name, correspond au nom de la variable souhaitée.

En ce qui concerne la création d'une telle variable, cela se complique quelque peu car plusieurs fonctions vous sont proposées et chacune d'entre elles introduit des limitations dans son utilisation :

Prototype Description
VALUE rb_gv_set(const char *name, VALUE val) Créer ou modifier une variable globale nommée name pour la valeur val (la valeur qu'elle retourne)
void rb_define_variable(const char *name, VALUE *var) Créer une variable globale name associée à l'objet Ruby désigné par var (pointeur sur une VALUE)
void rb_define_readonly_variable(const char *name, VALUE *var) Acton identique à la fonction rb_define_variable mais la variable ainsi définie sera en lecture seule
void rb_define_hooked_variable(const char *name, VALUE *var, VALUE (*getter)(ID, VALUE *), void (*setter)(VALUE, ID, VALUE *)) Identique à la fonction rb_define_variable sauf que l'accès à cette variable et sa modification seront respectivement redirigées vers les fonctions de rappel indiquées par les paramètres getter et setter
void rb_define_virtual_variable(const char *name, VALUE (*getter)(ID), void (*setter)(VALUE, ID)) Créer une variable globale virtuelle appelée <i>name</i>, c'est à dire qui n'a pas d'existence physique (pas de variable C de type VALUE associée). L'obtention de sa valeur est gérée par la fonction getter et sa modification par la fonction setter

Mettons en application ces fonctions autour d'un cas pratique afin d'identifier les différences que chacune entraîne :

  • La façon la plus simple de créer une variable globale est de la déclarer via la fonction rb_gv_set :
    #include <ruby.h>
     
    void Init_uid()
    {
        VALUE rb_mProcID_Syscall;
     
        rb_mProcID_Syscall = rb_path2class("Process::Sys");
        rb_gv_set("$UID", rb_funcall(rb_mProcID_Syscall, rb_intern("getuid"), 0));
    }

    Mais comme le prouve les tests suivants, on peut faire n'importe de quoi de la variable ainsi déclarée ce qui n'est pas toujours souhaitable :

    $UID
    => 1001
     
    $UID = 'root'
    => "root"
  • La fonction rb_define_variable ressemble fortement à la première, rb_gv_set. Sa seule différence par rapport à cette dernière est son deuxième paramètre qui n'est plus de type VALUE mais un pointeur sur ce type :
    #include <ruby.h>
     
    static VALUE rb_uid;
     
    void Init_uid()
    {
        VALUE rb_mProcID_Syscall;
     
        rb_mProcID_Syscall = rb_path2class("Process::Sys");
        rb_uid = rb_funcall(rb_mProcID_Syscall, rb_intern("getuid"), 0);
        rb_define_variable("$UID", &rb_uid);
    }

    Comme je le disais, son comportement est strictement identique à la fonction rb_gv_set puisque sa valeur peut être indifféremment modifiée de la même manière :

    $UID
    => 1001
     
    $UID = 'root'
    => "root"
  • À partir d'ici, nous allons voir qu'il est possible de restreindre, en quelque sorte, les accès à cette variable. La plus forte de toute est la fonction rb_define_readonly_variable qui a pour effet de rendre la variable globale ainsi créée inaltérable à partir de Ruby. On pourrait donc comparer, conceptuellement, une telle variable à une constante :
    #include <ruby.h>
     
    static VALUE rb_uid;
     
    void Init_uid()
    {
        VALUE rb_mProcID_Syscall;
     
        rb_mProcID_Syscall = rb_path2class("Process::Sys");
        rb_uid = rb_funcall(rb_mProcID_Syscall, rb_intern("getuid"), 0);
        rb_define_readonly_variable("$UID", &rb_uid);
    }

    Dès que vous chercherez à la modifier vous déclencherez une exception (sans pour autant modifier la valeur de la variable désirée) :

    $UID
    => 1001
     
    $UID = 'root'
    NameError: $UID is a read-only variable
  • À des fins de contrôle sur la valeur comme de connaître un état en temps réel, il est intéressant de pouvoir gérer soi-même l'accés à la variable (l'obtention de sa valeur) ou sa modification (affectation d'une valeur). C'est ce que propose la fonction rb_define_hooked_variable à l'aide de ses paramètres getter et setter où nous allons pouvoir donner un sens à notre variable $UID :
    #include <ruby.h>
     
    static VALUE rb_uid;
    static VALUE rb_mProcID_Syscall;
     
    static VALUE rb_uid_get(ID id, VALUE *var)
    {
        *var = rb_funcall(rb_mProcID_Syscall, rb_intern("getuid"), 0);
        rb_warn("[%s] rb_uid_get, current uid is: %d", rb_id2name(id), FIX2INT(*var));
     
        return *var;
    }
     
    static void rb_uid_set(VALUE val, ID id, VALUE *var)
    {
        rb_funcall(rb_mProcID_Syscall, rb_intern("setuid"), 1, val);
        rb_warn("[%s] rb_uid_set, setting uid to: %d", rb_id2name(id), FIX2INT(val));
        *var = val;
    }
     
    void Init_uid()
    {
        VALUE rb_mProcID_Syscall;
     
        rb_mProcID_Syscall = rb_path2class("Process::Sys");
        rb_uid = rb_funcall(rb_mProcID_Syscall, rb_intern("getuid"), 0);
        rb_define_hooked_variable("$UID", &rb_uid, rb_uid_get, rb_uid_set);
    }

    En effectuant des tests comme ceux ci-dessous, vous verrez que la variable n'est plus modifiable à souhait (bien que dans l'exemple ceci est géré par la fonction Process::Sys::setuid) et que l'identifiant de l'utilisateur courant du processus Ruby est modifié et obtenu en temps réel grâce aux fonctions faisant office d'accesseur et de mutateur :

    $UID
    => 0
     
    %x{whoami}
    => "root\n"
     
    $UID = 'julp'
    # TypeError: can't convert String into Integer
    # Normal, la fonction Process::Sys::setuid attend un nombre et non une chaîne
     
    $UID = 1001
    => 1001
     
    %x{whoami}
    => "julp\n"

    Les fonctions getter et setter ne sont pas obligatoires, ainsi l'attributation de la valeur NULL leur affectera des fonctions par défaut.
    Passons-nous dans un premier temps de la partie accesseur en remplaçant notre précédent appel à la fonction rb_define_hooked_variable par celui-ci :

    rb_define_hooked_variable("$UID", &rb_uid, NULL, rb_uid_set);

    Voilà ce qui se passe lorsque l'on tente d'en obtenir la valeur et de la modifier :

    $UID
    => 0
     
    %x{whoami}
    => "root\n"
     
    $UID = 1001
    => 1001
     
    $UID
    => 1001
     
    %x{whoami}
    => "julp\n"

    Le comportement semble inchangé alors que nous n'avons pas fourni de fonction getter. Pourquoi ? Cela vient tout simplement de l'instruction : *var = val; à la fin de la fonction setter rb_uid_set qui attribue, dès lors que l'on modifie avec succès l'identifiant utilisateur du processus courant avec la fonction Ruby setuid, l'identifiant demandé. Si vous supprimez l'instruction en question, le comportement de la variable UID en sera modifié puisqu'elle vous renverrait alors sa valeur initiale, correspondant à l'identifiant de l'utilisateur du processus Ruby au moment où l'inclusion de l'extension a eu lieu (avec require).
    Voyons maintenant comment réagit la variable globale $UID que nous avons créé à nos différentes interventions en l'absence de la fonction setter, mise à son tour à la valeur NULL :

    rb_define_hooked_variable("$UID", &rb_uid, rb_uid_get, NULL);

    On peut clairement observer que la valeur de la variable globale n'est jamais affectée par nos tentatives de modification, elle reste à sa valeur initiale (on revient à une variable en lecture seule bien qu'ici son accès est délégué à une fonction) :

    $UID
    => 0
     
    %x{whoami}
    => "root\n"
     
    $UID = 1001
    => 1001
     
    $UID
    => 0
     
    %x{whoami}
    => "root\n"
     
    Process::Sys::setuid(1001)
    => nil # C'est bien la valeur qu'elle renvoie
     
    $UID
    => 1001
     
    %x{whoami}
    => "julp\n"
  • La fonction rb_define_virtual_variable, dans l'idée est strictement identique à rb_define_hooked_variable mis à part que n'étant plus associée à aucun objet Ruby (VALUE) tout passe et doit passer par les fonctions de rappel getter et setter. D'où l'appellation virtuelle :
    #include <ruby.h>
     
    static VALUE rb_mProcID_Syscall;
     
    static VALUE rb_uid_get(ID id)
    {
        VALUE val;
     
        val = rb_funcall(rb_mProcID_Syscall, rb_intern("getuid"), 0);
        rb_warn("[%s] rb_uid_get, current uid is: %d %s", rb_id2name(id), FIX2INT(val));
     
        return val;
    }
     
    static void rb_uid_set(VALUE val, ID id)
    {
        rb_funcall(rb_mProcID_Syscall, rb_intern("setuid"), 1, val);
        rb_warn("[%s] rb_uid_set, setting uid to: %d", rb_id2name(id), FIX2INT(val));
    }
     
    void Init_uid()
    {
        rb_mProcID_Syscall = rb_path2class("Process::Sys");
        rb_define_virtual_variable("$UID", rb_uid_get, rb_uid_set);
    }

    Notre variable globale, même si elle est virtuelle, se comporte comme attendu :

    $UID
    => 0
     
    %x{whoami}
    => "root\n"
     
    $UID = 'julp'
    # TypeError: can't convert String into Integer
    # Normal, la fonction Process::Sys::setuid attend un nombre et non une chaîne
     
    $UID = 1001
    => 1001
     
    %x{whoami}
    => "julp\n"

    Comme le montre le résultat de la session IRB suivante, le recours à cette fonction sans définir une fonction de rappel pour l'obtention de la valeur de la variable n'a aucun intérêt puisqu'elle renvoie en son absence constamment la valeur false :

    $UID
    => false
     
    %x{whoami}
    => "root\n"
     
    $UID = 1001
    => 1001
     
    $UID
    => false
     
    %x{whoami}
    => "julp\n"

    Comme vous pouvez le constater, sans fonction de rappel pour la modification de la variable, la fonction rb_define_virtual_variable se comporte exactement de la même manière que rb_define_hooked_variable :

    $UID
    => 0
     
    %x{whoami}
    => "root\n"
     
    $UID = 1001
    # NameError: $UID is a read-only variable
     
    Process::Sys::setuid(1001)
    => nil # C'est bien la valeur qu'elle renvoie
     
    $UID
    => 1001
     
    %x{whoami}
    => "julp\n"

Vous pourriez avoir besoin de déterminer si une variable globale existe et ce à partir de son nom, c'est pourquoi je vous propose une telle fonction :

/* Le caractère dollar ($) sera requis ici pour le paramètre name !!! */
 
VALUE rb_gv_defined(const char *name)
{
    struct global_entry *entry;
 
    entry = rb_global_entry(rb_intern(name));
 
    return rb_gvar_defined(entry);
}

Avant d'achever la partie vouée aux variables globales, je vais évoquer la possibilité d'"aliaser" vos variables. Vous pouvez le faire à partir de leurs identifiants avec la fonction <i>rb_alias_variable</i> (son premier paramètre correspond à l'alias et le deuxième à l'ID de la variable originale) :

void rb_alias_variable(ID id1, ID id2)

Une version de la fonction <i>rb_alias_variable</i> attendant des noms plutôt que des identifiants est possible :

/* Le caractère dollar ($) sera requis ici pour les paramètres name1 et name2 !!! */
 
void rb_gv_alias_by_name(const char *name1, const char *name2)
{
    rb_alias_variable(rb_intern(name1), rb_intern(name2));
}

Application : pour disposer d'une variable $uid faisant référence à $UID, que nous avons précédemment utilisée, afin de l'utiliser indifféremment par rapport à la casse employée :

rb_gv_alias_by_name("$uid", "$UID");

Les modules

Les modules sont très usités car ils servent aussi de point de départ dans l'écriture d'une extension afin de définir un espace de noms et ainsi éviter tout conflit au niveau des noms.

Définir un nouveau module

Un module peut être ajouté sans espace de nom, à la racine des classes et modules Ruby. Cela se fait avec la fonction rb_define_module :

VALUE rb_define_module(const char *name);

Pour en revanche créer un module sous un espace de nom, il faut employer la fonction alternative rb_define_module_under :

VALUE rb_define_module_under(VALUE outer, const char *name);

Petit exemple où sont créés trois modules : Linux et BSD ayant tout pour "parent" le module Unix :

VALUE rb_mUnix = rb_define_module("Unix");
VALUE rb_mLinux = rb_define_module_under(rb_mUnix, "Linux");
VALUE rb_mBSD = rb_define_module_under(rb_mUnix, "BSD");

À ce stade les modules ne comprennent rien (ni fonctions ni constantes). Lors des parties suivantes nous verrons comment leur ajouter ces éléments.

Les constantes

Ajouter une constante se fait soit à partir de son nom, donc sous la forme d'une chaîne de caractère C à l'aide de la fonction rb_define_const :

void rb_define_const(VALUE klass, const char *name, VALUE val)

Soit à l'aide de son identifiant interne via la fonction rb_const_set que voici :

void rb_const_set(VALUE klass, ID id, VALUE val)

Info : Lorsque vous cherchez à modifier la valeur d'une constante existante, un warning sera automatiquement généré. Vous avez à votre disposition (voir ci-dessous) des fonctions pour déterminer de manière avancée leur présence ou non.

Pour créer une constante considérée comme globale, au sens où elle est rattachée au module Kernel, utilisez la fonction rb_define_global_const dont voici le prototype :

void rb_define_global_const(const char *name, VALUE val)

Il est pour ainsi dire identique à celui de la fonction rb_const_set hormis le fait que le paramètre klass a disparu puisque la constante, lors de l'appel à rb_define_global_const, finit dans la classe Object. Par conséquent, les deux instructions suivantes sont strictement équivalentes :

rb_define_global_const("UNE_CONSTANTE", val);
/* Equivaut à */
rb_define_const(rb_cObject, "UNE_CONSTANTE", val);

L'obtention de la valeur courante d'une constante peut être fait par trois fonctions agissant différemment. Elles incluent ou excluent la classe (ou le module) de base ou ses classes filles (héritage). La valeur retournée sera la valeur courante de la constante si celle-ci existe sinon elle dépendra de l'appel à la méthode spéciale const_missing (si implémentée et sera Qnil en dernier ressort).

Prototype Description
VALUE rb_const_get_from(VALUE klass, ID id) Obtenir la valeur de la constante répondant à l'identifiant id parmi les classes filles de klass (excluant celle-ci)
VALUE rb_const_get(VALUE klass, ID id) Récupérer la valeur de la constante correspondant à l'identifiant id dans la classe klass ou ses filles
VALUE rb_const_get_at(VALUE klass, ID id) Acquérir la valeur de la constante désignée par l'identifiant id dans la classe klass (les classes filles ne seront pas parcourues)

Pour savoir si une constante a été définie vous avez à votre disposition trois fonctions différentes qui permettent d'inclure ou non la classe (ou module) de base ou ses classes descendantes (héritage). Une valeur nulle retournée indique l'absence de la constante testée.

Prototype Description
int rb_const_defined_from(VALUE klass, ID id) Cherche la constante correspondant à l'identifiant id parmi les classes filles de klass (excluant celle-ci)
int rb_const_defined(VALUE klass, ID id) Cherche la constante coïncidant à l'identifiant id dans la classe klass ou ses filles
int rb_const_defined_at(VALUE klass, ID id) Détermine si la constante désignée par l'identifiant id a été définie dans la classe klass (les classes filles ne seront pas parcourues)

Les fonctions (méthodes de classe du module Kernel)

La fonction rb_define_module_function ajoute une fonction Ruby appelée name au module module :

void rb_define_module_function(VALUE module, const char *name, VALUE (*func)(), int argc);

C'est la fonction C func qui sera en réalité appelée lors de son utilisation dans un script Ruby. Le paramètre argc indique le nombre de paramètre attendus à la fois par la fonction Ruby (déclenchant ainsi une erreur de type ArgumentError lorsque ce contrat n'est pas respecté) et par la fonction C qui définit en grande partie son prototype :

Valeur de argc Nombre de paramètres attendus Prototype de la fonction C
-2 "libre" VALUE rb_ma_fonction(VALUE module, VALUE args)
Le paramètre args est un tableau Ruby constitué des paramètres de la fonction (donc manipunable avec les fonction rb_ary_*)
-1 "libre" VALUE rb_ma_fonction(int argc, VALUE *argv, VALUE module)
Le paramètre argv est un tableau C constitué des argc paramètres qui lui ont été passés
0 aucun VALUE rb_ma_fonction(VALUE module)
1 un seul et unique VALUE rb_ma_fonction(VALUE module, VALUE arg1)
2 exactement deux VALUE rb_ma_fonction(VALUE module, VALUE arg1, VALUE arg2)
3 trois VALUE rb_ma_fonction(VALUE module, VALUE arg1, VALUE arg2, VALUE arg3)
15 quinze VALUE rb_my_func(VALUE module, VALUE arg1, VALUE arg2, VALUE arg3, VALUE arg4, VALUE arg5, VALUE arg6, VALUE arg7, VALUE arg8, VALUE arg9, VALUE arg10, VALUE arg11, VALUE arg12, VALUE arg13, VALUE arg14, VALUE arg15)

Toute autre valeur pour le paramètre argc, sera considérée comme invalide générant ainsi une erreur de type bug et mettant aussitôt fin à l'exécution de l'interpréteur.

Le paramètre nommé module (par convention) désigne l'objet module auquel la fonction est associée.

Dans le cas particulier où argc est égal à -1, vous pouvez utiliser la fonction rb_scan_args afin de vérifier et obtenir les différents paramètres. Son prototype est le suivant :

int rb_scan_args(int argc, const VALUE *argv, const char *format, ...);

Ses paramètres argc et argv sont ceux que vous avez obtenu de votre fonction C. La chaîne format, dont je vais détailler plus bas l'utilisation, indique le nombre et la forme des VALUE que vous allez récupérer. Tous les paramètres supplémentaires sont des pointeurs de type VALUE.

La chaîne format attend dans l'ordre : un chiffre pour le nombre de paramètres obligatoires, un chiffre pour le nombre de paramètres optionnels, un opérateur de splat (l'astérisque) où la variable correspondante sera un tableau Ruby accueillant tous les paramètres supplémentaires inutilisés, un et commercial pour obtenir le bloc. Si le nombre de paramètres obligatoires n'est pas respecté, cette fonction se chargera elle même de provoquer l'erreur adéquate (ArgumentError). Quelques exemples d'utilisation :

  • Bien que cela ne présente pas d'intérêt, limiter le nombre de paramètres à exactement trois (ni plus ni moins) :
    void rb_exactement_trois(int argc, VALUE *argv, VALUE module) {
        VALUE arg1, arg2, arg3; /* arg1 : la valeur du premier paramètre, arg2 du deuxième, ... */
     
        rb_scan_args(argc, argv, "3", &arg1, &arg2, &arg3);
     
        return rb_ary_join(rb_ary_new3(3, arg1, arg2, arg3), rb_str_new2(","));
    }
  • Au moins un paramètre et construire un tableau contenant tous les autres :
    VALUE rb_chown(int argc, VALUE *argv, VALUE module) {
        VALUE utilisateur, fichiers; /* utilisateur : premier argument, fichiers : le restant */
     
        rb_scan_args(argc, argv, "1*", &utilisateur, &fichiers);
        if (RARRAY(fichiers)->len == 0) {
            rb_warn("Le tableau de fichiers est vide");
        }
        rb_str_cat2(utilisateur, ": ");
     
        return rb_str_plus(utilisateur, rb_ary_join(fichiers, rb_str_new2(",")));
    }

    Note : Le tableau sera créé vide s'il ne reste plus assez de paramètres.

  • Au moins un paramètre et éventuellement deux optionnels, ce qui signifie qu'au minimum nous aurons un paramètre et au maximum trois :
    VALUE rb_de_un_a_trois(int argc, VALUE *argv, VALUE module) {
        VALUE arg1, arg2, arg3;
     
        rb_scan_args(argc, argv, "12", &arg1, &arg2, &arg3);
     
        return rb_ary_join(rb_ary_new3(3, arg1, arg2, arg3), rb_str_new2(","));
    }

    Note : Les paramètres facultatifs (ici arg2 et arg3) auront la valeur Qnil par défaut (s'ils ne sont pas fournis).

  • Il est possible d'associer paramètres facultatifs et un paramètre de type splat à la condition que le nombre de paramètres facultatifs figure avant l'astérisque, comme ci-dessous :
    VALUE rb_au_moins_un_plus_deux_facultatifs(int argc, VALUE *argv, VALUE module) {
        VALUE arg1, arg2, arg3, rest;
        VALUE sprintf_args[5];
     
        rb_scan_args(argc, argv, "12*", &arg1, &arg2, &arg3, &rest);
     
        sprintf_args[0] = rb_str_new2("Arg1 : %s, arg2 : %s, arg3 : %s, reste : %p");
        sprintf_args[1] = arg1;
        sprintf_args[2] = arg2;
        sprintf_args[3] = arg3;
        sprintf_args[4] = rest;
     
        return rb_f_sprintf(5, sprintf_args);
    }

    Comme cela était déjà le cas auparavant, les arguments optionnels s'ils ne sont pas fournis se trouveront affectés à la valeur par défaut qui est Qnil et pour celui qui accueille le restant (splat), un tableau vide.

  • Pour un paramètre minimum obligatoire, ceux restants étant récupérés dans un tableau, et un bloc optionnel :
    VALUE rb_chmod_avec_tri_optionnel(int argc, VALUE *argv, VALUE module) {
        VALUE utilisateur, fichiers, bloc;
        VALUE sprintf_args[4];
     
        rb_scan_args(argc, argv, "1*&", &utilisateur, &fichiers, &bloc);
     
        /* On trie les fichiers à l'aide du bloc donné */
        if (bloc != Qnil) {
            rb_ary_sort_bang(fichiers);
        }
     
        sprintf_args[0] = rb_str_new2("utilisateur : %s, fichiers : %p, bloc : %p");
        sprintf_args[1] = utilisateur;
        sprintf_args[2] = fichiers;
        sprintf_args[3] = bloc;
     
        return rb_f_sprintf(4, sprintf_args);
    }

    Le bloc suit les mêmes principes que les paramètres facultatifs puisqu'il se verra affecter la valeur Qnil si aucun n'a été fournit à la fonction lors de son appel.

Finalement, la fonction rb_scan_args reprend les règles du langage Ruby au niveau des paramètres de fonction (ou de méthodes) : les paramètres obligatoires en premier puis ceux qui sont optionnels, les arguments regroupés (splat) et enfin le bloc (esperluette).

Pour créer une fonction qui sera considérée comme globale (méthode de classe du module Kernel), utilisez la fonction rb_define_global_function :

void rb_define_global_function(const char *name, VALUE(*func)(), int argc)

Remarquez que la liste des paramètres est identique à celle de la fonction rb_define_module_function vue ci-dessus à la différence près que le module n'y figure plus car toute fonction globale figure dans le module appelé Kernel. Par conséquent les deux instructions suivantes sont identiques :

rb_define_global_function("MaFonction", rb_ma_fonction, 0);
/* Equivaut à */
rb_define_module_function(rb_mKernel, "MaFonction", rb_ma_fonction, 0);

Les classes

Définir une nouvelle classe

Selon l'emplacement souhaité de la classe à ajouter, vous avez à votre disposition deux fonctions différentes. La première rb_define_class définit la classe à la racine (sans espace de nom) :

VALUE rb_define_class(const char *name, VALUE super)

Soit la déclaration suivante :

VALUE rb_cClasseVide = rb_define_class("ClasseVide", rb_cObject);

Le code Ruby suivant permettant de l'utiliser :

a = ClasseVide.new

Alors qu'avec la deuxième, rb_define_class_under, la classe sera contenue dans un autre élément Ruby (généralement un module), ce qui implique un espace de nom :

VALUE rb_define_class_under(VALUE outer, const char *name, VALUE super)

Soit les déclarations suivantes :

VALUE rb_mLinux = rb_define_module("Linux");
VALUE rb_cQuota = rb_define_class_under(rb_mLinux, "Quota", rb_cObject);

Par contre ici pour créer un objet Quota, il faudra indiquer le nom complet de la classe :

q = Linux::Quota.new

Ou inclure son conteneur avant son utilisation :

include Linux
 
q = Quota.new

Pour décrire les liens de parenté (héritage) entre classes, recourez au paramètre super de ces fonctions. Voyons un exemple trivial où il est question de représenter des formes géométriques. La représentation UML des quelques classes illustratives est la suivante :

La définition de ces classes se feraient donc comme suit :

VALUE rb_cForme = rb_define_class("Forme", rb_cObject);
VALUE rb_cTriangle = rb_define_class("Triangle", rb_cForme);
VALUE rb_cRectangle = rb_define_class("Rectangle", rb_cForme);
VALUE rb_cCarre = rb_define_class("Carre", rb_cRectangle);

Notez que toute classe en hérite au moins d'une autre et qu'elles sont toutes filles, de manière directe ou non, de la classe Object. La classe Forme de l'exemple précédent n'avait, a priori, aucun parent évident ou explicite c'est pourquoi le paramètre super prend la valeur rb_cObject (variable globale désignant la classe Object).

Les constantes

Reportez-vous à la partie précédente qui est consacrée aux modules car les constantes peuvent être indifféremment intégrées à une classe ou à un module (qui sont des classes en fin de compte).

Les méthodes

TODO

D'instance :

  • méthode publique : fonction rb_define_method
  • méthode protégée : fonction rb_define_protected_method
  • méthode privée : fonction rb_define_private_method

Les prototypes, bien que similaires au niveau des paramètres, sont les suivants :

/* Public */
void rb_define_method(VALUE klass, const char *name, VALUE(*func)(), int argc)
 
/* Protected */
void rb_define_protected_method(VALUE klass, const char *name, VALUE(*func)(), int argc)
 
/* Private */
void rb_define_private_method(VALUE klass, const char *name, VALUE(*func)(), int argc)

De classe (publique) :

void rb_define_singleton_method(VALUE object, const char *name, VALUE(*func)(), int argc)

TODO : Pour argc renvoyer aux modules/fonctions et préciser que le premier paramètre désigne la classe ?

Les variables d'instance (@)

On peut agir de deux façons différentes sur les variables d'instance d'un objet : à partir de leur nom (une chaîne C) ou de leur identifiant (de type ID).

Abordons dans un premier temps celles qui attendent un nom :

Prototype Description
VALUE rb_iv_get(VALUE obj, const char *name) Obtenir la valeur courante de la variable d'instance name pour l'objet obj. Si aucun attribut de ce nom n'est trouvé la valeur Qnil vous sera renvoyée
VALUE rb_iv_set(VALUE obj, const char *name, VALUE val) Fixer ou modifier la valeur courante de la variable d'instance name pour l'objet obj à la valeur val. Cette même valeur vous sera renvoyée

Attention : L'arobase dans le nom de la variable d'instance (paramètre name des fonctions concernées) ne doit pas être omis sous peine d'obtenir des résultats erronés du fait qu'elle sera introuvable ou désignera tout autre chose sans cela.

Et enfin celles qui y procédent avec un identifiant :

Prototype Description
VALUE rb_ivar_defined(VALUE obj, ID id) Retourne <i>Qtrue</i> si l'objet <i>obj</i> possède une variable d'instance correspondant à l'identifiant <i>id</i>, sinon elle renvoie <i>Qfalse
VALUE rb_ivar_get(VALUE obj, ID id) Récupérer la valeur courante de la variable d'instance concordant à l'identifiant id pour l'objet obj. Si un tel attribut n'existe pas, elle renverra la valeur Qnil et génèrera en plus un warning dans ce cas précis
VALUE rb_attr_get(VALUE obj, ID id) Identique à la fonction rb_ivar_get mais ne génère aucun warning si la variable d'instance désignée par l'identifiant id n'existe pas
VALUE rb_ivar_set(VALUE obj, ID id, VALUE val) Fixer ou modifier la valeur d'instance correspondant à l'identifiant id de l'objet obj à la valeur val

Si vous avez besoin de déterminer l'existence d'une variable d'instance à partir de son nom plutôt que de son id, vous pouvez écrire une fonction comme celle-ci réalisant la conversion :

VALUE rb_iv_defined(VALUE obj, const char *name)
{
    return rb_ivar_defined(obj, rb_intern(name));
}

TODO

/* Permettre l'accès en lecture/écriture */
void rb_define_attr(VALUE klass, const char *name, int read, int write)

TODO

/* Définir un alias pour une méthode */
void rb_define_alias(VALUE klass, const char *name1, const char *name2)

TODO : un exemple

/* ... */

Les variables de classe (@@)

Il y a deux méthodes pour manipuler une variable de classe : vous pouvez le faire à partir de son nom (une chaîne C) ou de son identifiant (type ID). Je rappelle que la fonction <i>ID rb_intern(const char *)</i> permert d'effectuer la conversion nom vers ID et <i>char *rb_id2name(ID)</i> l'opération inverse.

Voyons tout d'abord celles qui vous permettent d'utiliser le nom de la variable à traiter :

Prototype Description
void rb_define_class_variable(VALUE klass, const char *name, VALUE val) Définir à la classe désignée par klass une variable de classe appelée name ayant pour valeur initiale val
VALUE rb_cv_get(VALUE klass, const char *name) Obtenir la valeur courante de la variable de classe name de la classe klass. La valeur Qnil vous sera renvoyée dans le cas où celle-ci n'existe pas
void rb_cv_set(VALUE klass, const char *name, VALUE val) Modifier la valeur de la variable de classe name de la classe klass pour val

Attention : Vous devez impérativement faire figurer les deux arobases dans le nom car en leur absence votre extension aura un comportement indéterminé : cette variable ne vous sera pas accessible et vous obtiendriez alors des valeurs totalement erronées.

Puis celles qui attendent un id :

Prototype Description
VALUE rb_cvar_defined(VALUE klass, ID id) Permet, à partir de l'id d'une variable de classe si celle-ci existe (valeur Qtrue renvoyée) ou non (valeur Qfalse renvoyée) pour la classe klass
VALUE rb_cvar_get(VALUE klass, ID id) Obtenir la valeur courante de la variable de classe identifiée par id pour la classe klass. La valeur Qnil vous sera renvoyée si la variable de classe correspondante n'est pas définie actuellement
void rb_cvar_set(VALUE klass, ID id, VALUE val, int warn) Modifier la valeur de la variable de classe correspondant à l'identifiant id, de la classe klass pour la valeur val. Le paramètre warn indique si la fonction doit émettre un warning si la variable n'a pas encore été créée

Info : Les fonctions rb_define_class_variable, rb_cv_set et rb_cvar_set ont le même effet : si la variable de classe n'existe pas, elles la créeront toutes. En revanche, modifier une variable de classe existante avec la fonction rb_define_class_variable génèrera un warning.

Si vous aviez besoin de savoir si une variable de classe donnée est définie dans une classe à partir de son nom et non de son id, vous pouvez vous-mêmes écrire cette fonction de la sorte :

VALUE rb_cv_defined(VALUE klass, const char *name)
{
    ID id;
 
    id = rb_intern(name);
    if (!rb_is_class_id(id)) {
        rb_name_error(id, "wrong class variable name %s", name);
    }
 
    return rb_cvar_defined(klass, id);
}

Pour mettre en pratique ces fonctions, nous allons proposer une nouvelle classe TestCV limitée puisqu'elle ne se constitue que d'un constructeur qui, en interne, incrémentera une variable de classe nommée @@compteur. Il s'agit d'un compteur d'objets créés en somme :

#include <ruby.h>
 
VALUE rb_testcv_initialize(VALUE obj)
{
    VALUE curv;
 
    curv = rb_iv_get(CLASS_OF(obj), "@@compteur");
    if (curv != Qnil) {
        int newv;
 
        newv = FIX2INT(curv) + 1;
        rb_warn("Nombre d'objets '%s' créés : %d", rb_obj_classname(obj), newv);
        rb_iv_set(CLASS_OF(obj), "@@compteur", INT2FIX(newv));
    }
 
    return obj;
}
 
void Init_cv()
{
    VALUE rb_cTestCV;
 
    rb_cTestCV = rb_define_class("TestCV", rb_cObject);
    rb_define_method(rb_cTestCV, "initialize", rb_testcv_initialize, 0);
    rb_define_class_variable(rb_cTestCV, "@@compteur", INT2FIX(0));
}

Ce n'est qu'une manière de procéder car nous aurions aussi bien pu agir sur cette variable de classe à partir de son id avec les fonctions adéquates au lieu de son nom ici.

Contrôlons à présent le comportement de cette petite extension en situation réelle au sein d'une session IRB :

require 'cv'
=> true

t = TestCV.new
warning: Nombre d'objets 'TestCV' créés : 1
=> #<TestCV:0x805cf64>

t = TestCV.new
warning: Nombre d'objets 'TestCV' créés : 2
=> #<TestCV:0x81624e0>

TestCV.new
warning: Nombre d'objets 'TestCV' créés : 3
=> #<TestCV:0x815d378>

Concepts avancés

Les blocs

Un bloc a-t-il été fourni à ma méthode ? (block_given?)

La fonction rb_block_given_p dont vous trouverez ci-dessous le prototype, vous permet de savoir si un bloc est disponible pour votre fonction ou méthode :

int rb_block_given_p(void)

Elle renverra 0 (valeur de Qfalse) ou 2 (valeur de Qtrue) suivant qu'un bloc lui a été fourni ou non. Voici un exemple la mettant en œuvre :

#include <ruby.h>
#include <env.h>
 
static VALUE rb_my_method(VALUE self /*, ...*/)
{
    if (rb_block_given_p()) {
        rb_warning("Un block a été donné à %s", rb_id2name(ruby_frame->last_func));
        return Qtrue;
    } else {
        rb_warning("Aucun block n'a été donné à %s", rb_id2name(ruby_frame->last_func));
        return Qfalse;
    }
}

Info : Pas de panique si vous ne voyez pas les messages de la fonction rb_warning s'afficher sur votre écran : les messages ne s'afficheront uniquement si vous définissez la variable globale $VERBOSE à la valeur true ou si vous utilisez l'interpréteur ruby, vous avez ajouté l'option -v à la ligne de commande.

Cette fonction ne nous permet que de prendre connaissance du passage ou non d'un bloc à notre méthode. Vous apprendrez plus bas comment l'utiliser et l'exécuter (l'équivalent de yield en somme).

Exécuter et transmettre des valeurs au bloc (yield)

L'équivalent au mot clé Ruby yield sont trois fonctions en C dont les prototypes varient peu et permettent plus de souplesse au développeur suivant ses besoins. Celles-ci sont :

VALUE rb_yield(VALUE val);
VALUE rb_yield_splat(VALUE values);
VALUE rb_yield_values(int n, ...);

Comme exemple d'application nous allons ajouter une méthode find_all à la classe Hash afin de constituer un nouveau hash à partir d'un autre et qui contiendra toutes les clés/valeurs du premier pour lesquelles le bloc renverra vrai :

#include <ruby.h>
#include <st.h>
 
static int find_all_i(VALUE key, VALUE val, VALUE hash)
{
    if (key != Qundef) {
        /*
           On appelle le bloc sur chaque paire et on ajoute
           les clés/valeurs au nouveau hachage si ce bloc nous
           renvoie une valeur "vraie" (but de la macro RTEST)
        */
        if (RTEST(rb_yield_values(2, key, val))) {
            rb_hash_aset(hash, key, val);
        }
    }
 
    return ST_CONTINUE;
}
 
/*
 *  call-seq:
 *     hash.find_all { block }  -> an_other_hash
 *
 *  Constitue un hash avec les paires clés/valeurs pour lesquelles
 *  le bloc, à fonction de filtre, renvoie une valeur vraie.
 *
 *     annuaire = {"Jean" => "555-221", "Jasmine" => "555-483", "Robert" => "555-816"}
 *     annuaire.find_all { |k,v| k =~ /^j/i }  #=> {"Jean"=>"555-221", "Jasmine"=>"555-483"}
 */
static VALUE rb_hash_find_all(VALUE h)
{
    VALUE res_h;
 
    res_h = rb_hash_new();
    rb_hash_foreach(h, find_all_i, (st_data_t) res_h);
 
    return res_h;
}
 
void Init_hashext()
{
    rb_define_method(rb_cHash, "find_all", rb_hash_find_all, 0);
}

Vérifions le bon fonctionnement de cette nouvelle méthode dans une session IRB :

require 'hashext'
=> true

carres = {0 => 0, 1 => 1, 2 => 4, 3 => 9}
=> {0=>0, 1=>1, 2=>4, 3=>9}

carres.find_all
=> LocalJumpError: no block given

carres.find_all { |k,v| k > 1 }
=> {2=>4, 3=>9}

{}.find_all { |k,v| k > 1 }
=> {}

Exécuter des instructions après celle du bloc (ensure)

L'instruction ensure est un moyen très commode de faciliter la programmation et de s'assurer que certaines actions sont toujours effectuées après d'autres (libération des ressources, fermer un fichier, etc). On trouve bien évidemment une forme d'équivalent en C qui se manifeste par la fonction rb_rescue dont le prototype est le suivant :

VALUE rb_ensure(VALUE (*begin_proc)(), VALUE data1, VALUE (*ensure_proc)(), VALUE data2);

begin_proc est un pointeur sur une fonction C à appeler. ensure_proc est un pointeur sur la fonction C à appeler après, quoi qu'il arrive. data1 sera fournie à la fonction pointée par begin_proc lors de son appel. Il en va de même pour data2 qui concerne en revanche la fonction désignée par ensure_proc.

Ci-dessous un exemple qui n'a d'autre but pédagogique que de montrer premièrement comment est utilisée rb_ensure et deuxièmement comment se déroule l'exécution :

#include <ruby.h>
 
static VALUE executer_bloc()
{
    rb_yield_values(0);
}
 
static VALUE apres_bloc()
{
    rb_warn("Code à exécuter après le bloc (nettoyage, libération de mémoire, fermeture d'E/S, ...)");
}
 
static VALUE rb_exemple_ensure()
{
    rb_warn("Code à exécuter avant le bloc (configuration, ...)");
    rb_ensure(executer_bloc, Qundef, apres_bloc, Qundef);
 
    return Qtrue;
}
 
void Init_ensure()
{
    rb_define_global_function("exemple_ensure", rb_exemple_ensure, 0);
}

On met ensuite cette extension en œuvre dans une session IRB pour observer le résultat :

require 'ensure'
=> true

exemple_ensure
LocalJumpError: no block given

exemple_ensure { puts 'Pendant' }
warning: Code à exécuter avant le bloc (configuration, ...)
Pendant
warning: Code à exécuter après le bloc (nettoyage, libération de mémoire, fermeture d'E/S, ...)
=> true

Nous allons pousser un peu plus loin l'exemple avec la gestion d'une liste HTML (balise <ul>) à laquelle il est possible d'ajouter des éléments (balise <li>). Le résultat de cette liste s'affichera au fur et à mesure sur la sortie standard. Nous donnons la possibilité aux développeurs Ruby d'utiliser cette classe, nommée ListeHtml à l'aide d'un bloc ou non. Le code C en question :

#include <ruby.h>
#include <rubyio.h>
 
static VALUE rb_ul_close(VALUE obj)
{
    if (!RTEST(rb_iv_get(obj, "@closed"))) {
        VALUE ul;
 
        ul = rb_str_new2("</ul>");
        rb_io_puts(1, &ul, rb_stdout);
        rb_iv_set(obj, "@closed", Qtrue);
    }
 
    return Qnil;
}
 
static VALUE rb_ul_initialize(VALUE obj)
{
    VALUE ul;
 
    ul = rb_str_new2("<ul>");
    rb_io_puts(1, &ul, rb_stdout);
    rb_iv_set(obj, "@closed", Qfalse);
    if (rb_block_given_p()) {
        rb_ensure(rb_yield, obj, rb_ul_close, obj);
    }
 
    return obj;
}
 
static VALUE rb_ul_li(VALUE obj, VALUE str)
{
    VALUE buf;
 
    Check_Type(str, T_STRING);
    buf = rb_str_buf_new2("<li>");
    rb_str_buf_append(buf, str);
    rb_str_buf_cat2(buf, "</li>");
    rb_io_puts(1, &buf, rb_stdout);
 
    return str;
}
 
void Init_ListeHtml()
{
    VALUE rb_cListeHtml = rb_define_class("ListeHtml", rb_cObject);
    rb_define_method(rb_cListeHtml, "initialize", rb_ul_initialize, 0);
    rb_define_method(rb_cListeHtml, "li", rb_ul_li, 1);
    rb_define_method(rb_cListeHtml, "close", rb_ul_close, 0);
}

Et un script Ruby montrant les deux approches qu'il est alors possible d'utiliser :

#!/usr/bin/env ruby
 
require 'ListeHtml'
 
ListeHtml.new do |ul|
    ul.li('Element 1')
    ul.li('Element 2')
    ul.li('Element 3')
end # ul.close est inutile car fait automatiquement à la fin du bloc
 
ul2 = ListeHtml.new
ul2.li('Elément 1')
ul2.li('Elément 2')
ul2.li('Elément 3')
ul2.close # Nécessaire

Voici la sortie qui résulte de l'exécution de ce script Ruby :

<ul>
<li>Element 1</li>
<li>Element 2</li>
<li>Element 3</li>
</ul>
<ul>
<li>Elément 1</li>
<li>Elément 2</li>
<li>Elément 3</li>
</ul>

Les exceptions

Les exceptions prédéfinies

Vous trouverez ci-dessous la liste exhaustive des exceptions et erreurs de base avec leur description :

Nom de la classe Ruby Objet C (nom de la variable) Description
ArgumentError rb_eArgError Erreur dans les paramètres de la méthode
EOFError rb_eEOFError Fin de fichier atteinte
Exception rb_eException Classe de base des exceptions
fatal rb_eFatal -
FloatDomainError rb_eFloatDomainError Ce nombre décimal dépasse les capacités du type Float
IndexError rb_eIndexError L'index donné est en dehors des limites du tableau
Interrupt rb_eInterrupt Signal d'interruption reçu (comme Ctrl + C)
IOError rb_eIOError Erreur d'entrée/sortie
LoadError rb_eLoadError Erreur de chargement lors de require
LocalJumpError rb_eLocalJumpError Erreur de branchement local (lorsqu'un bloc attendu est omis, par exemple)
NameError rb_eNameError Erreur de nom : élément (classe, constante) indéfini
NoMemoryError rb_eNoMemError Mémoire insuffisante
NoMethodError rb_eNoMethodError Cette méthode n'existe pas
NotImplementedError rb_eNotImpError Méthode non implémentée. Peut être employée pour simuler une méthode abstraite
RangeError rb_eRangeError La valeur donnée excède les limites
RegexpError rb_eRegexpError Expression régulière erronée
RuntimeError rb_eRuntimeError -
ScriptError rb_eScriptError -
SecurityError rb_eSecurityError Erreur de sécurité
SignalException rb_eSignal -
StandardError rb_eStandardError -
SyntaxError rb_eSyntaxError Erreur de syntaxe
SystemCallError rb_eSystemCallError Echec de l'appel système
SystemExit rb_eSystemExit Sortie du programme demandée (fonctions exit)
SystemStackError rb_eSysStackError -
ThreadError rb_eThreadError Erreur liée aux processus
TypeError rb_eTypeError L'objet donné ne correspond au(x) type(s) attendu(s)
ZeroDivisionError rb_eZeroDivError Division par zéro (mathématiquement impossible)

Je vous propose également une représentation hiérarchique de celles-ci afin de montrer l'héritage entre elles :

Exception
    fatal
    NoMemoryError
    StandardError
        RegexpError
        ZeroDivisionError
        ThreadError
        NameError
            NoMethodError
        SystemCallError
        RuntimeError
        SystemStackError
        LocalJumpError
        ArgumentError
        TypeError
        IOError
            EOFError
        SecurityError
        RangeError
            FloatDomainError
        IndexError
    ScriptError
        SyntaxError
        NotImplementedError
        LoadError
    SignalException
        Interrupt
    SystemExit

Créer sa propre exception

Il n'y a rien de particulier à faire puisqu'une exception est avant tout une classe, il faut simplement qu'elle hérite d'une telle classe, comme Exception (rb_eException), l'une des exceptions de base. Exemple de déclaration (que nous utiliserons plus bas) :

#include <ruby.h>
 
static VALUE rb_eDomainException;
 
voit Init_exception()
{
    rb_eDomainException = rb_define_class("DomainException", rb_eException);
}

Comme toute classe vous pouvez lui redéfinir ou ajouter des méthodes voire autres (constantes, attributs, etc).

Provoquer une exception (raise)

La fonction C qui permet de provoquer une exception s'appelle rb_raise. Elle permet, comme le montre son prototype figurant ci-dessous, de choisir la classe de l'exception à lever ainsi que de fournir une description.

void rb_raise(VALUE exception, const char *format, ...);

En guise d'exemple, nous allons réécrire à titre purement pédagogique, la fonction mathématique log pour obtenir le logarithme naturel d'un nombre. Elle lancera une première exception TypeError si son seul paramètre n'est pas de type numérique ou une exception DomainException, définie tantôt, si la valeur n'est pas mathématiquement acceptable (inférieure ou égale à zéro) :

#include <math.h>
#include <ruby.h>
 
static VALUE rb_mDvpMath;
static VALUE rb_eDomainException;
 
static VALUE rb_log(VALUE obj, VALUE val)
{
    double d;
    int var_type;
 
    var_type = TYPE(val);
    if (!(var_type == T_FLOAT || var_type == T_STRING || var_type == T_FIXNUM || var_type == T_BIGNUM)) {
        rb_raise(rb_eTypeError, "Paramètre numérique attendu");
    }
    d = RFLOAT(rb_Float(val))->value; /* En réalité, cette fonction fait ce contrôle de type et lève une exception TypeError au besoin */
    if (d <= 0) {
        rb_raise(rb_eDomainException, "log n'accepte que les valeurs strictement supérieures à 0 (valeur donnée : %f)", d);
    }
    return rb_float_new(log(d));
}
 
void Init_DvpMath()
{
    rb_mDvpMath = rb_define_module("DvpMath");
 
    rb_define_module_function(rb_mDvpMath, "log", rb_log, 1);
 
    rb_eDomainException = rb_define_class_under(rb_mDvpMath, "DomainException", rb_eException);
}

Vérifions, à l'aide d'une session interactive (irb), le comportement de notre fonction log en situation réelle :

require 'DvpMath'
=> true

DvpMath::log(10)
=> 2.30258509299405

DvpMath::log(0)
=> in `log': log n'accepte que les valeurs strictement supérieures à 0 (valeur donnée : 0.000000) (DvpMath::DomainException)

DvpMath::log(-1)
=> in `log': log n'accepte que les valeurs strictement supérieures à 0 (valeur donnée : -1.000000) (DvpMath::DomainException)

Il existe quelques fonctions complémentaires liées aux exceptions comme rb_warn ou rb_warning qui affiche respectivement le message suivant que la variable globale $VERBOSE (ou un de ses alias) ait une valeur différente de nil ou la valeur true (sa valeur par défaut est false). Leurs prototypes sont les suivants :

void rb_warn(const char *fmt, ...);    /* Le message ne sera affiché seulement si $VERBOSE est à nil */
void rb_warning(const char *fmt, ...); /* Le message ne sera affiché seulement si $VERBOSE est à true */

Enfin, la fonction rb_fatal déclenchant une exception de type fatal, que vous ne pouvez récupérer et qui se définit sur le modèle des autres :

void rb_fatal(const char *fmt, ...);

Sachez qu'il existe d'autres fonctions pour lever des exceptions très spécifiques mais vous n'aurez probablement jamais besoin de les utiliser vous-mêmes dans une extension.

Prendre en charge une exception (rescue)

Pour gérer les exceptions Ruby qui peuvent survenir, vous avez à votre disposition deux fonctions C fortement semblables, rb_rescue et rb_rescue2 dont voici leurs prototypes :

VALUE rb_rescue(VALUE (*begin_proc)(ANYARGS), VALUE arg1, VALUE (*rescue_proc)(ANYARGS), VALUE arg2);
VALUE rb_rescue2(VALUE (*begin_proc)(ANYARGS), VALUE arg1, VALUE (*rescue_proc)(ANYARGS), VALUE arg2, ...);

Le paramètre begin_proc est un pointeur sur une fonction qui peut être à l'origine d'une exception et arg1 est un objet à transmettre lors de l'appel de cette fonction. rescue_proc est un pointeur sur une fonction qui a pour charge de gérer la ou les exceptions susceptibles de se manifester. Le paramètre arg2 est le pendant de arg1 pour cette dernière fonction lors de son appel. Ces deux fonctions vous renvoie une valeur qui correspond à celle qui est retournée par la fonction rescue_proc (Qnil si vous ne l'employez pas).

Il vous est possible de ne pas utiliser la partierescue_proc (valeur NULL) afin d'ignorer tout bonnement une ou plusieurs exceptions. Ceci prend tout son sens avec la fonction rb_rescue2 puisqu'elle attend une liste d'exceptions à gérer (celles qui ne sont pas précisées seront tout simplement ignorées).

Pour présenter cet aspect des exceptions, le code suivant introduit une fonction Ruby globale nommée "loterie" qui déclenche aléatoirement une exception suivant les paramètres qui lui sont passés. Les deux parties qui nous intéressent sont la fonction loterie qui provoque l'erreur et attraper_exceptions qui la gère.

Cette fonction Ruby attend un paramètre optionnel sous la forme d'un Hash associant une Exception (objet de type Class) à un tableau de deux valeurs qui sont une valeur numérique représentant la probabilité de tirer cette exception et un booléen indiquant si elle doit être gérée par notre code C. La clé nil possède un sens particulier, permettant d'indiquer une probabilité de n'en provoquer aucune. Si ce Hash n'est pas fourni, il sera généré et retourné par celle-ci.

#include <ruby.h>
#include <st.h>
 
static ID id_rand;
 
/* Définit la génération aléatoire des exceptions */
#define MAX_GEN_ERRORS        5  /* Nombre d'erreurs maximum obtenues */
#define MAX_ERROR_PROBABILITY 10 /* Probabilité maximale d'obtention d'une exception */
#define DEFAULT_PROBABILITY   20 /* Probabilité de ne pas en avoir une */
 
 
#define SIZE_OF(array) (sizeof(array) / sizeof *(array))
#define SIZE_OF_EXCEPTIONS (SIZE_OF(exceptions))
#define BOOL_P(x) (x == Qtrue || x == Qfalse)
 
static const VALUE *exceptions[] = {
    &rb_eIOError,
    &rb_eIndexError,
    &rb_eFloatDomainError,
    &rb_eArgError,
    &rb_eTypeError,
    &rb_eZeroDivError
};
 
/*
 * Générer un nombre aléatoire de 0 à max - 1 (inclus).
 * On fait appel à la fonction Ruby de manière à être portable.
 */
static int rand_int(int max)
{
    return FIX2INT(rb_funcall(rb_mKernel, id_rand, 1, INT2FIX(max)));
}
 
/*
 * Générer aléatoirement une valeur booléenne.
 */
static VALUE rand_Boolean()
{
    static const VALUE bvalues[] = { Qfalse, Qtrue };
 
    return bvalues[rand_int(SIZE_OF(bvalues))];
}
 
/*
 * Vérifier la structure des informations fournies par l'utilisateur.
 */
static int is_an_exception_i(VALUE key, VALUE val, VALUE *args)
{
    if (key == Qnil && RARRAY(val)->len >= 1 && FIXNUM_P(RARRAY(val)->ptr[0])) {
        return ST_CONTINUE;
    } else if (TYPE(key) == T_CLASS && rb_class_inherited_p(key, rb_eException) == Qtrue && TYPE(val) == T_ARRAY && RARRAY(val)->len >= 2 && FIXNUM_P(RARRAY(val)->ptr[0]) && BOOL_P(RARRAY(val)->ptr[1])) { /* rb_obj_is_kind_of */
        return ST_CONTINUE;
    } else {
        *args = Qfalse;
        return ST_STOP;
    }
}
 
/*
 * Générer un hash aléatoire d'exceptions.
 */
static VALUE generate_random_hash() {
    int nb, i;
    VALUE h, k;
 
    h = rb_hash_new();
    nb = rand_int(SIZE_OF_EXCEPTIONS) + 1; /* Pour en avoir au moins une */
    for (i = 0; i < nb; i++) {
        do {
            k = *exceptions[rand_int(SIZE_OF_EXCEPTIONS)];
        } while (st_lookup(RHASH(h)->tbl, k, 0));
        st_insert(RHASH(h)->tbl, k, rb_ary_new3(2, INT2FIX(rand_int(MAX_ERROR_PROBABILITY) + 1), rand_Boolean()));
    }
    st_insert(RHASH(h)->tbl, Qnil, rb_ary_new3(1, INT2FIX(DEFAULT_PROBABILITY)));
 
    return h;
}
 
/*
 * Faire la somme des probabilités des différentes exceptions.
 */
static int sum_i(VALUE key, VALUE val, long *args) {
    *args += FIX2LONG(RARRAY(val)->ptr[0]);
    return ST_CONTINUE;
}
 
/*
 * Trouver l'exception en fonction de leurs probabilités qui correspond au nombre aléatoire généré.
 */
static int search_i(VALUE key, VALUE val, VALUE *args) {
    long largs0, lval;
 
    largs0 = FIX2LONG(args[0]);
    lval = FIX2LONG(RARRAY(val)->ptr[0]);
    if (largs0 < lval) {
        args[1] = key;
 
        return ST_STOP;
    }
    args[0] = LONG2FIX(largs0 - lval);
 
    return ST_CONTINUE;
}
 
/*
 * Tirer aléatoirement une exception.
 */
static VALUE tirer_exception(VALUE h)
{
    VALUE args[2];
    long sum;
    int val;
 
    sum = 0;
    rb_hash_foreach(h, sum_i, (st_data_t) &sum);
    val = rand_int(sum);
    args[0] = LONG2FIX(val); /* Valeur à atteindre */
    args[1] = Qnil;          /* Résultat (la clé) */
    rb_hash_foreach(h, search_i, (st_data_t) args);
 
    if (!NIL_P(args[1])) {
        rb_raise(args[1], "Une exception '%s' a été provoquée", rb_class2name(args[1]));
    }
    return args[1];
}
 
/*
 * Intercepter les exceptions.
 */
static VALUE attraper_exceptions(VALUE h, VALUE error)
{
    VALUE v;
 
    if (st_lookup(RHASH(h)->tbl, rb_obj_class(ruby_errinfo), &v) && RARRAY(v)->ptr[1] == Qtrue) {
        VALUE strerror;
 
        strerror = rb_str_to_str(error); /* Les fonctions de conversion modifient l'objet en un objet String */
        rb_warn("*** Une exception '%s' a été prise en charge, description : %s", rb_obj_classname(error), StringValuePtr(strerror));
        return Qnil;
    }
 
    return error;
}
 
/*
 *  call-seq:
 *     loterie(hash)  -> hash
 *
 *  Déclencher aléatoirement une exception. Si son paramètre est omis, un +Hash+
 *  associant une +Exception+ ou +nil+ à un tableau de deux éléments [Fixnum, Boolean]
 *  représentant respectivement une probabilité de déclenchement de l'exception et
 *  si l'exception doit être prise en charge (valeur +true+) ou ignorée +false+.
 *
 *  La valeur +nil+ en tant que clé du hachage représente la possibilité de n'en
 *  déclencher aucune.
 *
 *     loterie
 *     #=> {nil => [20], ArgumentError => [6, true]}
 *
 *     loterie({nil => [7], ArgumentError => [1, false], TypeError => [2, true]})
 *     #=> {nil => [7], ArgumentError => [1, false], TypeError => [2, true]}
 */
static VALUE rb_loterie(int argc, VALUE *argv, VALUE obj)
{
    VALUE h;
 
    if (argc <= 1) {
        VALUE err;
 
        if (argc == 0) {
            h = generate_random_hash();
        } else if (argc == 1) {
            h = argv[0];
            if (TYPE(h) == T_HASH) {
                VALUE ret;
 
                ret = Qtrue;
                rb_hash_foreach(h, is_an_exception_i, (st_data_t) &ret);
                if (ret != Qtrue) {
                    rb_raise(rb_eTypeError, "wrong argument type (expected Hash {Exception or Qnil => [Fixnum, Boolean]})");
                }
            } else {
                rb_raise(rb_eTypeError, "wrong argument type %s (expected Hash)", rb_obj_classname(h));
            }
        }
        err = rb_rescue(tirer_exception, h, attraper_exceptions, h);
        if (!NIL_P(err)) {
            VALUE strerr;
 
            strerr = rb_str_to_str(err); /* Les fonctions de conversion modifient l'objet en un objet String */
            rb_fatal("*** L'exception '%s' n'a pu être récupérée (%s)", rb_obj_classname(err), StringValuePtr(strerr));
        }
    } else {
        rb_raise(rb_eArgError, "wrong number of arguments %d (0 or 1 expected)", argc);
    }
 
    return h;
}
 
void Init_rescue()
{
    if (MAX_GEN_ERRORS > SIZE_OF_EXCEPTIONS) {
        rb_fatal("La génération aléatoire des exceptions en demande plus que l'extension n'en définit");
    }
 
    id_rand = rb_intern("rand");
 
    rb_define_global_function("loterie", rb_loterie, -1);
}

Pour mettre en application cette extension, nous allons utiliser ce simple script Ruby :

#!/usr/bin/env ruby
 
require 'rescue'
h = {nil => [7], ArgumentError => [2, true], TypeError => [1, false]}
1.upto(10) { |i|
    puts "#{i}"
    loterie(h)
}

Si une exception de type ArgumentError est générée, le script continuera son exécution puisque cette exception est "gérée". En revanche, l'inverse se produira avec une erreur TypeError qui mettra fin au script.

Si vous remplacez maintenant la ligne faisant appel à rb_rescue par sa consoeur rb_rescue2 de façon à ne gérer que les exceptions ArgumentError et ZeroDivisionError, de la manière suivante :

err = rb_rescue2(tirer_exception, h, attraper_exceptions, h, rb_eArgError, rb_eZeroDivError, 0);

(le 0 marquant la fin de la liste). En réutilisant le script donné plus haut (et qui ne prend pas en charge les exceptions grâce à rescue), on obtient une erreur normale de type TypeError :

./test.rb:9:in `loterie': Une exception 'TypeError' a été provoquée (TypeError)
        from ./test.rb:9
        from ./test.rb:7:in `upto'
        from ./test.rb:7

Alors qu'auparavant elle était reprise puis relancée (en tant qu'erreur de type fatal) :

./test.rb:9:in `loterie': *** L'exception 'TypeError' n'a pu être récupérée (Une exception 'TypeError' a été provoquée) (fatal)
        from ./test.rb:9
        from ./test.rb:7:in `upto'
        from ./test.rb:7

Distribuer son extension

mkmf : l'équivalent Ruby des autotools

Afin d'offrir une extension la plus portable possible et de permettre aux utilisateurs avancés de définir partiellement son comportement via des options en ligne de commande, vous utiliserez mkmf, une librairie simple d'emploi qui permet de générer le Makefile pour principalement la compilation des sources que composent votre extension.

C'est une adaptation allégée des célèbres autotools adaptée à l'environnement Ruby. Il est ainsi possible de détecter les éléments nécessaires à la compilation (librairies requises, absence de fichiers d'entête ou de fonctions système, …) voire de s'adapter automatiquement au système par le biais de la compilation conditionnelle (directives du préprocesseur). De plus, les personnes qui maintiennent l'extension peuvent proposer des options de configuration pour modifier le comportement de celle-ci (options liées au débogage par exemple), désactiver par défaut une fonctionnalité expérimentale, modifier des paramètres par défaut, etc.

Pour recourir à la librairie mkmf, il faudra tout d'abord l'importer à l'aide de l'instruction require.

L'appel à la fonction create_makefile, générant le makefile, doit être appelée en dernier et il est conseillé de ne créer ce fichier que si aucune erreur n'est susceptible d'intervenir, ce que vous devriez être en mesure de déterminer à l'issue des différents tests effectués. Le premier paramètre obligatoire de create_makefile doit impérativement correspondre au nom de la fonction C appelée au chargement de l'extension par l'interpréteur (la partie après Init_). Il peut éventuellement prendre la forme d'un chemin, ce qui aura pour effet de placer le binaire de l'extension dans celui-ci (exemple : libs/modules/MyUtils installera la librairie MyUtils.so dans un sous-répertoire libs/modules/ lors du make install).

Passons en revue, pour commencer, les variables globales importantes que vous pourriez avoir besoin de modifier (le terme compléter serait plus approprié) pour que la compilation puisse se faire :

  • $CFLAGS : sa valeur sera ajoutée à la variable CFLAGS du makefile (options d'optimisation : -O, liées aux erreurs : -W, …)
  • $CPPFLAGS : sera incluse à la variable CPPFLAGS du makefile (options -I et éventuellement -D)
  • $defs : similaire à $CPPFLAGS mais est réservée à la définition de macros (option -D) car celles-ci peuvent être écrites dans un fichier d'entête à part via la fonction create_header au lieu de figurer directement dans la ligne de commande de compilation
  • $LDFLAGS : adjointe à la variable LDFLAGS du makefile (options de compilation comme -L et -l)

L'équivalent du script configure

La librairie mkmf propose toute une batterie de fonctions pour que l'environnement puisse être soumis à toute une panoplie de tests. Les exemples accompagnant ceux-ci tournent pour beaucoup autour des chflags. Les chflags sont une fonctionnalité propre aux systèmes Unix BSD (inutile de les chercher sous Windows ou encore sous Linux) qui permettent d'accoler aux fichiers des drapeaux ayant des significations souvent très fortes. Ils sont, pour la plupart, liés à la modification d'un fichier, empêchant ainsi toute altération sur ceux-ci (suppression, modifications de permission ou de propriétaire, etc). Ces fonctions sont les suivantes :

check_sizeof(type, headers = nil)

Il peut être nécessaire de savoir combien d'octets occupe un certain type en mémoire, ce que permet de faire la fonction check_sizeof. Elle attend en paramètres le type type à tester et optionnellement des fichiers d'entêtes définissant ce type qu'il est indispensable d'inclure pour le test. Elle vous renverra, si tout se passe correctement, la taille en octets de ce type et la variable $defs comprendra une nouvelle macro SIZEOF_X (X représentant le nom de type après conversion en caractères majuscules puis suppression des caractères non alphanumériques et underscore restants) qui servira à obtenir cette taille.

Pour exemple, reprenons un des impératifs du langage Ruby qui requiert pour sa propre compilation qu'un long doit être de la même taille qu'un pointeur :

require 'mkmf'
 
unless check_sizeof('long') == check_sizeof('void *')
    message "---->> ruby requires sizeof(void *) == sizeof(long) <<----\n"
    exit 1
end
 
create_makefile('...')

On peut être redondant en effectuant exactement la même chose en C :

#if SIZEOF_LONG != SIZEOF_VOID_
# error ---->> ruby requires sizeof(void *) == sizeof(long) <<----
#endif

Pour les pointeurs, l'astérisque est perdue. Ce qui explique que pour void * on obtienne ici un simple souligné à la fin de la macro.

have_func(func, headers = nil)

Cette fonction cherche si la fonction C nommée func existe dans les entêtes par défaut et celles que vous auriez éventuellement fournit par le paramètre headers. S'il s'avère que cette fonction est disponible (ou tout du moins convenablement détectée) elle renverra true et ajoutera la macro HAVE_X (où X est la valeur donnée à func après transformation du nom en majuscules) à la variable globale $defs.

En guise d'exemple, nous cherchons à savoir si la fonction chflags est présente. Nous écrirons alors dans notre fichier extconf.rb le test suivant :

require 'mkmf'
 
have_func('chflags')
 
create_makefile('...')

Voilà ensuite comment exploiter la directive définie pour le préprocesseur, HAVE_CHFLAGS :

#include <sys/stat.h>
#include <unistd.h>
 
#include <ruby.h>
 
static VALUE rb_chflags(VALUE klass, VALUE filename, VALUE flags)
{
#ifdef HAVE_CHFLAGS
    int res;
 
    SafeStringValue(filename);
    res = chflags(StringValueCStr(filename), NUM2ULONG(flags));
    if (res) {
        rb_sys_fail(NULL);
    }
    return (res ? Qfalse : Qtrue);
#else
    rb_warn("Fonction chflags inexistante");
    return Qfalse;
#endif /* HAVE_CHFLAGS */
}
have_header(header)

Permet de savoir si le fichier d'entête header est disponible sur ce système. Si tel est le cas, elle renverra true et une macro du type HAVE_X_H, où X correspond au nom du fichier (en majuscules et sans son extension), sera ajoutée à $defs de sorte à ce qu'elle soit utilisable lors de la compilation.

Exemple usuel, chercher à connaître si les fonctions à liste d'arguments variables sont utilisables en testant stdarg.h :

have_header('stdarg.h')

Et pour nous y adapter dans notre code C, nous pouvons procéder ainsi :

#ifdef HAVE_STDARG_H
# include <stdarg.h>
# define va_init_list(a,b) va_start(a,b)
#else
# include <varargs.h>
# define va_init_list(a,b) va_start(a)
#endif /* HAVE_STDARG_H */
#include <string.h>
 
#ifndef HAVE_STPCPY
static size_t strlcpy(char *dst, const char *src, size_t siz)
{
    char *d = dst;
    const char *s = src;
    size_t n = siz;
 
    if (n != 0) {
        while (--n != 0) {
            if ((*d++ = *s++) == '\0')
                break;
        }
    }
 
    if (n == 0) {
        if (siz != 0)
            *d = '\0';
        while (*s++)
            ;
    }
 
    return (s - src - 1);
}
#endif /* !HAVE_STPCPY */
 
VALUE
#ifdef HAVE_STDARG_H
rb_strconcat(int freeze, const char *string1, ...)
#else
rb_strconcat(freeze, string1, va_alist)
    int freeze;
    const char *string1;
    va_dcl
#endif /* HAVE_STDARG_H */
{
    long len;
    va_list args;
    char *s, *ptr;
    struct RString *str;
 
    if (!string1) {
        return Qnil;
    }
 
    len = strlen(string1);
    va_start(args, string1);
    s = va_arg(args, char *);
    while (s != NULL) {
        len += strlen(s);
        s = va_arg(args, char *);
    }
    va_end(args);
 
    str = (struct RString *) rb_newobj();
    OBJSETUP(str, rb_cString, T_STRING);
 
    str->ptr = ALLOC_N(char, len + 1);
    str->aux.capa = len;
    str->len = len;
 
    ptr = str->ptr;
    ptr = stpcpy(ptr, string1);
    va_init_list(args, string1);
    s = va_arg(args, char *);
    while (s != NULL) {
        ptr = stpcpy(ptr, s);
        s = va_arg(args, char *);
    }
    va_end(args);
 
    if (freeze) {
        rb_obj_freeze((VALUE) str);
    }
 
    return (VALUE) str;
}
have_library(lib, func = nil, headers = nil)

Souvent certaines librairies dynamiques écrites en C sont des prérequis pour une extension développée (surtout quand il s'agit d'en interfacer une). Un test a donc été prévu pour cela : have_library. Comme toutes les fonctions have_*, elle vous renverra true si la librairie lib a pu être trouvée. Le paramètre func permet de chercher un point d'entrée précis dans celle-ci auquel cas headers fichiers d'entête spécifiques où chercher func, pourrait être nécessaire.

Voici comment détecter la librairie libxml2 :

require 'mkmf'
 
have_library('xml2', 'xmlParseFile')
 
create_makefile('...')

À noter, que c'est seulement en cas de succès que les options -l<i>nom_bibliothèque</i> (pour libxml2 il s'agit de -lxml2) sont ajoutées à la variable $LDFLAGS.

have_macro(macro, headers = nil, opt = "")

Tester la disponibilité de la macro nommée macro parmi les entêtes par défaut et celles qui sont indiquées par headers (s'il y a en a plusieurs headers doit se présenter sous la forme d'un tableau de chaînes). Le paramètre opt vous permet de spécifier vous-mêmes les options utilisées lors du test par le préprocesseur (commande cpp généralement). Cette fonction ne retourne qu'un booléen, elle ne générera aucune macro pour indiquer la présence ou l'absence de la macro. Ce sera à vous de le faire par un moyen ou un autre.

Illustration : tous les systèmes BSD ne définissent pas le même nombre de chflags (mais les noms leur sont identiques). Ils sont créés à l'aide de macros et pour disposer d'une constante Ruby permettant par la suite l'utilisation de ceux qui sont définis nous devons donc procéder à ces tests :

require 'mkmf'
 
def have_chflag(*flags)
    flags.each do |f|
        $defs << "-DHAVE_#{f}" if have_macro(f, 'sys/stat.h')
    end
end
 
have_chflag(
    'UF_NODUMP',
    'UF_IMMUTABLE',
    'UF_APPEND',
    'UF_NOUNLINK',
    'UF_OPAQUE',
    'SF_ARCHIVED',
    'SF_IMMUTABLE',
    'SF_APPEND',
    'SF_NOUNLINK'
)
 
create_makefile('...')

Puisqu'à chacun des chflags est attribué une macro, il est facile de ne créer que ceux dont le système dispose réellement :

#ifdef HAVE_UF_NODUMP
    rb_define_const(rb_mBSD, "CF_UNODUMP", ULONG2NUM(UF_NODUMP));
#endif /* HAVE_UF_NODUMP */
#ifdef HAVE_UF_IMMUTABLE
    rb_define_const(rb_mBSD, "CF_UCHG", ULONG2NUM(UF_IMMUTABLE));
#endif /* HAVE_UF_IMMUTABLE */
#ifdef HAVE_UF_APPEND
    rb_define_const(rb_mBSD, "CF_UAPPEND", ULONG2NUM(UF_APPEND));
#endif /* HAVE_UF_APPEND */
#ifdef HAVE_UF_NOUNLINK
    rb_define_const(rb_mBSD, "CF_UUNLNK", ULONG2NUM(UF_NOUNLINK));
#endif /* HAVE_UF_NOUNLINK */
#ifdef HAVE_UF_OPAQUE
    rb_define_const(rb_mBSD, "CF_OPAQUE", ULONG2NUM(UF_OPAQUE));
#endif /* HAVE_UF_OPAQUE */
#ifdef HAVE_SF_ARCHIVED
    rb_define_const(rb_mBSD, "CF_ARCH", ULONG2NUM(SF_ARCHIVED));
#endif /* HAVE_SF_ARCHIVED */
#ifdef HAVE_SF_IMMUTABLE
    rb_define_const(rb_mBSD, "CF_SCHG", ULONG2NUM(SF_IMMUTABLE));
#endif /* HAVE_SF_IMMUTABLE */
#ifdef HAVE_SF_APPEND
    rb_define_const(rb_mBSD, "CF_SAPPEND", ULONG2NUM(SF_APPEND));
#endif /* HAVE_SF_APPEND */
#ifdef HAVE_SF_NOUNLINK
    rb_define_const(rb_mBSD, "CF_SUNLNK", ULONG2NUM(SF_NOUNLINK));
#endif /* HAVE_SF_NOUNLINK */
have_struct_member(type, member, headers = nil)

S'assurer que la structure type dispose bel et bien d'un champ appelé member. Cette vérification portera sur les entêtes par défaut à moins que le paramètre headers ne soit utilisé pour indiquer un fichier particulier ou bien un ensemble sous les traits d'un tableau.

Conservons l'idée de départ : il n'est possible d'accéder aux chflags d'un fichier uniquement sur les systèmes où la structure de type stat possède un membre st_flags. Implémentons ce contrôle :

require 'mkmf'
 
unless have_struct_member('struct stat', 'st_flags', 'sys/stat.h')
    message "stat stucture has no member 'st_flags'\n"
    exit 1
end
 
create_makefile('...')

Vous êtes en mesure de réutiliser le résultat de ce test, dans votre code C car une macro sera automatiquement définie (dans la variable globale $defs) sur le modèle HAVE_ST_X où X est le nom du membre après avoir été mis en majuscules :

#include <sys/stat.h>
 
#ifndef HAVE_ST_ST_FLAGS
# error stat stucture has no member 'st_flags'
#endif /* !HAVE_ST_ST_FLAGS */

Info :

  • La répétition de la partie ST dans le nom de cette macro vient purement et simplement du nom du membre (st_flags)
  • Faites figurer le type de la structure tel qu'il a été déclaré : le mot-clé struct a son importance.
have_type(type, headers = nil, opt = "")

Prendre connaissance de la définition ou non d'un type de données appelé type. Cette recherche portera sur les fichiers d'entêtes définis par défaut ou ceux que vous aurez indiqué par le biais du paramètre headers. Le paramètre opt vous permet de spécifier vos propres options pour la compilation du test. Si cette vérification aboutie, une macro de style HAVE_TYPE_X, X représentant la valeur du paramètre type après avoir été transformé en caractères majuscules, rejoindra la variable $defs.

Le membre st_flags de la structure stat, abordé précédemment est défini comme étant de type fflags_t. Nous pouvons contrôler que c'est le cas :

require 'mkmf'
 
have_type('fflags_t', 'sys/stat.h')
 
create_makefile('...')

Et au besoin le définir nous-mêmes, bien qu'ici ça n'est peu d'intérêt, grâce à la macro HAVE_TYPE_FFLAGS_T :

#include <sys/stat.h>
 
#ifndef HAVE_TYPE_FFLAGS_T
typedef unsigned int fflags_t;
#endif /* !HAVE_TYPE_FFLAGS_T */
have_var(var, headers = nil)

Un contrôle quelque peu particulier puisque cette fonction a pour but de prendre en charge les variables globales. Elle tentera donc de s'assurer que la variable var est définie parmi les entêtes par défaut ou à défaut celles que vous lui auriez fourni par le paramètre headers. Si le test est concluant, elle vous retournera true et créera une macro HAVE_X (X représentant la valeur de var après conversion en lettres majuscules).

Pour illustrer, essayons avec la célèbre variable errno :

require 'mkmf'
 
have_var('errno', 'errno.h')
 
create_makefile('...')

Et la macro s'y rapportant s'utiliserait de la sorte :

FILE *fp;
char *p;
 
if (NULL == (fp = fopen(fichier, "r")) {
#ifdef HAVE_ERRNO
    p = strerror(errno);
#else
    p = "can't open";
#endif /* HAVE_ERRNO */
    fprintf(stderr, "[%s:%d] %s\n", __FILE__, __LINE__, p);
    exit(-1);
}
/* ... */

Info :

  • Je n'ai présenté ici que les principaux tests. Vous pouvez par ailleurs écrire les vôtres. Vous trouverez la source de mkmf dans le répertoire lib de votre installation Ruby ou des sources de Ruby. La documentation de mkmf est également disponible en ligne
  • N'oubliez pas de régénérer le Makefile à chaque fois que cela s'avère nécessaire en exécutant la commande : ruby extconf.rb éventuellement suivie de make

Les options --enable/--disable

Allons un peu plus loin à présent en abordant la possibilité d'ajouter des options de configuration. Commençons par les plus simples : celles de type enable/disable qui, retournant une valeur indiquant un état (souvent un booléen), permettent d'activer ou désactiver une fonctionnalité.

Cas le plus courant, définir une option pour le débogage qui sera désactivée par défaut car essentiellement réservée aux développeurs :

require 'mkmf'
 
$defs << '-DDEBUG' if enable_config('debug')
 
create_makefile('chflags')

Une manière d'exploiter ensuite le résultat de cette configuration pourrait être :

#ifdef DEBUG
# include <stdarg.h>
# include <stdio.h>
# define debug(format, ...) \
    fprintf(stderr, __FILE__ ":%d:" format "\n", __LINE__, __VA_ARGS__)
#else
# define debug(format, ...)
#endif /* DEBUG */
 
void Init_chflags()
{
    debug("Chargement de l'extension", 0);
 
    /* ... */
 
#ifdef HAVE_UF_NODUMP
    debug("nodump disponible (%08X)", UF_NODUMP);
#else
    debug("nodump indisponible", 0);
#endif /* HAVE_UF_NODUMP */
#ifdef HAVE_UF_IMMUTABLE
    debug("uchg disponible (%08X)", UF_IMMUTABLE);
#else
    debug("uchg indisponible", 0);
#endif /* HAVE_UF_IMMUTABLE */
#ifdef HAVE_UF_APPEND
    debug("uappend disponible (%08X)", UF_APPEND);
#else
    debug("uappand indisponible", 0);
#endif /* HAVE_UF_APPEND */
#ifdef HAVE_UF_NOUNLINK
    debug("uunlnk disponible (%08X)", UF_NOUNLINK);
#else
    debug("uunlnk indisponible", 0);
#endif /* HAVE_UF_NOUNLINK */
#ifdef HAVE_UF_OPAQUE
    debug("opaque disponible (%08X)", UF_OPAQUE);
#else
    debug("opaque indisponible", 0);
#endif /* HAVE_UF_OPAQUE */
#ifdef HAVE_SF_ARCHIVED
    debug("arch disponible (%08X)", SF_ARCHIVED);
#else
    debug("arch indisponible", 0);
#endif /* HAVE_SF_ARCHIVED */
#ifdef HAVE_SF_IMMUTABLE
    debug("schg disponible (%08X)", UF_NODUMP);
#else
    debug("schg indisponible", 0);
#endif /* HAVE_SF_IMMUTABLE */
#ifdef HAVE_SF_APPEND
    debug("sappend disponible (%08X)", SF_APPEND);
#else
    debug("sappend indisponible", 0);
#endif /* HAVE_SF_APPEND */
#ifdef HAVE_SF_NOUNLINK
    debug("sunlnk disponible (%08X)", SF_NOUNLINK);
#else
    debug("sunlnk indisponible", 0);
#endif /* HAVE_SF_NOUNLINK */
 
    /* ... */
}

Ces options sont à fournir lors de l'exécution du script extconf.rb. Il est, par ailleurs, important de les faire figurer après le nom du script car elles seront, sinon, destinées à l'interpréteur :

ruby extconf.rb --enable-debug
make

Les options --with/--without

Les options de type --with sont comparables aux champs libres des formulaires. Une telle option attend généralement un paramètre et son absence ou inutilisation peut être forcée via --without. mkmf prévoit une fonction spécifique à cette fin : with_config(nom, *defauts). Le paramètre nom est le nom de l'option (n'y faites pas figurer --with- ou with) et defauts sont la ou les valeurs par défaut que vous obtiendrez dans le cas où l'utilisateur n'a pas fait figurer cette option dans la ligne de commande pour l'exécution du script extconf.rb. Elle vous renverra suivant les cas :

  • false : si l'option a été niée (--without-X) ou si sa valeur est 'no'
  • true : si l'option a la valeur 'yes'
  • les valeurs par défaut que vous avez fixé via son deuxième paramètre et au-delà si aucune valeur n'a été explicitée par l'utilisateur
  • la valeur fournit par l'utilisateur (une chaîne donc)

L'exemple ci-dessous consiste à implémenter une vérification d'utilisateur basée sur l'identifiant système de l'utilisateur (uid). Pour cela, l'utilisateur qui exécutera l'extension devra appartenir à une plage d'identifiants. Cette fourchette est configurable grâce à une option, --with-uid, qui par défaut n'autorisera que les utilisateurs possédant un uid d'au moins 100. Voici les différentes valeurs acceptées ainsi que leur signification :

  • --without-uid ou --with-uid=no : ne définit aucune des macros MIN_UID et MAX_UID
  • --with-uid=X : requiert un uid égal à X
  • --with-uid=+X : requiert un uid supérieur ou égal à X
  • --with-uid=X-Y : requiert un uid compris entre X et Y (inclus tous deux)

Le code du script Ruby extconf.rb respectant ce comportement est le suivant :

require 'mkmf'
 
case with_config('uid', '+100')
    when false
        # NOP
    when /^(\d+)$/
        $defs << "-DMIN_UID=#{$1}" << "-DMAX_UID=#{$1}"
    when /^\+(\d+)$/
        $defs << "-DMIN_UID=#{$1}"
    when /^(\d+)-(\d+)$/
        $defs << "-DMIN_UID=#{$1}" << "-DMAX_UID=#{$2}"
    else
        message "Valeur inattendue pour l'option --with-uid\n"
        exit 1
end
 
create_makefile('...')

Et voici un code C possible exploitant les macros qui sont définies (ou non) afin d'effectuer ce travail :

#include <unistd.h>
#include <sys/types.h>
 
enum {
   KO,
   OK
};
 
int checkuid() {
    uid_t ruid;
 
    ruid = getuid();
#if defined(MIN_UID) && defined(MAX_UID)
    return (ruid >= MIN_UID && ruid <= MAX_UID ? OK : KO);
#elif defined(MIN_UID) && !defined(MAX_UID)
    return (ruid >= MIN_UID ? OK : KO);
#else
    return OK;
#endif
}
 
/* ... */

mkmf dédie à la fonction dir_config tout le travail de liaison avec une librairie externe. Son entête est le suivant : dir_config(nom, idefault = nil, ldefault = nil) sachant que nom est le nom des différentes options, idefault est le chemin par défaut vers les fichiers d'entêtes de la librairie en question et ldefault le répertoire par défaut vers sa forme binaire compilée. Elle gère ainsi de manière autonome les options --with-X-dir, --with-X-include et --with-X-lib, X correspondant à la valeur du paramètre nom pour ajuster les options de compilation suivant les indications de l'utilisateur.

Clarifions les capacités de cette fonction à l'aide d'un exemple, où il est question d'utiliser la librairie libxml2. Pour que la compilation se passe sans accroc, nous proposerons les options citées ci-dessus, de manière à la faciliter et à ce que tout le monde, y compris ceux qui ne l'ont pas installé dans des répertoires standards puissent obtenir une forme exploitable de ladite extension :

require 'mkmf'
 
dir_config('libxml2')
 
create_makefile('...')

Les options suivantes seront ainsi disponibles :

  • --with-libxml2-dir=X : indique le répertoire de base de libxml2 et ajoute -IX/include à $CPPFLAGS et X/lib à $LIBPATH
  • --with-libxml2-include=Y : ajoute -IY à $CPPFLAGS
  • --with-libxml2-lib=Z : ajoute Z à $LIBPATH

De quoi faciliter la tâche de ceux qui développent l'extension comme de ceux qui l'utilisent.

Documenter son code C pour rdoc

Vous êtes probablement familiers avec RDoc pour avoir documenté le code que vous aviez écrit en Ruby pur. C'est cette même fonctionnalité que nous utiliserons et que je vais détailler. Du fait que nous développons en C, les commentaires devront être placés entre /* et */.

RDoc étant initialement prévu pour du code Ruby, il fait de son mieux pour déduire le plus de choses possibles du code C mais il est dans l'impossibilité de tout deviner. Il est principalement incapable de déduire la signature d'une fonction ou méthode Ruby à partir de son prototype C. Pour compenser, c'est à vous de l'indiquer en ajoutant à chacune des fonctions C "exportées" une partie <b>call-seq</b> qui définit celle-ci (après un retour à la ligne). Vous l'avez probablement remarqué dans les exemples donnés précédemment, semblable à celui-ci :

/*
 *  call-seq:
 *     array.product  -> float
 *
 *  Calcul le produit des éléments du tableau _self_.
 */

La mise en forme RDoc fonctionne de manière similaire aux wikis : elle propose de mettre en forme simplement, à l'aide de symboles, des éléments de votre source. Voyons plus en détail, ces possibilités :

  • RDoc fournit 6 niveaux de titre (à comparer aux balises HTML h1 à h7) :
    /*
     *  = Titre de niveau 1
     *  == Titre de niveau 2
     *  === Titre de niveau 3
     *  ==== Titre de niveau 4
     *  ===== Titre de niveau 5
     *  ====== Titre de niveau 6
     */
  • Mettre en gras un mot en l'entourant d'astérisques ou une partie du texte à l'aide des balises b :
    /*
     *  Le mot suivant sera en *gras*.
     *  <b>Cette phrase est entièrement en gras</b>.
     */
  • Dans le même ordre d'idée, l'italique, où nous encadrons l'élément de caractères soulignés ou de balises em :
    /*
     *  Le mot suivant sera en _italique_.
     *  <em>Cette phrase est entièrement en italique</em>.
     */
  • Pour ajouter une ligne séparatrice horizontale placer au moins trois tirets successifs :
    /*
     *  Ci-dessous une ligne séparatrice :
     *  ---
     */
  • Pour créer une liste non ordonnée, on précède chacun de ses éléments par un astérisque :
    /*
     *  * Un premier élémént
     *  * Un deuxième élément
     *  * Un troisième élément
     */
  • On peut faire de même en utilisant un tiret au lieu d'un astérisque :
    /*
     *  - Un premier élémént
     *  - Un deuxième élément
     *  - Un troisième élément
     */
  • Créer une liste ordonnée et préfixée par un nombre, un caractère majuscule ou minuscule :
    /*
     *  9. Elément 1
     *  0. Elément 2
     *  3. Elément 3
     *
     *  z. Elément a
     *  e. Elément b
     *  f. Elément c
     *
     *  Y. Elément A
     *  C. Elément B
     *  T. Elément C
     */

    J'ai volontairement choisi des numérotations arbitraires car elles ne sont utilisées par RDoc que pour déterminer la nature du préfixe (chiffre, lettre majuscule ou minuscule) qu'il doit à son tour employer pour la sortie. Il les réindexera alors correctement comme le montre le texte de chacun des éléments de chaque liste.

  • Pour ajouter un extrait de code au commentaire, indentez simplement un peu plus la partie qui y correspond :
    /*
     *  Exemples d'utilisation :
     *     h = {1 => 'un', 2 => 'deux'}
     *     h.each { |k,v| puts "#{k} => #{v}" }
     */
  • Faire apparaître un élément en police à chasse fixe grâce aux symboles plus, aux balises tt ou code :
    /*
     * Cette méthode renvoie +true+ ou <tt>false</tt>
     * si l'élément <tt>elt</tt> a été trouvé dans +tableau+
     */
  • Ne pas faire apparaître une partie du commentaire :
    /*
     * J'apparais.
     *--
     * Je n'apparais pas.
     *++
     * Je réapparais !
     */

    Le morceau situé entre -- et ++ (ici Je n'apparais pas.) ne sera pas présent dans la documentation générée.

Info :

  • Les liens sont automatiquement transposés en une URL cliquable
  • Les termes qui correspondent à des éléments que vous avez vous-mêmes écrit (une classe, un module, une méthode) sont automatiquement transformés en un lien menant vers eux
  • Vous pourriez avoir besoin d'utiliser normalement les symboles ou les balises dans vos commentaires. Pour les conserver tels quels faites précéder celui qui marque le début d'un backslash. Exemple : Ce \*mot* ne sera pas en gras.

Votre code étant maintenant agrémenté de commentaires, faites appel à la commande rdoc pour générer la documentation HTML :

rdoc

Cette documentation est par défaut générée dans un sous-répertoire nommé doc. La commande rdoc fournit un certain nombre d'options comme -x pour exclure les fichiers correspondant à un motif, --diagram pour générer une représentation graphique des classes et modules que vous avez défini, d'autres sont liées à la consultation de la documentation en ligne de commande (ri) ou aux formats de génération de la documentation (XML ou CHM). Vous obtiendrez une liste exhaustive de celles-ci avec une description en utilisant :

rdoc --help | less

Automatiser les différentes étapes de la production avec rake

require 'rubygems'
require 'rake'
require 'rake/clean'
require 'rake/testtask'
require 'rake/rdoctask'
require 'mkmf'
 
 
PKG_NAME = 'MonExtension'
 
 
task :default => [:config, :compile]
 
desc 'Lancer les tests unitaires'
Rake::TestTask.new(:test => :compile) do |t|
    t.libs    << 'test'
    t.libs    << 'ext'
    t.pattern = 'test/test*.rb'
    t.verbose = true
end
 
desc 'Générer la documentation HTML'
Rake::RDocTask.new(:rdoc) do |rdoc|
    rdoc.rdoc_dir = 'html'
    rdoc.title = PKG_NAME
    rdoc.main = "README.rdoc"
    rdoc.rdoc_files.include("README.rdoc", "ext/*.c")
end
 
desc 'Configuration'
task :config do
    unless File.exists?('extconf.h')
        $CFLAGS << ' -DDEBUG' if enable_config('debug')
        create_header()
        create_makefile(PKG_NAME)
    end
end
 
desc 'Compilation'
task :compile => [:config] do
    unless system('make')
        STDERR.puts 'Failed to build extension'
        break
    end
end
 
desc 'Installation'
task :install => [:compile] do
    unless false or system('make install')
        STDERR.puts 'Failed to install extension'
        break
    end
end

Conclusion

Épilogue

Trouvant peu de documentations sur le sujet, le but de cet article consistait à rassembler le plus d'informations possible sur le développement d'une extension Ruby en C et de guider tout développeur ayant un minimum de connaissances du langage C dans l'écriture d'une telle extension. J'espère avoir honoré ce contrat, bien qu'il me soit impossible d'aborder l'intégralité de l'API de Ruby tant elle est vaste.

À noter qu'il existe un projet nommé SWIG dont le but est d'écrire pour vous le code C de l'extension à partir des prototypes des fonctions C que vous souhaitez interfacer. Il n'est pas propre au langage Ruby car cet outil est capable de faire la même chose pour d'autres langages tels PHP, Python, Perl, Java, etc.

Concernant la distribution de son extension, il existe des outils qui ne sont pas abordés par le présent tutoriel dont les plus importants : la diffusion sous forme de gemme qui requiert une démarche sur RubyForge (on peut éventuellement s'en passer si votre extension ne présente aucune dépendance vis à vis d'autres gemmes mais pour vos utilisateurs il est tout de même plus facile de ne pas avoir à gérer de multiples serveurs non standards) et Rake, pour écrire son Makefile en Ruby (tant qu'à faire) vous permettant d'effectuer les différentes tâches de compilation, installation, génération de la documentation ou que sais-je encore …

On aurait pu ici faire le parallèle avec l'extension nommée RubyInline qui vous permet d'intégrer du code C dans vos scripts Ruby mais l'exécution en sera plus lente et le code C qu'il est possible d'employer s'avère plutôt limité.

Les exemples de ce tutoriel ont été écrits soit sous GNU/Linux Gentoo 2007.0 soit sur FreeBSD 6.2 sur lesquelles ont été successivement installées les versions stables de Ruby (1.8.5 et 1.8.6).

Liens externes :

langages/ruby/etendre-ruby-en-c.txt · Dernière modification: 08/12/2014 16:28 (modification externe)