Tcl / Tk
Interfaçage C et tcl
Auteur :
Arnaud LAPREVOTE
Linbox Free&Alter Soft
152, rue de Grigy
57070 METZ
tel: 03 87 50 87 90 - 06 11 36 15 30
fax : 03 87 75 19 26
Email : arnaud.laprevote@linbox.com
1. Introduction
Une grande partie de ce qui est écrit ici s'inspire de manière très directe de l'excellent court article que l'on trouve sur le site web de La Rochelle Innovation consacré au tcl/tk. L'url est la suivante : http://www.larochelle-innovation.com/tcltk/278 écrit par Stéphane Padovani . L'autre grande source d'informations vient du livre "Practical Programming in Tcl/Tk" ISBN: 0-13-038560-3 par Brent Welch <welch@acm.org>, Ken Jones, et Jeff Hobbs.2 grandes possibilités existent :
- intégrer un interpréteur tcl dans un programme existant,
- ajouter des instructions au tcl.
Nous allons commencer par nous intéresser à ce dernier problème. Pour ce faire nous allons partir d'un exemple simple. Nous allons créer une librairie C qui contiendra des commandes Tcl.
2. Qu'est-ce qu'une librairie ?
Une librairie est un fichier, contenant un ensemble de fonctions, correspondant souvent à un même thème. Il peut s'agir d'une librairie de fonctions mathématiques, d'une libraire de fonctions graphiques ... Il existe deux catégories de librairies : les librairies statiques et les librairies dynamiques (LD). Les libraires statiques sous unix ont pour extension .a tandis que sous windows, l'extension est .lib. Elles sont liées au programme lors de la compilation plus précisemment lors de l'édition des liens. Les librairies dynamiques sont des fichiers d'extension .so sous unix et .dll sous windows.Ces librairies ne sont pas liées au programme lors de la compilation mais à chaque exécution du programme ; la librairie est chargée en mémoire ( si elle n'y est pas déjà ) et se greffe au programme durant son exécution.
3. Ecrire une extension C pour Tcl/Tk
Pour ajouter de nouvelles commandes au langage Tcl/Tk, vous pouvez procéder de deux façons : Soit créer un nouvel interpréteur Tcl/Tk, intégrant ces nouvelles commandes ; c'est par exemple le cas de l'interpréteur http://tix.sourceforge.net/dist/current/man/html/UserCmd/tixwish.htm. Soit construire des librairies dynamiques contenant les nouvelles commandes, puis charger ces librairies dans l'interpréteur, par l'intermédiaire de la commande load. La librairie est alors montée en mémoire et le code est "greffé" à celui de l'interpréteur. C'est la seconde option qui nous intéresse ici. Les commandes ajoutées doivent être décrites à travers une API C (Application Personnal Interface) propre à Tcl/Tk. Le nombre de fonctions contenues dans cette API est assez énorme (plus d'une centaine), néanmoins nous verrons qu'il est possible de s'en sortir avec quelques fonctions et surtout, sans avoir à se transformer en K3rN31 h4><0r les nuits de pleines lunes.3.1. Un exemple simple
Nous allons créer une LD déclarant une variable A_dans_le_code_C entière (int) et deux commandes Tcl permettant d'accéder à cette valeur à partir de l'interpréteur : get_A_in_C_prog qui donne la valeur de A_dans_le_code_C. set_A_in_C_prog nouvelle_valeur qui affecte la valeur nouvelle_valeur à la variable A_dans_le_code_C. Les commandes sont créées par un appel à la fonction Tcl_CreateCommand(). Un appel simplifié est de la forme :
Tcl_CreateCommand(
interp,/*interpreteur chargeant la LD*/
"get_A_in_C_prog",/*non de la commande pour l'interpreteur*/
displayVariable_A,/*fonction C a invoquer quand la commande est appelee*/
NULL, /*client data, inutile de s'en soucier dans la majorite des cas*/
NULL/*fonction C a invoquer si la commande est detruite*/
)
interp est une variable de type Tcl_Interp*, il s'agit d'un pointeur sur l'interpréteur chargeant la librairie. displayVariable_A est la fonction C à invoquer quand la commande get_A_in_C_prog est invoquée en tcl.
int (nom de la fonction)( ClientData clientdata, Tcl_Interp* interp, int argc, char **argv )En analogie avec la fonction main du C, argc est le nombre d'arguments passés à la commande Tcl, tandis que argv est un tableau contenant ces arguments.
Le troisième paramètre de Tcl_CreateCommand, de type Clientdata mériterait de plus longs développements et fera l'objet d'une page détaillée dans un avenir proche . Il reste encore un point vraiment essentiel : le point d'entrée de la LD. Supposons que votre LD s'appelle maLib.dll, lors du chargement de la LD, l'interpréteur va chercher une fonction nommée maLib_Init qu'il executera. Le nom de cette fonction est nécessairement la concaténation du nom de la LD et de "_Init". Son prototype est le suivant : int (nom de la fonction)( Tcl_interp *interp) . Vous devez placer dans cette fonction, toutes les fonctions à exécuter lors du chargement de la LD. Il peut - par exemple - s'agir de la déclaration des nouvelles commandes.
#include <stdio.h>
#include <stdlib.h>
#include <tcl.h>
#include <tk.h>
/* assure la compatibilité avec les divers compilateurs */
#if defined(__WIN32__)
# define WIN32_LEAN_AND_MEAN
# include <windows.h>
# undef WIN32_LEAN_AND_MEAN
# if defined(_MSC_VER)
# define EXPORT(a,b) __declspec(dllexport) a b
# else
# if defined(__BORLANDC__)
# define EXPORT(a,b) a _export b
# else
# define EXPORT(a,b) a b
# endif
# endif
#else
# define EXPORT(a,b) a b
#endif
EXTERN EXPORT(int,Example_Init)(Tcl_Interp *interp);
int setVariable_A (ClientData, Tcl_Interp*, int, char **);
int tcl_setVariable_A( Tcl_Interp *);
int tcl_displayVariable_A( Tcl_Interp *);
int displayVariable_A (ClientData, Tcl_Interp*, int, char **);
int A_dans_le_code_C = 0;
/* point d'entree de la librairie dynamique*/
/* interp est l'interpreteur chargeant la dll */
EXPORT(int,Example_Init)(Tcl_Interp *interp)
{
/* creer les commandes lors du chargement de la librairie dynamique */
tcl_setVariable_A(interp);
tcl_displayVariable_A(interp);
Tcl_PkgProvide(interp, "example", "1.0");
return TCL_OK;
}
/**************************************************/
/* Commande permettant d'affecter une valeur a A */
int
setVariable_A (
ClientData clientdata,/*inutile de s'en soucier dans les cas simples*/
Tcl_Interp* interp, /*interpreteur chargeant la dll, inutile de l'initialiser*/
int argc, /*nombre d'argument de la commande Tcl*/
char **argv /*listes des arguments de la commande*/)
{
if ( argc != 2 )
{
Tcl_AppendResult(
interp,
"nombre d'arguments incorrect. la syntaxe est : \"",
argv [0],
" valeur_entiere",
(char *) NULL
);
return TCL_ERROR;
}
A_dans_le_code_C = atoi( argv[1] );
Tcl_AppendResult(
interp,
"La variable A du code C, à maintenant la valeur ",
argv [1],
(char *) NULL );
return TCL_OK;
}
int
tcl_setVariable_A( Tcl_Interp *interp)
{
if ( Tcl_CreateCommand(
interp,/*interpreteur chargeant la dll, inutile de l'initialiser*/
"set_A_in_C_prog",/*non de la commande pour l'interpreteur*/
setVariable_A, /*fonction C a invoquer quand la commande est appelee*/
NULL, /*client data, inutile de s'en soucier dans la majorite des cas*/
NULL /*fonction C a invoquer si la commande est detruite*/
) == NULL )
{
return TCL_ERROR;
}
return TCL_OK;
}
/*************************************************/
/* commande permettant de lire A */
int
displayVariable_A(
ClientData clientdata,/*inutile de s'en soucier dans les cas simples*/
Tcl_Interp* interp, /*interpreteur chargeant la dll, inutile de l'initialiser*/
int argc, /*nombre d'argument de la commande Tcl*/
char **argv /*listes des arguments de la commande*/)
{
char buffer[6];
if (argc != 1)
{
Tcl_AppendResult(
interp,
"nombre d'arguments incorrect : \"",
argv [0],
(char *) NULL
);
return TCL_ERROR;
}
else
{
sprintf(buffer,"%d",A_dans_le_code_C);
Tcl_AppendResult(
interp,
buffer,
(char *) NULL
);
return TCL_OK;
}
}
int
tcl_displayVariable_A( Tcl_Interp *interp)
{
if ( Tcl_CreateCommand(
interp,/*interpreteur chargeant la dll, inutile de l'initialiser*/
"get_A_in_C_prog",/*non de la commande pour l'interpreteur*/
displayVariable_A,/*fonction C a invoquer quand la commande est appelee*/
NULL, /*client data, inutile de s'en soucier dans la majorite des cas*/
NULL /*fonction C a invoquer si la commande est detruite*/
) == NULL )
{
return TCL_ERROR;
}
return TCL_OK;
}
/*******************************************************/
3.2. Compiler
J'aborderai la compilation avec Visual C++, celle avec la distribution Cygwin ( http://sources.redhat.com/cygwin/ ) et je finirai par la compilation sous unix.3.2.1. Avec Visual C++
La principale difficulté est de configurer correctement les options du compilateur. Si vous créez un nouveau projet, il vous faut choisir le type de projet "Win32 dynamic-link library". Après avoir créé le projet, il faut positionner les options. Ca se passe dans l'écran ouvert à partir du menu project/settings : Dans l'onglet "C/C++", sélectionnez "preprocessor" dans la combo-box "category", puis - dans l'entrée "additionnal include directories" - indiquez l'emplacement du répertoire contenant les fichiers includes de Tcl/Tk ( par exemple : c: clinclude ). Dans l'onglet "Link", sélectionnez "input" dans la combo-box "category", puis - dans l'entrée "additionnal librairy path" - indiquez l'emplacement du répertoire contenant les librairies d'importation tkxx.lib et tclxx.lib ( par exemple : c: cllib ) ; xx est le numéro de version de votre distribution Tcl/Tk. Dans l'entrée "object/library modules", ajoutez tkxx.lib et tclxx.lib en les séparant d'un espace ( par exemple : tcl83.lib kernel32.lib user32.lib gdi32.lib ). Vos options sont maintenant correctement positionnées. A ce stade il est un point important à éclaircir. Windows utilisent deux types de librairies ayant la meme extension .lib : les librairies statiques et les librairies d'importation. Nous avons déjà parlé des librairies statiques. Les librairies d'importations sont associées aux librairies dynamiques. Elles ne contiennent pas de code mais juste les prototypes des fonctions de la librairie dynamique correspondante. Elles sont utilisées uniquement lors de la compilation, pour fournir les informations nécessaire au compilateur sur les librairie dynamiques dont dépend le programme. Si ces explications vous ont semblé confuses, vous pouvez télécharger ftp://ftp.larochelle-innovation.com/tcl/pado_dll_example.zip
3.2.2. Avec Cygwin
Je fournis un makefile cygwin en exemple : ftp://ftp.larochelle-innovation.com/tcl/pado_dll_makefile_cygwin Le point important est l'utilisation de la commande dllwrap qui permet de génerer la librairie. Attention : votre librairie est maintenant liée avec la librairie cygwin1.dll qui se trouve dans le répertoire /bin de votre distribution Cygwin. Lorsque vous distribuez votre programme, vous devez fournir cette librairie, qui devra être impérativement placée soit dans le répertoire du programme, soit dans c:/winnt (c:/windows), soit dans un des répertoires définis dans la variable d'environnement PATH.3.2.3. Compiler sous Unix
A nouveau, je vous donne un makefile : ftp://ftp.larochelle-innovation.com/tcl/pado_dll_linux_makefile en exemple. A noter, que la notion de librairie d'importation n'existe pas sous Unix. Vous linkez directement avec la librairie dynamique.3.3. Débugger une librairie dynamique
Votre libraire dynamique ne fonctionne pas, il vous faut donc DEBUGGER de toute urgence!3.3.1. Principe
L'idée est de réaliser un pado_dll_mainDebug qui crée un interpréteur et lance le script TCL. Ce programme est alors lancé à partir du debugger. Il vous suffit ensuite de placer les points d'arrêts dans le code de votre librairie.3.3.2. Avec Visual C++
En plus du projet correspondant à votre librairie, vous ajoutez à l'espace de travail un nouveau projet ; nommons le mainDebug. Après avoir sélectionné ce projet comme projet actif (project/set as active project), il vous faut positionner correctement les options du compilateur comme décrit plus haut mais en plus, il vous faut aussi déclarer la librairie à débugger. Pour ce faire : Sélectionner "project/settings" Dans l'onglet "DEBUG", selectionner "Additional DLL" dans la combobox "category" Dans la liste "locale", ajouter la ou les librairies appelées par votre script. Evidemment, débugger les librairies, suppose que vous les avez préalablement compilées en mode DEBUG. Ce fichier : ftp://ftp.larochelle-innovation.com/tcl/pado_dll_example_debug.zip vous montre comment débugger la librairie donnée précedemment en exemple.3.3.3. Avec Cygwin
Tout d'abord commencez par vous procurer un front-end pour le gnu-debugger 'http://www.gnu.org/software/gdb/'. Celui que j'utilise est 'http://libre.act-europe.fr/gvd/' (merci à l'ada-tholla Pascal ;-) ). Voici un exemple complet : ftp://ftp.larochelle-innovation.com/tcl/pado_dll_example_cygwin.tar.gz, il s'agit de débugger la librairie example.dll appelée par le script example.tcl.Décompresser le, $HOME désigne le répertoire dans lequel vous avez décompresser le fichier. Compiler le code en tapant la commande make dans le répertoire $HOME/cygwin_example/avecDebug/ (je suppose que vous êtes dans un shell cygwin). Lancer gvd, la fenêtre de gvd contient une zone permettant de taper directement les commandes de gdb, placez-vous dans cette zone et tapez les commandes suivantes.
(gdb) cd \$HOME/example_cygwin/avecCygwin (gdb) dll-symbols example.dll (gdb) file mainForDebug.exe
Par le menu File/Open Source..., chargez le fichier mainForDebug.cpp et placez un point d'arrêt à la ligne 37 return TCL_OK. Lancer le programme en cliquant sur le bouton "START". Le programme s'arrête alors au point d'arrêt, sélectionner le fichier example.cpp dans l'arborescence affichée dans la fenêtre de gvd, placer -par exemple- un point d'arrêt à la ligne 49 tcl_setVariable_A(interp);, presser le bouton Cont et le débugger s'arretera bien à la ligne 49 de la dll ( gagné! ;-) ). Je donne maintenant deux-trois mots d'explications. La ligne importante dans le fichier example.cpp, est la ligne 31 if ( Tcl_Eval( interprete, "load ./example.dll") == TCL_ERROR ) qui charge en mémoire la librairie dynamique example.dll. Bien que cette librairie soit chargée dans le script example.tcl, il est nécessaire de la charger avant de lancer le script, afin de pouvoir y placer un point d'arrêt. C'est pour cela que nous avons placé le point d'arrêt dans la dll uniquement après que le débugger se soit arrêter au point d'arrêt placer sous la ligne 31 chargeant la librairie.



