Appels de méthodes déterminés dynamiquement

Julien Jorge

Cet article présente une technique pour appeler des méthodes de certaines classes dans un système de type « script ». Dans une telle situation, les méthodes et leurs paramètres ne sont connus qu'à l'exécution, sous la forme de chaînes de caractères.
Commentaires, conseils et réactions dans cette discussion : Commentez ♪

- Titre : Appels de méthodes déterminés dynamiquement

- Auteur : Julien Jorge

- Parution : 27 janvier 2010

- Licence : Copyright 2010 Julien Jorge. Cet article est mis à disposition sous les termes de la licence Creative Commons paternité, partage à l'identique, dans sa version 3.0.

Sommaire

- 1. Présentation du système

- 1-1. Exemple introductif

- 1-2. Vue globale

- 2. Les éléments génériques

- 2-1. Classe de base pour exporter les méthodes

- 2-2. Exécuter une méthode d'une dérivée de base_exportable

- 2-3. Implémentation de l'appel d'une méthode

- 2-4. Associer un method_caller à un nom de méthode

- 2-4-1. Types et méthodes nécessaires à l'export

- 2-4-2. Ajout de méthodes à la table

- 3. Utilisation du système

- 4. Conclusion

1 - Présentation du système

Dans le cadre de votre projet, vous avez peut-être la nécessité d'appeler des méthodes de vos classes en ne connaissant leur nom et la valeur de leurs paramètres que sous forme de chaîne de caractères. Cela arrive quand la méthode à appeler est donnée par l'utilisateur ou lorsque des commandes sont lues dans un fichier.

Cet article décrit une technique pour « exporter » le nom des méthodes d'une classe. Le cœur du système tourne autour d'une classe de base pour toutes celles que l'on voudra exporter. Cette classe est paramétrée par le contexte d'exécution, qui pourra contenir des informations utiles dans la conversion des paramètres depuis une chaîne de caractères vers un type donné.

1-1 - Exemple introductif

Imaginons un logiciel de traitement d'image par lot, nommé batch-img, pour lequel la classe d'image propose ces opérations classiques :

class image
{
public:
/* Applique une rotation d'angle a à l'image, autour du centre aux
coordonnées (cx, cy). */
void tourner( double a, int cx, int cy );

/* Retourne l'image horizontalement. */
void miroir();

/* Copie l'image img sur l'image courante, à la position (x, y). */
void copier( image* img, int x, int y );

}; // class image

On aimerait utiliser ce logiciel en ligne de commande pour effectuer des traitements sur des images. En particulier, on aimerait que l'utilisateur puisse écrire des petits scripts comme celui ci-dessous, qu'il passerait au programme.

traitement.txt:
image_1 miroir
image_2 tourner 18.6 24 -16
image_1 copier image_2 700 30
image_1 miroir

L'utilisateur pourrait appliquer ce script à des images avec une commande comme celle-ci :

batch-img image_1=fred.jpg image_2=moscou.jpg traitement.txt

1-2 - Vue globale

Nous allons créer une classe base_exportable de laquelle devront hériter celles pour lesquelles nous voudrons exporter des méthodes. À côté de cela, nous écrirons une classe method_caller dont le rôle sera, à partir de chaînes de caractères représentant les valeurs des paramètres, d'appeler une méthode d'un base_exportable en lui passant les bonnes valeurs. Ainsi, la classe base_exportable contiendra une table statique qui associera à un nom de méthode une instance de method_caller. Lorsque nous voudrons appeler une méthode, il suffira de retrouver cette instance et de lui passer les valeurs des paramètres. method_caller sera redéfinie selon le nombre de paramètres à passer à la méthode. Cet exemple se limitera à trois paramètres, mais l'ajout d'un method_caller pour quatre paramètres ou plus est simple.

Toute la difficulté consiste à choisir automatiquement la classe dérivée de method_caller et les conversions de types en ne demandant que le nom de la méthode les types des paramètres à l'utilisateur de notre outil.

2 - Les éléments génériques

Cette section présente les éléments au cœur du système.

2-1 - Classe de base pour exporter les méthodes

base_exportable est la classe de base de toutes celles pour lesquelles nous voulons exporter des méthodes. Elle contient une méthode base_exportable::execute() qui permettra de retrouver une méthode à partir de son nom et de l'appeler.

Les paramètres de la méthode appelée sont passés sous forme de chaînes de caractères et seront convertis vers des valeurs du type réel des paramètres. Par défaut, cette conversion utilise la lecture formatée d'un std::istringstream. Cependant, dans certaines situations, l'utilisateur peut vouloir gérer lui-même la conversion. Par exemple, dans le cas de batch-img, les images sont nommées. On aimerait donc que celles passées en paramètre aux méthodes soient retrouvées à partir de leur nom. Pour autoriser ce genre de situation, la classe base_exportable est paramétrée par un type <Context>, qui sera ensuite transmis à la classe string_to_arg ci-dessous. Le rôle de cette dernière est de convertir une chaîne de caractères représentant un paramètre en la valeur réelle du paramètre, via une méthode statique nommée convert. En spécialisant cette classe, nous pourrons ajuster la conversion en fonction du contexte.

/** Convertit un paramètre une valeur de type T. */
template<typename Context, typename T>
class string_to_arg
{
public:
static T convert( const Context& context, const std::string& arg )
{
T result;
std::istringstream iss(arg);
iss >> result;
return result;
}
}; // class string_to_arg

Le squelette de la classe base_exportable prenant en compte le contexte est présenté ci-dessous. Nous le complèterons dans la suite de l'article.

template<typename Context>
class base_exportable
{
public:
// Le type du contexte.
typedef Context script_context_type;

/** Exécute une méthode de la classe. \a n est le nom de la méthode, \a args
ses paramètres. \a context est le contexte d'exécution. */
void execute( const std::string& n, const std::vector<std::string>& args,
const script_context_type& context );

}; // class base_exportable

2-2 - Exécuter une méthode d'une dérivée de base_exportable

La classe method_caller permet de faire la transition entre un appel en chaînes de caractères et l'appel effectif d'une méthode. Des instances seront stockées dans une table statique de base_exportable pour faire la correspondance.

Comme nous ne savons pas encore, à ce niveau, de quel type est l'instance sur laquelle l'appel doit se faire, nous devrons nous contenter de la déclaration ci-dessous.

template<typename Context>
class method_caller
{
public:
typedef Context script_context_type;

public:
/* Exécute une méthode de 'self' en lui passant les paramètres
de 'args' convertis selon le contexte 'context'. */
virtual void execute
( base_exportable<script_context_type>* self,
const std::vector<std::string>& args,
const script_context_type& context ) const = 0;

}; // class method_caller

La méthode method_caller::execute est abstraite, ce qui signifie que nous laissons le soin d'effectuer l'appel à une classe dérivée, qui aura plus d'informations sur le type de la classe. Cette classe dérivée sera explicit_method_caller.

template<typename SelfClass>
class explicit_method_caller:
public method_caller<typename SelfClass::script_context_type>
{
public:
typedef typename SelfClass::script_context_type script_context_type;

public:
/* explicit_execute va effectivement appeler la méthode sur une instance de
SelfClass. Cette méthode sera définie dans des classes dérivées
pour gérér la conversion des paramètres et leur nombre. */
virtual void explicit_execute
( SelfClass& self, const std::vector<std::string>& args,
const script_context_type& context )  const = 0;

private:
virtual void execute
( base_exportable<script_context_type>* self,
const std::vector<std::string>& args,
const script_context_type& context ) const
{
SelfClass* s = dynamic_cast<SelfClass*>(self);

if ( s!=NULL )
explicit_execute(*s, args, context);
}
}; // explicit_method_caller

Là encore, comme nous ne connaissons pas la méthode à appeler, nous laissons le soin à une classe dérivée de faire l'appel. La classe explicit_method_caller ne sert donc qu'à convertir l'instance de base_exportable vers son type réel.

Il ne nous reste plus qu'à faire des dérivées de explicit_method_caller. Ces classes seront paramétrées par la méthode à appeler, définie par son type de retour, le type de ses paramètres et son adresse. Nous allons en faire une pour chaque nombre de paramètres. Nous ne pouvons pas en faire une infinité, mais il sera facile d'ajouter une nouvelle classe lorsque l'utilisateur de notre système l'utilisera avec un nombre de paramètres non prévu. Les cas d'une méthode sans paramètres et d'une méthode avec deux paramètres sont présentés dans la section suivante.

2-3 - Implémentation de l'appel d'une méthode

Nous allons maintenant créer les patrons de classes qui appellent effectivement les méthodes. Les types paramétrés de cette classe entrent dans trois catégories : le type de l'objet sur lequel la méthode est appelée, la signature de ladite méthode, représentée par ses paramètres et sa valeur de retour, puis l'adresse de la méthode. La version la plus simple appelle une méthode sans paramètres et est présentée ci-dessous :

1. /** Cette classe définit un dérivé de explicit_method_caller pour appeler une

2. méthode donnée d'une classe donnée. Cette dernière est définie par

3. SelfClass. L'adresse de la méthode est Member et elle ne doit prendre

4. aucun paramètre et retourne une valeur de type R. */

5. template< typename SelfClass, typename R,

6. R (SelfClass::*Member)() >

7. class method_caller_args_0

8. {

9. public:

10. // Le type de la méthode appelée

11. typedef R (SelfClass::*mem_fun_type)();

13. // Le type du contexte dans lequel la méthode est appelée

14. typedef typename SelfClass::script_context_type script_context_type;

16. public:

17. /** Cette classe appelle la méthode voulue sur un objet passé en

18. paramètre. */

19. class caller_type:

20. public explicit_method_caller<SelfClass>

21. {

22. private:

23. /** L'appel de la méthode Member s'effectue ici, sur l'objet self,

24. en lui passant les paramètres du tableau args. */

25. void explicit_execute

26. ( SelfClass& self, const std::vector<std::string>& args,

27. const script_context_type& context ) const

28. {

29. const mem_fun_type member(Member);

30. (self.*member)();

31. }

32. }; // class caller_type;

34. public:

35. /** Il suffit d'utiliser cet objet pour appeler la méthode. */

36. static const caller_type s_caller;

38. }; // class method_caller_args_0

L'appel de la méthode se fait enfin, dans caller_type::explicit_execute(), au niveau des lignes 29 et 30. Ici, la méthode ne prend pas de paramètres. Ainsi, ''args'' devrait être vide. Le contexte n'est pas utilisé ici, mais il l'est dans la classe ci-dessous, à peine plus complexe, implémentant l'appel d'une méthode à deux paramètres :

/** Cette classe définit un dérivé de explicit_method_caller pour appeler une
méthode donnée d'une classe donnée. Cette dernière est définit par
SelfClasse. L'adresse de la méthode est Member et doit prendre deux
paramètres de types A1 et A2. */
template< typename SelfClass, typename R, typename A0, typename A1,
R (SelfClass::*Member)(A0, A1) >
class method_caller_args_2
{
public:
// Le type de la méthode appelée
typedef R (SelfClass::*mem_fun_type)(A0, A1);

// Le type du contexte dans lequel la méthode est appelée
typedef typename SelfClass::script_context_type script_context_type;

public:
class caller_type:
public explicit_method_caller<SelfClass>
{
private:
/** L'appel de la méthode Member s'effectue ici, sur l'objet self,
en lui passant les paramètres du tableau args. Les chaînes de
caractères de ce dernier sont converties dans le type des
paramètres en utilisant le contexte. */
void explicit_execute
( SelfClass& self, const std::vector<std::string>& args,
const script_context_type& context ) const
{
const mem_fun_type member(Member);
(self.*member)
( string_to_arg<script_context_type, A0>::convert(context, args[0]),
string_to_arg<script_context_type, A1>::convert(context, args[1]) );
}
}; // class caller_type;

public:
/** Une seule instance de caller_type est suffisante dans la mesure où il
n'y a pas de variables membres (s_caller ne sert qu'à faire la
transition vers la méthode réelle). On pourra ainsi utiliser son adresse
et éviter des allocations dynamiques. */
static const caller_type s_caller;

}; // class method_caller_args_2

Cette classe est similaire à la précédente, sauf qu'elle appelle une méthode avec deux paramètres. Nous pouvons voir que les chaînes de caractères du tableau ''args'' sont converties dans le type des paramètres en utilisant le contexte. Cela nous permettra de faire la conversion vers un objet de type image à partir de son nom. En dehors de cela, la difficulté est identique à la classe précédente.

Remarquons qu'il est nécessaire de faire une nouvelle classe de ce type pour chaque nombre de paramètres. Pour cela, nous pouvons utiliser la bibliothèque Boost.Preprocessor. Celle-ci nous permettra de générer les classes method_caller_args_X automatiquement, pour tout X dans un intervalle donné.

2-4 - Associer un method_caller à un nom de méthode

Pour résoudre le problème de l'export des méthodes, nous allons partir du résultat. Pour la méthode image::tourner(double, int, int), par exemple, nous aimerions ne rien avoir à dire d'autre que « je veux exporter la méthode tourner de la classe image, ses paramètres sont un réel, un entier et un autre entier ».

L'idéal, en effet, serait de n'avoir à indiquer que le nom de la méthode, le type des paramètres et la classe. À partir de ces indications, le système doit pouvoir associer une méthode à une instance adéquate de method_caller.

Pour que notre système puisse appeler une méthode à partir de son nom, il faut que l'association nom/méthode soit stockée quelque part. Un endroit correct semble être dans une table de la classe concernée. De plus, comme la méthode est indépendante de l'instance de la classe, il s'agit bien d'une table statique.

La première partie, ci-après, présente le type de cette table et les méthodes nécessaires pour bien l'utiliser. La seconde partie présente l'ajout de méthodes à cette table.

2-4-1 - Types et méthodes nécessaires à l'export

Pour chaque méthode exportée, une instance du method_caller adéquat est associée au nom de la méthode dans un std::map. Nous stockons aussi une référence vers la table de la classe mère, pour pouvoir remonter dans la hiérarchie lors de la recherche d'une méthode. Toutes ces informations sont stockées dans la structure method_list ci-dessous :

/** La liste des méthodes appelables. */
template<typename Context>
struct method_list
{
/** La liste de la classe mère. Cela implique qu'il n'y a pas d'héritage
multiple pour l'instant. */
method_list<Context> const* parent;

/** La table des exécuteurs. */
std::map<std::string, method_caller<Context> const*> data;

}; // struct method_list

Enfin, pour simplifier la déclaration de la table des exécuteurs, nous définissons une macro pour l'effectuer. Cette macro est à utiliser dans la définition de toutes les classes pour lesquelles nous voulons exporter des méthodes. Elle suppose que le type script_context_type est un alias du type du contexte passé aux exécuteurs. Elle déclare trois éléments :

- une variable membre statique de type method_list<Context>, définie ci-dessus ;

- une méthode statique void self_methods_set( const std::string&, method_caller<Context> const* ) permettant d'ajouter une méthode dans le method_list de cette classe ;

- une méthode virtuelle method_list<Context> const* get_method_list() const qui retourne la liste des méthodes exportées définies dans le premier point.

Du fait que cette dernière méthode est virtuelle, nous pourrons récupérer depuis base_exportable::execute() la liste des méthodes exportées de la classe la plus basse dans la hiérarchie. Si la méthode demandée n'est pas dans cette liste, nous pourrons accéder à la liste de la classe mère en passant par method_list<Context>::parent.

La première partie de cette macro est définie ci-dessous :

/** Cette macro, déclare les variables membres statiques utilisées pour
stocker les exécuteurs. Elle est appelée automatiquement par
DECLARE_METHOD_LIST. */
#define DECLARE_METHOD_LIST_BASE                                        \
protected:                                                            \
typedef method_list<script_context_type> method_list_type;            \
typedef                                                               \
std::map<std::string, method_caller<script_context_type> const*>      \
method_list_data_type;                                                \
\
static method_list_type s_method_list;                                \
\
static void self_methods_set                                          \
( const std::string& name,                                            \
method_caller<script_context_type> const* m )                       \
{ s_method_list.data[name] = m; }                                     \
\
private:                                                              \
virtual method_list_type const* get_method_list() const               \
{                                                                     \
init_method_list();                                                 \
return &s_method_list;                                              \
}

Remarquons la présence d'un appel à une méthode init_method_list depuis get_method_list. Cette méthode a pour rôle d'initialiser la table s_method_list. Nous avons alors besoin de connaître la classe mère pour initialiser le membre method_list<Context>::parent. Or nous ne savons pas ici quelles sont les méthodes à exporter. Nous demanderons donc le nom d'une méthode statique à appeler, qui se chargera de faire des appels à self_methods_set pour ajouter les méthodes. Là encore, nous masquons tout cela dans une macro définie ci-dessous :

/** Cette macro, appelée dans la définition d'une classe, déclare les
variables membres statiques utilisées pour stoker les exécuteurs.

\param parent_class Le type de la classe mère (héritant de ou étant
base_exportable)
\param export_func Une méthode statique à appeler pour initialiser
la liste des exécuteurs.
*/
#define DECLARE_METHOD_LIST(parent_class, export_func)                  \
DECLARE_METHOD_LIST_BASE                                              \
\
static void init_method_list()                                        \
{                                                                     \
if (s_method_list.parent == NULL)                                   \
{                                                                 \
parent_class::init_method_list();                               \
s_method_list.parent = &parent_class::s_method_list;            \
export_func();                                                  \
}                                                                 \
}

Remarquons que cette macro appelle la précédente. Elle sera donc la seule à devoir être appelée dans la définition des classes de l'utilisateur.

Seule la macro DECLARE_METHOD_LIST_BASE sera appelée dans base_exportable. En effet, celle-ci va contenir une table de méthodes exportées et doit définir la méthode virtuelle get_method_list(). Cependant, elle n'a pas de classe mère à passer à DECLARE_METHOD_LIST. Nous devrons alors définir la méthode base_exportable<Context>::init_method_list directement dans la classe.

Enfin, la table s_method_list doit être implémentée quelque part, ce que l'utilisateur fera en appelant la macro ci-dessous dans le fichier d'implémentation de sa classe.

/** Cette macro est à utiliser dans le fichier d'implémentation de votre
classe. */
#define IMPLEMENT_METHOD_LIST(self_type)        \
method_list<self_type::script_context_type> self_type::s_method_list;

Nous pouvons maintenant compléter la classe base_exportable en déclarant une table d'exécuteurs et en écrivant la méthode execute :

/** Cette classe est la base de toutes les classes pouvant exécuter des
commandes. */
template<typename Context>
class base_exportable
{
public:
// Le type du contexte. Déclaration indispensable pour l'utilisation de
// DECLARE_METHOD_LIST ci-après.
typedef Context script_context_type;

DECLARE_METHOD_LIST_BASE

public:
/** Destructeur indispensable pour pouvoir utiliser le dynamic_cast dans
explicit_method_caller. */
virtual ~base_exportable() {}

/** Exécute une méthode de la classe. \a n est le nom de la méthode, \a args
ses paramètres. \a context est le contexte d'exécution. */
void execute( const std::string& n, const std::vector<std::string>& args,
const script_context_type& context )
{
// on récupère la liste des exécuteurs dans la classe la plus basse dans
// la hiérarchie.
const method_list_type* m( get_method_list() );
typename method_list_data_type::const_iterator it(m->data.find(n));

bool stop(false);

while( !stop )
if ( it == m->data.end() )
{
// On n'a pas trouvé la méthode, on passe alors à la
// liste de la classe mère, si elle existe.
if ( m->parent != NULL )
{
m = m->parent;
it = m->data.find(n);
}
else
{
std::cout << "Method '" << n << "' not found." << std::endl;
stop = true;
}
}
else
{
// On a trouvé la bonne méthode, on l'exécute.
stop = true;
it->second->execute(this, args, context);
}
}

}; // class base_exportable

base_exportable::execute commence par récupérer les méthodes exportées par la classe la plus basse dans la hiérarchie. Elle y cherche la méthode demandée puis l'exécute. Si la méthode demandée n'est pas exportée par cette classe, elle effectue la recherche dans la classe mère, jusqu'à la trouver ou arriver en haut de la hiérarchie.

La dernière étape est d'ajouter des méthodes dans la table des méthodes exportées. Cela est le sujet de la section suivante.

En pratique, la gestion des méthodes est similaire au système des vtables et vpointers créés par le compilateur pour les méthodes virtuelles. Ici, la table s_method_list correspondrait à la vtable tandis que la méthode get_method_list serait le vpointer de l'instance. Par conséquent, il est possible ici de rendre virtuelles des méthodes qui ne le sont pas. Par exemple, supposons une classe base ayant une méthode f non virtuelle et une classe dérivée héritant de base et redéfinissant la méthode f. Si ces deux classes exportent cette méthode sous le même nom, alors celle de dérivée sera appelée sur toute instance de dérivée, même en passant par le type base. En pratique, il est tout à fait possible de masquer n'importe quelle méthode d'une classe mère en exportant une autre méthode avec le même nom.

2-4-2 - Ajout de méthodes à la table

L'ajout d'une méthode dans la table se fait en appelant base_exportable<Context>::self_methods_set() lors de l'appel de la méthode donnée en second paramètre à la macro DECLARE_METHOD_LIST. Il suffit de lui donner le nom et le method_caller adéquat, obtenus à partir des classes method_caller_args_0 et similaires.

Par exemple, dans le cadre de la classe image, le résultat pourrait ressembler au code ci-dessous :

class image:
public base_exportable<my_context>
{
DECLARE_METHOD_LIST(base_exportable<my_context>, set_exported)

private:
void miroir();
void tourner( double a, int cx, int cy );
void copier( const image& img, int x, int y );

private:
static void set_exported()
{
self_methods_set
( "miroir",
&method_caller_args_0<image, void, &image::miroir>::s_caller );
}
}; // class image

Écrit de cette façon, l'ajout de la méthode n'est pas très élégant. D'une part, le nom de la méthode est à indiquer plusieurs fois, d'autre part, l'utilisation de s_caller n'est peut-être pas intuitive. Nous allons donc à nouveau utiliser des macros pour simplifier la syntaxe.

Le code ci-dessous est celui de la macro déclarant un method_caller_args_0, pour appeler une méthode sans paramètres. Le premier paramètre est le type de la classe exportant la méthode, le second est le nom de la méthode et le troisième est son type de retour.

/** Cette macro est utilisée dans l'implémentation d'une classe dérivant de
base_exportable pour ajouter un exécuteur d'une méthode sans paramètres.
*/
#define CONNECT_METHOD_0( self_type, method_name, R )           \
self_methods_set                                              \
( #method_name,                                               \
&method_caller_args_0                                       \
<                                                           \
self_type,                                                  \
R,                                                          \
&self_type::method_name>::s_caller )

Pour reprendre l'exemple précédent, il suffit d'utiliser cette macro dans la méthode image::set_exported pour exporter la méthode :

class image:
public base_exportable<my_context>
{
DECLARE_METHOD_LIST(base_exportable<my_context>, set_exported)

private:
void miroir();
void tourner( double a, int cx, int cy );
void copier( const image& img, int x, int y );

private:
static void set_exported()
{
CONNECT_METHOD_0( image, miroir, void );
}
}; // class image

La macro ci-dessous effectue la même chose pour un method_caller_args_2. Nous pouvons remarquer deux nouveaux paramètres, qui sont les types des paramètres de la méthode à appeler.

/** Cette macro est utilisée dans l'implémentation d'une classe dérivant de
base_exportable pour ajouter un exécuteur d'une méthode ayant deux
paramètres, de type T1 et T2. */
#define CONNECT_METHOD_2( self_type, method_name, R, T1, T2 )   \
self_methods_set                                              \
( #method_name,                                               \
&method_caller_args_2                                       \
<                                                           \
self_type,                                                  \
R, T1, T2,                                                  \
&self_type::method_name >::s_caller )

Comme pour la définition de method_caller, il faudra déclarer une macro pour chaque nombre de paramètres des méthodes à exporter.

3 - Utilisation du système

Tous les éléments du système d'export ont maintenant été déclarés, il ne reste plus qu'à les utiliser. Nous utiliserons un contexte nommé my_context pour les classes exportables de notre programme, du type my_exportable déclaré comme suit :

/** my_context est le contexte d'exécution de nos scripts, transportant les
informations dont nous avons besoin pour leur exécution (voir
my_context.hpp). */
class my_context;

/** La classe de base des classes que nous voulons exporter. */
typedef script::base_exportable<my_context> my_exportable;

Dans l'exemple situé au début de l'article, nous pouvons remarquer que des images sont passées en paramètre à des méthodes et identifiées par leur nom. Pour pouvoir faire la conversion de ce nom en une image en mémoire, nous allons devoir faire l'association dans notre contexte et spécialiser la classe string_to_arg.

Le contexte se présente alors comme suit :

/* prédéclaration de la classe des images pour pouvoir l'utiliser dans
my_context. */
class image;

/** Un contexte spécifique à notre programme de traitement d'images. */
class my_context
{
public:
/** Associe une image à un nom. */
void add( const std::string& n, image* img )
{
m_images[n] = img;
}

/** Récupère l'image associée à un nom. */
image* get_image( const std::string& n ) const
{
m_images.find(n)->second;
}

private:
/** Les images manipulées. */
std::map<std::string, image*> m_images;

}; // class my_context

Enfin, pour que les paramètres de type image soient retrouvés automatiquement à partir de leurs noms, nous effectuons la spécialisation ci-dessous de string_to_arg.

/** Spécialisation pour retourner une image à partir de son nom. On fait une
prédéclaration ici pour que le compilateur sache qu'elle existe. */
template<>
class string_to_arg<my_context, const image&>
{
public:
static const image&
convert( const my_context& context, const std::string& arg )
{
return *context.get_image(arg);
}
}; // string_to_arg

Le contexte étant défini, il ne reste plus qu'à exporter les méthodes de la classe image. Dans le fichier d'entête :

class image:
public my_exportable
{
DECLARE_METHOD_LIST(my_exportable, set_exported)

private:
void miroir();
void tourner( double a, int cx, int cy );
void copier( const image& img, int x, int y );

static void set_exported()
{
CONNECT_METHOD_0( image, miroir, void );
CONNECT_METHOD_3( image, tourner, void, double, int, int );
CONNECT_METHOD_3( image, copier, void, const image&, int, int );
}
}; // class image

Dans le fichier source, nous devons aussi ajouter l'appel de macro ci-dessous pour implémenter la liste des méthodes exportées :

IMPLEMENT_METHOD_LIST(image)

Et voilà, les méthodes de la classe image peuvent être aisément appelées sous forme de texte. Pour nous en convaincre, essayons ce petit programme :

int main()
{
image img1, img2;
my_context context;

context.add("image_1", &img1);
context.add("image_2", &img2);

std::vector<std::string> args;
args.push_back("image_2");
args.push_back("24");
args.push_back("18");

img1.execute("copier", args, context);

return 0;
}

4 - Conclusion

Nous avons mis en place un système permettant d'appeler des méthodes de certaines classes en donnant sous forme de chaînes de caractères leur nom et leurs paramètres. Ce système remplit correctement sa tâche, son utilisation reste très simple et permet d'avoir des résultats satisfaisants rapidement, pour peu que les classes d'exécuteurs de méthodes soient correctement déclarées, de même que les macros permettant d'exporter les méthodes. En particulier, nous avons utilisé ce système pour ajouter des scripts dans notre jeu Plee the Bear.

Cependant, ce système a plusieurs inconvénients. Tout d'abord, nous pouvons remarquer que la valeur de retour des fonctions exportées est totalement ignorée et perdue dans les appels. De plus, le système est intrusif : il est nécessaire que les classes exportées héritent de base_exportable. Ainsi, il n'est pas possible d'exporter des classes que l'utilisateur ne peut modifier (objets d'une bibliothèque annexe, par exemple). Le lecteur souhaitant un système moins intrusif pourra s'intéresser à CAMP. Si l'objectif est un système de script, l'utilisateur pourra aussi s'orienter vers LUA, pour lequel un tutoriel est présent sur developpez.com.

Je remercie en particulier 3DArchi et Laurent Gomilla pour leurs nombreux retours et commentaires sur le fond et la forme de cet article, ainsi que azertix pour la relecture de cet article.

L'archive appels-dynamiques.tar.gz contient le code du système, un lecteur de script et un exemple un petit peu plus poussé.