Plan du site  
pixel
pixel

Articles - Étudiants SUPINFO

Introduction au dialogue entre C et Lua et création d’une surcouche C++ de l’API C existante

Par Vincent PENANDO Publié le 29/03/2016 à 01:10:48 Noter cet article:
(0 votes)
Avis favorable du comité de lecture

Introduction

Voilà près d’un an que je m’intéresse au dialogue entre C et Lua sous Windows. J’ai utilisé une API bas niveau afin d’inclure à certains de mes projets écrits en C un module de scripting en Lua. J’ai voulu, dans le cadre du Supinfo Low-Level Laboratory, mettre ce module à jour, et l’adapter à du C++, donc à de l’orienté objet. J’ai commencé à travailler sur ce projet lorsque je terminais ma première année, et l’ai achevé quelques semaines avant de rentrer en seconde année. Voici l’état actuel de mes travaux, permettez-moi donc de vous les présenter dans cet article.

Syntaxe de Lua et prise en main du langage

Avant toute chose, il serait intéressant de faire une rapide présentation du langage Lua. Il ne s’agit pas d’un cours complet sur Lua, car le présent document n’a pas pour finalité d’apprendre Lua. Ainsi, des sujets tels que les tables ou les métatables ne seront pas traités ici. Lua est un langage de script. Comme la plupart des langages de scripts tels que Ruby ou Python, Lua est dynamiquement typé. Ainsi, une variable peut successivement avoir des valeurs de différents types. Pour déclarer une variable, il suffit d’écrire son nom, suivi de l’opérateur d’affectation (« = », comme dans la plupart des langages). L’exemple ci-dessous illustre ceci :

Code Lua :

var = "Hello word!"
var = true
var = 42

Parmi les types que peuvent prendre les variables, on retrouve des types « classiques » tels que les entiers, les flottants, les booléens et les chaînes de caractères. Les fonctions existent également en Lua. Elles se déclarent ainsi :

Code Lua :

function hello_world()
print("Hello word!")
end

Note – En Lua, les fonctions peuvent renvoyer plusieurs valeurs. Il suffit pour cela de les séparer par une virgule.

Elles commencent par le mot-clé "function" et se terminent par "end". Pour revenir aux types que peuvent prendre des variables, sachez qu’une variable peut également prendre la valeur d’une fonction. Il suffit pour cela d’utiliser, une fois encore, l’opérateur d’affectation. Ainsi, cet exemple crée une copie de la fonction "hello_world" ci-dessus :

Code Lua :

hello_world2 = hello_world
hello_world2() -- Affiche "Hello word!"

NoteSi nous avions écrit "hello_world2 = hello_world()", alors "hello_world2"aurait été égal à la valeur de retour de "hello_world". Ici "hello_world" ne renvoie rien, alors "hello_world2" vaudrait "nil". Cette valeur représente la valeur nulle, le vide.

En Lua, les commentaires débutent par deux tirets. Il est également possible d’écrire des commentaires sur plusieurs lignes. Ceux-ci débutent par -–[[ et se terminent par ]]. Exemple :

Code Lua :

--[[ Ceci
est
un
commentaire ]]

Lua permet également l’utilisation de structures itératives et conditionnelles. Nous traiterons ici de la condition if, et des boucles while, for et repeat. La structure if / else est très simple d’utilisation :

Code Lua :

if <condition> then
-- Code à exécuter
else
-- Code à exécuter
end

NoteMalgré la présente du "else", il n’y a qu’un seul "end", marquant la fin de la structure conditionnelle.

Ainsi, le code est très clair et compréhensible. En reprenant les cours d’algorithmique de première année, nous l’écririons de cette manière :

Code Algorithmique :

SI <condition> ALORS
<...>
SINON
<...>
FINSI

Voici un exemple concret :

Code Lua :

var = 1
if var ~= 0 then
print("var est différent de zéro !")
else
print("var n’est pas différent de zéro !")
end

NoteL’opérateur « ~= » signifie « différent de ». Il s’écrit « != » dans la plupart des autres langages.

Les structures itératives sont également assez instinctives. Elles s’écrivent sous la même forme que les conditions, excepté que « then » est remplacé par « do ». Ainsi, une structure itérative peut se comprendre comme :

Code Algorithmique :

TANTQUE <condition> FAIRE
<...>
FINTANTQUE

Nous ferons en sorte que chaque boucle écrite ci-après fasse la même chose. Concrètement, cela donne, en traitant les différentes boucles nommées ci-dessus :

Code Lua :

i = 0
while i < 10 do
print(i)
i = i + 1 -- L’opérateur ++ ou += n’existe pas en Lua
end

Cette boucle est assez classique et aisée à comprendre. Voici à présent la boucle "for", un peu plus délicate à manier mais tout aussi connue :

Code Lua :

for i = 0, 9, 1 do
print(i)
end

NoteLes bornes étant comprises dans l’intervalle, nous écrivons « for i = 0, 9, 1 do » et non « for i = 0, 10, 1 do ».

En C++, nous l’écririons ainsi :

Code C++ :

for (int i = 0 ; i < 10 ; i++) {
cout << i ;
}

NoteNous aurions très bien pu nous passer des accolades, mais j’estime qu’elles expriment bien le parallèle avec le “end” de Lua en marquant la fin de la structure.

La dernière boucle que nous allons aborder est la boucle repeat / until, soit « répéter jusqu’à-ce que ».

Exemple :

Code Lua :

i = 0
repeat
print(i)
i = i + 1
until (i == 10)

Ainsi se termine cette partie sur la syntaxe générale de Lua. Nous entrons à présent dans le vif du sujet, à savoir la communication entre C et Lua.

Introduction au dialogue entre C et Lua

Avant toute chose, il vous faut installer Lua 5.1 afin que votre compilateur puisse exécuter les fonctions relatives à ce que nous allons traiter. Pour cela, le dossier « Lua5.1 » contient les sous dossiers « include » et « lib », contenant respectivement les headers nécessaires et les .lib à linker ainsi que les .dll à placer dans le dossier de votre projet.

Nous nous intéresserons ici aux points suivants :

  • Appel de fonction Lua depuis C

  • Passage d’arguments à une fonction Lua depuis C

  • Récupération des valeurs de retour d’une fonction Lua

  • Appel de fonction C depuis Lua

  • Passage d’arguments à une fonction C depuis Lua

Ceci étant dit, passons à présent à la pratique. Il vous faut commencer par inclure les headers nécessaires :

Code C/C++ :

#ifdef __cplusplus
extern "C" {
#endif
#include <Lua\lua.h>
#include <Lua\lauxlib.h>
#include <Lua\lualib.h>
#ifdef __cplusplus
}
#endif

Nous ferons ici un programme console, car le but est simplement de vérifier que nos tests fonctionnent.

Nous allons commencer par appeler une fonction Lua.

Il faut en premier lieu déclarer un pointeur de type "lua_State". Il est à considérer comme un tunnel au travers duquel voyagent les données entre C et Lua, c’est donc un élément crucial pour ce que nous souhaitons faire.

Voici le code entier, sur lequel il est possible de se baser :

Code C/C++ :

#include <stdio.h>
#include <stdlib.h>
#ifdef __cplusplus
extern "C" {
#endif
#include <Lua\lua.h>
#include <Lua\lauxlib.h>
#include <Lua\lualib.h>

#ifdef __cplusplus
}
#endif
int main()
{
lua_State *lua;
return 0;
}

Du côté de Lua, nous allons procéder ainsi :

Code Lua :

-- Fichier ‘script.lua’
function hello_world()
print("Hello world !")
end

Puis nous complétons notre code C/C++ :

Code C/C++ :

// On ouvre Lua
lua = lua_open();
// On charge les bibliothèques standards de Lua
luaL_openlibs(lua);
// On charge le script
luaL_dofile(lua, "script.lua");
// Nom de la fonction à appeler
lua_getglobal(lua, "hello_world");
// Nombre de d'arguments pris et de valeurs renvoyées
lua_call(lua, 0, 0);
// Fermeture de Lua
lua_close(lua);

Si nous lançons l’application, la console nous affichera bel et bien « Hello word ! ».

Le passage d’arguments à Lua se fait de manière relativement simple. En effet, la fonction lua_call, prend trois arguments ; le premier est le pointeur lua_State, le second le nombre de variables que nous souhaitons passer en arguments, et le dernier correspond au nombre de valeurs de retour. Par exemple, si nous souhaitons passer une chaîne de caractères à une fonction dénommée afficher_chaine qui prend un argument et l’affiche en console, nous procéderons ainsi :

Code C/C++ :

lua_pushstring(lua, "Message a envoyer") ;

Et nous modifierons lua_call en conséquence. Voici le code complet de main() :

Code C/C++ :

lua_State *lua = lua_open();
luaL_openlibs(lua);
luaL_dofile(lua, "script.lua");
lua_getglobal(lua, "afficher_chaine") ;
lua_pushstring(lua, "Message a envoyer") ;
lua_call(lua, 1, 0);
lua_close(lua);
return 0;

La console nous affichera bien la chaîne souhaitée. Notons que nous aurions employé lua_pushnumber plutôt que lua_pushstring si nous souhaitions envoyer un nombre à Lua.

À présent, voyons comment récupérer la ou les valeurs de retour d’une fonction Lua. Supposons une fonction renvoyer_somme qui prend deux entiers en paramètre et renvoie la somme de ceux-ci. Nous lui enverrons par exemple les entiers 2 et 4.

Nous modifions en premier lieu lua_call pour envoyer deux nombres via lua_pushnumber et récupérer une valeur de retour.

Récupérer cette valeur est alors un jeu d’enfant ; en effet, il suffit de procéder, après l’appel à lua_call, de la manière suivante :

Code C/C++ :

int sum = (int) lua_tonumber(lua, 1) ;

Cette ligne mérite quelques explications. Nous récupérons la première valeur renvoyée par la fonction Lua et nous effectuons un cast sur celle-ci afin de stocker sa valeur dans une variable de type int.

NoteContrairement au C, l’indice du premier élément est de 1 et non de 0. Ainsi, passer 1 à la fonction lua_tonumber signifie bel et bien récupérer la première (et unique) valeur de retour.

Voici donc le code complet de main() :

Code C/C++ :

lua_State *lua = lua_open();
luaL_openlibs(lua);
luaL_dofile(lua, "script.lua");
lua_getglobal(lua, "renvoyer_somme") ;
lua_pushnumber(lua, 2);
lua_pushnumber(lua, 4);
lua_call(lua, 2, 1);
int sum = (int) lua_tonumber(lua, 1) ;
lua_close(lua);
return 0;

Après avoir vu comment appeler une fonction Lua, lui passer des arguments et récupérer ses valeurs de retour, voyons comment appeler une fonction C depuis Lua et lui passer des arguments.

Imaginons que nous avons une fonction C du nom de displaySum. Celle-ci affiche la somme de deux entiers passés en arguments. Côté Lua, nous procédons ainsi :

Code Lua :

function display_sum()
a = 1
b = 3
displaySum(a, b)
end

Nous aurons donc une fonction Lua qui appelle une fonction C. Cette fameuse fonction C aura le prototype suivant :

Code C/C++ :

int displaySum(lua_State *L) ;

Force est de constater que notre fonction ne prend pas deux entiers en paramètres, mais un pointeur lua_State. En effet, celle-ci, étant appelée depuis Lua, devra récupérer les deux valeurs passées en arguments de la même manière que nous avons récupéré les valeurs de retour d’une fonction Lua : via lua_tonumber. La fonction étant appelée depuis Lua, il faut qu’elle soit de type int, et qu’elle renvoie, conventionnellement, 0.

De plus, il nous faut enregistrer la fonction afin que Lua puisse l’appeler.

Voici le code complet :

Code C/C++ :

int main()
{
lua_State *lua = lua_open();
luaL_openlibs(lua);
luaL_dofile(lua, "script.lua");
lua_getglobal(lua, "display_sum");
// Enregistrement dans la fonction dans Lua
lua_register(lua, "displaySum", displaySum);
lua_call(lua, 0, 0);
lua_close(lua);
return 0;
}
int displaySum(lua_State *L)
{
int sum = (int) lua_tonumber(L, 1) + (int) lua_tonumber(L, 2);
printf("La somme est egale a : %d", sum);
return 0 ;
}

NoteLa fonction lua_register prend trois arguments : le premier est le pointeur lua_State, que nous connaissons, mais les deux autres sont plus spécifiques. Le second argument est le nom de la fonction tel qu’il sera appelé depuis Lua, et le troisième est le nom de la fonction dans notre programme C (il ne s’agit pas d’une chaîne de caractères).

Notons que lorsque le nombre d’arguments n’est pas défini, il est possible de le récupérer de la manière suivante :

Code C/C++ :

int total = (int) lua_gettop(L) ;

La fonction lua_gettop renvoie le nombre d’arguments passés à la fonction C.

Après avoir étudié les bases du dialogue entre C et Lua afin de comprendre certains mécanismes bas niveau de cette API, voyons à présent la surcouche C++ développée dans le cadre du SL3.

Étude de la surcouche C++

Cette partie sera considérablement plus simple et légère que la partie précédente, car la surcouche C++ encapsule la plupart des fonctions C étudiées ci-dessus.

L’ensemble des entités de cette surcouche permet de faire les mêmes choses que l’API C, à cela près qu’il s’agit là d’un code extrêmement souple et qu’il est entièrement orienté objet. Elle n’est constituée que de trois classes, qui suffisent amplement à reproduire le travail effectué précédemment en C.

Ces entités respectent la hiérarchie suivante :

LuaBase contient un pointeur de type lua_State et l’initialise depuis son constructeur d’instance.

Ainsi, il est prêt à être utilisé par ses classes filles lors de la création d’instances de celles-ci. Son champ ‘path’ est le chemin vers un fichier à ouvrir, de la même manière que nous avons ouvert des scripts Lua afin d’exécuter certaines de leurs fonctions.

Afin d’appeler une fonction Lua, il nous faut simplement appeler la méthode callLuaFunction de LuaManager. Le prototype de cette fonction est le suivant :

Code C++ :

bool callLuaFunction(const std::string& name, int returnedValues = 0, char *format = NULL, ...);

Cette fonction est de type bool ; elle renverra false si le nom de la fonction est une chaîne vide ou si elle rencontre une erreur. De plus, la plupart de ses paramètres ont une valeur par défaut : ainsi, il est possible de l’appeler en ne spécifiant que le nom de la fonction, dans le cas où elle ne prend pas d’argument et ne renvoie pas de valeur de retour. A contrario, il est possible de lui indiquer le nombre de valeurs de retours, et de lui passer des valeurs en arguments via l’élément format, à la manière de printf. En effet, si on veut passer en arguments une chaîne de caractères et deux entiers à une fonction Lua, on lui indiquera "sii" afin que callLuaFunction identifie le type des variables passées après le format. ‘s’ correspond à des chaînes (string), ‘i’ à des entiers (int), et nous pouvons envoyer des nombres décimaux via ‘d’ (double). En voici un exemple d’utilisation :

Code C++ :

if (!callLuaFunction("show_args", 0, "iids", 1, 42, 3.14, "Hello"))
{
// Une erreur a eu lieu ; prendre les mesures adéquates
}

Cette méthode montre à merveille la souplesse de l’API C++ ; on peut se contenter d'indiquer uniquement le nom de la fonction à appeler, ou au contraire lui spécifier le nombre de valeurs de retour et les arguments à passer.

Si une fonction Lua renvoie des valeurs, alors les membres listOfNumbers et ListOfStrings de LuaManager les contiendront et seront automatiquement vidés au prochain appel de callLuaFunction. Il est bien entendu possible de les récupérer via leurs accesseurs respectifs.

La méthode openNewFile permet de charger en mémoire un second script afin d’exécuter ses fonctions ; il est ainsi possible d’avoir plusieurs fichiers Lua ouverts simultanément. Il faudra cependant veiller à ce que deux fichiers ouverts en même temps n’aient pas de fonction du même nom.

La classe LuaWriter permet quant à elle d’écrire un script Lua depuis un terminal, sans avoir à utiliser de logiciels externes. Ainsi, il est directement possible d’automatiser des tests sur les fonctions que l’on vient d’écrire en les appelant via LuaManager et sa méthode callLuaFunction.

Conclusion

Pour conclure ce bref article, j’aimerais avant tout préciser que j’ai beaucoup apprécié ce projet, que ce soit à ses débuts ou dans sa finalité. La surcouche C++, bien qu’assez légère en soi, permet de faire de manière souple et simple des tâches plus complexes à réaliser en C, étant plus haut niveau que cette dernière. Je pense réutiliser cette API pour les projets de seconde année d’étude qui viendront dans les mois à venir.

J’aimerais remercier tous les membres du SL3, grâce auxquels j’ai pu progresser dans mon étude extra-scolaire de C++, pour leurs conseils avisés, leur présence au long du projet et leur soutien.

Je me suis inspiré de l’ouvrage « Langage C – L’essentiel du code et des commandes » d’Yves Mettier, notamment pour la syntaxe de printf et quelques astuces, comme lorsque dans une structure itérative où on compare une variable à une constante (il est de meilleur goût de mettre la constante à gauche et la variable à droite du signe d’égalité afin d’être averti par le préprocesseur d’un éventuel oubli d’un ‘=’).

J’espère avoir, au long du présent document, suffisamment bien expliqué les points cruciaux de la communication entre Lua et C/C++.

A propos de SUPINFO | Contacts & adresses | Enseigner à SUPINFO | Presse | Conditions d'utilisation & Copyright | Respect de la vie privée | Investir
Logo de la société Cisco, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société IBM, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sun-Oracle, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Apple, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sybase, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Novell, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Intel, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Accenture, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société SAP, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Prometric, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Toeic, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo du IT Academy Program par Microsoft, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management

SUPINFO International University
Ecole d'Informatique - IT School
École Supérieure d'Informatique de Paris, leader en France
La Grande Ecole de l'informatique, du numérique et du management
Fondée en 1965, reconnue par l'État. Titre Bac+5 certifié au niveau I.
SUPINFO International University is globally operated by EDUCINVEST Belgium - Avenue Louise, 534 - 1050 Brussels