Extensions en C
Documentation rédigée par GuiguilinuxIntroduction
Comme la plupart des langages actuels, Ruby peut être étendu à l'aide de modules écrits en C, que ce soit pour des raisons de performances, ou simplement pour proposer l'accès à des bibliothèques existantes en C, telles que SDL, Gtk... Là où dans d'autres langages le processus devient vite synonyme de manipulations de pointeurs et de conversion de types délicates, l'API C de Ruby se veut simple et cohérente : chaque construction retrouve en C sa correspondance, et, mise à part quelques ajouts nécessaires pour faire se comprendre les deux mondes, on a l'impression d'écrire du Ruby en C plutôt que du C, d'où mon expression "Ruby/C" pour désigner ce "langage" :-) Nous allons, dans cette première partie, voir comment réaliser une version un peu élaborée du fameux "Hello, World !". Le code obtenu sera équivalent au code Ruby suivant, qui, je penses, se passe d'éclaircissements : une classe, deux méthodes d'instance et un accesseur :-pclass HelloWorld attr_accessor :msg def initialize @msg = "Hello, World !" end def saluer puts @msg end end
Prérequis
- Le sujet étant Ruby/C, vous aurez besoin de l'interpréteur Ruby (nonnn ?). Les exemples de ce tutoriel ont été testés sur la version 1.8.5, mais toute version pas trop ancienne devrait convenir :-) Attention, vous aurez également besoin des bibliothèques de développement associées : dans certaines distributions, elles sont disponibles sous la forme d'un paquet séparé, nommé le plus souvent ruby-dev. N'oubliez pas de l'installer, sinon ça ne marchera pas, et c'est logique, vu qu'il s'agit de développement :-p
- Vous aurez également besoin d'un compilateur C en état de marche : Vous pouvez seulement lire les exemples, mais je vous recommande de les taper et de les exécuter au fur et à mesure pour que ça rentre.
- Enfin, vous aurez besoin d'un éditeur de texte. S'il propose la coloration syntaxique Ruby et C, parfait. La cas échéant, vous pourrez tout de même taper vos programmes :-)
Définir la classe HelloWorld
Nous allons créer un fichier de code C nommé hello.c : il contiendra le code de notre exemple. Tout le long de ce tutoriel, nous allons utiliser les fonctions de l'API C fournie par Ruby. Pour que ça marche, il est souhaitable qu'elles soient définies, nous allons donc les inclure en premier lieu:#include <ruby.h>
Maintenant, cherchons celle qui nous intéresse : en lisant README.EXT, on repère vite une fonction nommée rb_define_class, pour définir une nouvelle classe. Sa syntaxe est la suivante :
VALUE rb_define_class(char* nomClasse, VALUE superClasse);
- nomClasse sera le nom porté par la classe en Ruby, de type chaîne de caractères, char*.
- superClasse, la classe parente de notre nouvelle classe, de type VALUE.
VALUE cHello = rb_define_class("HelloWorld", rb_cObjet);
Notez le "c" préfixant le nom de notre variable : ce prefix indique que l'objet reférencé est une classe, par convention. S'il s'était agi d'un module, cela aurait été "m", et "e" pour une classe d'exceptions. Où placer ce code ? Nous n'avons pas droit à la fonction main(), étant donné que nous programmons une librairie : en fait, quand il charge une extension, l'interpréteur Ruby cherche une fonction Init_<nom_extension> à exécuter, pour l'initialiser. Celle-ci ne prend pas d'arguments, et retourne void. Après l'avoir ajouté, on a donc le code suivant :
#include <ruby.h> VALUE cHello; void Init_hello() { cHello = rb_define_class("Hello", rb_cObject); }
rb_cObject est l'équivalent Ruby/C de la classe Ruby Object : rien de choquant, donc, C ne connaissant pas les valeurs par défaut :-p
Nous allons maintenant compiler notre extension, et tester notre début de classe. Pour nous éviter d'écrire le makefile à la main, assez long et complexe, Ruby nous fournit la bibliothèque mkmf : Il nous suffit donc, dans le même répertoire, de créer le fichier Ruby suivant (en général nommé extconf.rb)
require 'mkmf' create_makefile("hello")
Attention, le parametre de create_makefile définit la fonction C que l'interpréteur essaiera d'appeler au chargement de l'extension ! Exécutons notre script :
$ ruby extconf.rb
A l'aide du Makefile géneré, on construit l'extension :
$ make
On obtient alors un fichier nommé hello.so : c'est notre extension Ruby/C, que l'on peut maintenant essayer de charger avec require.
$ irb >> require 'hello' => true >> h = Hello.new => #<HelloWorld:0xb7c665c8>
Pour l'instant, notre classe marche, mais ne fait pas grand chose... Nous allons maintenant definir nos méthodes initialize et saluer.
Les méthodes initialize et saluer
En Ruby/C, définir une méthode consiste en fait à déclarer une fonction C qui sera appelée lors de la réception d'un message donné par l'objet. Celle-ci devra TOUJOURS retourner une VALUE, même s'il s'agit de Qnil (équivalent Ruby/C de nil). Elle prend en premier paramètre l'objet destinataire.
Ecrivons les fonctions C qui vont être utilisées :
VALUE hello_initialize(VALUE self) { // prototype : rb_iv_set(VALUE receveur, char* nomAttribut, VALUE valeurAttribut) rb_iv_set(self, "@msg", rb_str_new2("Hello, World !"); return self; }
Détaillons un peu : on a nommé l'objet courant self, comme en Ruby : on le retourne à la fin du constructeur, ce qui permet d'empiler les appels de méthodes tels que Hello.new.saluer Qu'est-ce que cette fameuse ligne rb_iv_set ... ? En fait, il s'agit de l'abbréviation de "rb_instance_variable_set" : cette fonction sert à définir la valeur d'une variable d'instance de l'objet, ici @msg. Son premier paramètre signifie que l'opération s'applique sur l'objet courant. Mais qu'est-ce que ce second paramètre ? En fait, rb_str_new2 crée un objet String Ruby à partir d'une chaîne C, étant donné que nous devons assigner un objet. Cette ligne est donc équivalente au Ruby :
@msg = "Hello, World :"
Associons maintenant cette fonction C au message Hello#initialize... Pour cela, on ajoute dans Init_hello() la ligne :
// prototype : rb_define_method(VALUE receveur, char* nomMessage, fonctionC, int nbArguments) rb_define_method(cHello, "initialize", hello_initialize, 0);
Voilà ! Maintenant, quand nous ferons Hello.new, @msg sera bien initialisée :-p L'autre fonction maintenant :
VALUE hello_saluer(VALUE self) { // prototype : rb_iv_get(VALUE receveur, char* nomAttribut) VALUE msg = rb_iv_get(self, "@msg"); // prototype : rb_funcall(VALUE receveur, ID nomMethode, int nbArguments, ...) rb_funcall(self, rb_intern("puts"), 1, msg); return self; }
Tiens, voilà du nouveau ! Qu'est-ce que cet appel a rb_funcall() ? Si vous vous rappelez du code Ruby équivalent extension, cité en début de tutoriel, il y a une ligne "puts @msg". Et bien, il s'agit ici du code Ruby/C équivalent :-) rb_funcall() appelle une méthode de l'objet spécifié, désignée par une variable de type ID, équivalent à la classe Ruby Symbol. Pour obtenir l'ID correspondant à un nom de méthode, on utilise rb_intern() : c'est en fait l'équivalent de #to_sym On précise également le nombre d'argument passés, suivi de ceux-ci (d'où les "..."). Cette ligne fera donc appel à Kernel#puts, et affichera le message à l'écran, comme désiré. On l'associe au message Hello#saluer :
rb_define_method(cHello, "saluer", hello_saluer, 0);
Et on recompile notre code, via make : ça marche !
$ irb >> require 'hello' => true >> h = Hello.new => #<Hello:0xb7c665c8 @msg="Hello, World"> >> h.saluer Hello, World ! => #<Hello:0xb7c665c8 @msg="Hello, World">Et voilà, c'est fini :-) Comment ça non ? Ah oui, c'est vrai : on ne peut pas modifier le message, j'avais prévu un accesseur dans mon code Ruby... Retour au code C pour l'ajouter :-) On utilise
rb_define_attr(VALUE receveur, char* nom, int read, int write)pour définir un accesseur en Ruby/C : Si _read_ est différent de 0, l'attribut sera acessible en lecture, si _write_ est différent de 0, en écriture. On ajoute donc la ligne suivante dans Init_hello() :
rb_define_attr(cHello, "msg", 1, 1);Maintenant, on à notre accesseur :
$ irb >> require 'hello' => true >> h = Hello.new => #<Hello:0xb7c665c8 @msg="Hello, World"> >> h.msg = "salut !" => "salut !" >> h.saluer salut ! => #<Hello:0xb7c665c8 @msg="salut !">

