Utilisation de Bison / Flex pour la création du convertisseur de code d’un sous-ensemble du langage MATLAB vers le code C. Le convertisseur est
utilisé pour créer des applications et des bibliothèques natives à partir de code MATLAB.
Votre adresse email est-elle correcte? Vous êtes inscrit à nos newsletters mais votre adresse e-mail n'est pas confirmée ou n'a
pas été reconfirmée depuis longtemps. Veuillez cliquer ici pour recevoir un e-mail de confirmation afin que nous puissions
confirmer votre adresse e-mail et recommencer à vous envoyer des newsletters. Alternativement, vous pouvez mettre à jour
vos abonnements .
introduction
Cet article décrit un convertisseur de code MATLAB-to-C écrit à l'aide du générateur d'analyseur syntaxique GNU Bison et du générateur
d'analyseur lexical GNU Flex. L'article explique les fonctionnalités de base de ces utilitaires et montre comment générer le code de sortie à partir
des résultats de l'analyseur. Ce convertisseur génère un code compilé et lié à la bibliothèque de support d'exécution. Une introduction à
l'utilisation de la bibliothèque est également fournie.
Afin de créer un analyseur de langage, la syntaxe du langage source doit être définie par une technique de notation. Une des techniques de
notation utiles pour les langages de programmation est la forme Backus – Naur ( BNF ). La transcription de langue BNF est un ensemble de
règles de dérivation pour les symboles de langue . Le fichier de définition de syntaxe (TmcParParser.Y) contient cette transcription de langage.
Prenons un exemple de fragment de fichier: fichier:
%union
{
char *str;
double num;
LSP_IDENT ident;
%token PLUS
%token MUL
%left PLUS
%left MUL
%token END_OF_INPUT
%start module
%%
module
: file_hdr list_function END_OF_INPUT
{
$$=$2;
tmcpar_parsing_module = $$;
YYACCEPT;
}
;
expression : identifier
{
$$=(T_expr_gen*)($1);
}
| const
{
$$=(T_expr_gen*)($1);
}
| bin_expr
{
$$=(T_expr_gen*)($1);
}
;
identifier : IDENT
{
$$ = create_identifier($1,(int)tmcpar_lineno,(int)tmcpar_colno);
}
;
const : variant_val
{
$$ = create_constant((enum CONST_VAL_TYPES)0,$1,(int)tmcpar_lineno,(int)tmcpar_colno);
}
;
variant_val : NUMBER
{
$$ = make_number($1,0,const_val_real);
}
| NUMBER_IM
{
$$ = make_number(0,$1,const_val_complex);
}
| STRING
{
$$ = make_string($1);
}
;
%%
// C++ code here
...
Ce fichier se compose de 4 sections, séparées par %{, %}et des %%séparateurs. La 1ère section (séparée par% {,%}) contient du code C / C ++,
elle peut inclure les fichiers qui définissent les classes et les variables utilisées. La deuxième section contient les définitions de type. La 3ème
section contient les règles de syntaxe et les actions sémantiques à effectuer à chaque correspondance de règle. La machine d'état de l'analyseur
fonctionne de manière itérative. Une nouvelle règle de correspondance est appliquée à chaque état, sinon l'analyseur appelle en interne la
fonction d'analyse lexicaletmcpar_lexqui provient du flux d’entrée suivant le lexema. Chaque règle a des arguments d'entrée ($ 1, $ 2 etc.) et
la sortie ($$). Un argument d'entrée peut être le résultat d'une règle d'enfants ou d'un lexema renvoyé par l'analyseur lexical. Ces arguments sont
représentés de manière interne dans la machine à états par une union avec des champs définis par une %unioncommande pouvant être du type
de données renvoyé par le lexema ou par une autre règle. Le type des arguments qui sont des règles est défini par la %typecommande. Le type
des arguments qui sont des données de lexema est défini par la %tokencommande. L'analyseur lexical associé à la valeur de lexema renvoie
également la valeur du code de jeton.
%union
{
double num;
...
}
%token <num> NUMBER
signifie que lorsque le lexer retourne un nombre, sa valeur de jeton renvoyée est NUMBERet le type de données est double.
La définition
%union
{
class T_expr_gen *lsp_expression_val;
...
}
%type <lsp_expression_val> expression
signifie que lorsque la règle de l'analyseur renvoie un expression, son type de données est class T_expr_gen *.
La règle
expression : identifier
{
$$=(T_expr_gen*)($1);
}
| const
{
$$=(T_expr_gen*)($1);
}
| bin_expr
{
$$=(T_expr_gen*)($1);
}
;
La règle
signifie qu'il bin_expr s'agit d'une séquence de symboles expression , PLUSet expresionou expression, MULet expresion.
Une définition de règle contient également les actions sémantiques , c'est-à-dire le code C ++ à exécuter lors de la correspondance de règles. Par
exemple
signifie que lorsque la règle expression PLUS expressionest vérifiée, le nœud de type class T_expr_bin *sera créé par la
fonction create_binary_expression()et renvoyé par la règle sous forme de symbole bin_expr.
Ces nœuds d'arbre renvoyés à chaque correspondance de règle sont connectés à un arbre d'analyse. La procédure d'analyse est terminée lorsque
le symbole principal moduledéfini par la %start modulecommande est renvoyé. Dans l'exemple présenté, les noeuds sont connectés les
uns aux autres dans un arbre ou sous forme de liste.
Cet utilitaire produit l'analyseur lexical à partir du fichier de définition de lexer TmcParLexer.L. La sortie est le fichier C ++ est lex.tmcpar_.cpp. Ce
fichier contient l’implémentation de la fonction lexer tmcpar_lex()appelée en interne par l’analyseur produit par Bison. Le fichier de
définition de lexer utilise les définitions de jeton fournies par le fichier de définition de l'analyseur. Etant donné que le lexer doit renvoyer les
définitions de jeton, Bison doit être exécuté en premier.
%{
#include "tmcpar_tree.h"
#include "TmcParParser.tab.hpp"
%}
%Start ExpectQuoteAsCTRANSPOSE
IDENTIFIER [a-zA-Z][_a-zA-Z0-9]*
%%
{IDENTIFIER} {
get_identifier();
return ReturnLspToken( IDENT);
}
%%
char* get_identifier()
{
//yyleng is length
//yytext is src
strcpy(tmcpar_lval.ident[0],yytext);
return tmcpar_lval.ident[0];
}
Comme dans le fichier de définition de Bison, le fichier de définition Flex est composé de sections séparées par %{, %}et %%. La première section
contient des options et des définitions pour les expressions régulières . La deuxième section contient les actions, c'est-à-dire le code qui est
exécuté lorsqu'une expression régulière est mise en correspondance. Une action peut être assortie de manière conditionnelle, par exemple, dans
le code suivant:
Masquer le code de copie
<INITIAL>'[^'\r\f\n]*' {
register int i, size;
char* modified;
const int length = yyleng-2;
for (size = 0, i = 1;i <= length; size++, i++)
if (*(yytext+i) == '\'')
i++;
modified = alloc_string(size+1);
*(modified+size) = '\0';
for (size = 0, i = 1;i <= length; size++, i++)
{
*(modified+size) = *(yytext+i);
if (*(yytext+i) == '\'')
i++;
}
tmcpar_lval.str = modified;
yyinsert_comma_in_input(STRING);
return ReturnLspToken( STRING);
}
reconnaît la chaîne littérale et retourne le lexema STRING, il est exécuté uniquement dans l'état INITIAL. Le code suivant
Masquer le code de copie
<ExpectQuoteAsCTRANSPOSE>' {
tmcpar_lval.int_value = CTRANSPOSE;
return ReturnLspToken( CTRANSPOSE ); }
reconnaît l'action de transposition (') et renvoie le lexema CTRANSPOSE; il est exécuté uniquement dans l'état ExpectQuoteAsCTRANSPOSE. L'état
initial est défini par l'option
Masquer le code de copie
%Start ExpectQuoteAsCTRANSPOSE
BEGIN(INITIAL)
L'analyseur lexical essaie d'appliquer toutes les règles de la liste en fonction de son ordre. S'il réussit, le code de la règle est exécuté et peut
renvoyer le code de lexema à l'analyseur de syntaxe. La valeur lexema est renvoyée dans l'union tmcpar_lval.
Le constructeur de classe est appelé à partir de l'analyseur et stocke la valeur constante dans le membre T_const::val. Ensuite, lors de la
génération du code, la méthode virtuelle T_const::generate_rtl_nodecrée les instructions intermédiaires et les ajoute à la liste à
ilistpartir de laquelle le code C cible sera généré. Par exemple, le code C suivant sera généré pour la constante "1":
Masquer le code de copie
tmcScalar(reg[11],1.000000000000000e+000);
Ceci est un appel à la fonction de bibliothèque d'exécution tmcScalarqui initialise la variable temporaire "registre" avec le résultat d'exécution
du noeud, la constante "1.0". La classe d'instruction intermédiaire CInstrcontient les informations sur le type d'instruction, le numéro de
"registre" en sortie et la valeur constante:
Masquer le code de copie
class CInstr
{
public:
//! instruction operation types
enum instr_types
{
instr_create_matrix_empty,
instr_complex,
instr_scalar,
instr_create_cell_empty,
instr_create_string_empty,
instr_create_string,
instr_create_magic_colon,
...
};
instr_types m_inst_type;
CIReg outreg; // output register
double m_Real;
double m_Imag;
...
}
Lorsque la liste complète du code intermédiaire CInstrListpour a functionest terminée, le code cible est simplement imprimé par la
CInstrList::print_rtlméthode. Ceci est fait par la CInstr::gen_icodeméthode qui imprime le code C ou par la
CInstr::gen_asmcodeméthode, le code assembleur équivalent, par exemple:
Masquer le code de copie
...
case instr_scalar: // make a scalar matrix
sprintf(buff,"%s,%.15e",get_reg_name(outreg).c_str(),m_Real);
retval=std::string("tmcScalar(").append(buff).append(");");
break;
...
Un exemple plus complexe est le code de fordéclaration. Il est implémenté par classe T_ctrl_cmd_for:
for k=0:N-1
..
end
Ici , la variable kest analysée dans T_ctrl_cmd_for::lhs, l'expression 0:N-1dans expret le code du corps list.
void generate_rtl_list()
// generate list of instructions from the list pointed by tmcpar_parsing_module
{
CInstrList InstList; // intermediate instructions list
class T_func_hdr *tfd;
extern L_stmnt_gen *tmcpar_parsing_module;// global pointer to parser tree, initialized by tmc_parse()
int NumLocalFuncs=0;
if (tmcpar_parsing_module)
{ /// Go through list of functions
for (L_stmnt_gen::iterator p = tmcpar_parsing_module->begin();
p != tmcpar_parsing_module->end();p++)
{
tfd = (T_func_hdr *)(*p);
Compiler.indFunc = NumLocalFuncs;
Compiler.indFunc_RTL = NumLocalFuncs;
InstList.store_function_ind(NumLocalFuncs);
tfd->generate_rtl_node(&InstList);/// Generate intermediate instructions
InstList.print_rtl(NumLocalFuncs);/// Convert instructions to the target code
InstList.reset_new_local_function();
NumLocalFuncs++;
}
}
}
Cordage de hachage
Afin d'optimiser les calculs avec des chaînes littérales et des noms de champs, une table de hachage est utilisée. Les noms de champs de
structure sont accessibles par codes de hachage. L'affectation de champ de structure de code
S.x = 100;
est converti en
Masquer le code de copie
tmcScalar(reg[3],1.000000000000000e+002);
tmcGetRefByFieldHcode(pRefHelper,S,0x00780000);/* x */
tmcAssign(pRefHelper,reg[3]);
La chaîne de nom de champ "x" est accessible par code de hachage 0x00780000. La table de hachage est pré-calculée lors de la conversion et
est chargée lors de l'initialisation de la bibliothèque d'exécution. Chaque chaîne connue au moment de la conversion a son code de hachage
unique et est accessible par ce code. Cette table de hachage contient les chaînes d'origine et leur code de hachage, par exemple:
La base de données de fichiers source est implémentée par class CTmcFileList TmcFileList. Il gère la liste des fichiers source
réellement utilisés trouvés dans le chemin de recherche. La table des symboles est initialisée après la première passe du convertisseur lorsqu'il est
déjà possible de distinguer les variables des fonctions et d'obtenir les prototypes de fonctions.
Bibliothèque d'exécution
Le convertisseur est construit sur les mêmes principes qu’un compilateur, mais contrairement aux compilateurs qui génèrent un code d’assassin
ou de code C bas niveau (par exemple, MATISSE http://specs.fe.up.pt/tools/matisse/), il génère des appels. aux fonctions de la bibliothèque
d'exécution fournie. La raison en est l'absence d'informations de type sur les variables MATLAB. Chacune de ces variables et les variables
intermédiaires ( reg[]) sont implémentées par un seul type tmsMatrix.
La bibliothèque d'exécution implémente des opérations élémentaires sur les matrices et autres fonctions intégrées nécessaires à l'exécution du
code. Les opérations d'algèbre linéaire telles que la division matricielle sont implémentées à l'aide du package LAPACK. La bibliothèque
d'exécution doit être initialisée afin de créer des variables globales et de charger les chaînes hesh-table. Le code d'initialisation nécessaire est
généré par le convertisseur. Enfin, la bibliothèque devrait être non initialisée.
Pour effectuer le premier passage, le convertisseur TMCCO est appelé par exemple
Il commence à analyser la fonction principale dans le fichier TestO.m. Si un symbole non attribué est trouvé, il est supposé être une fonction
externe et son fichier source est recherché dans les répertoires passés par le commutateur -i. Ces fichiers source sont traités de manière récursive
jusqu'à ce que tous les fichiers dépendants soient analysés. Il en résulte une liste de m-fichiers réellement utilisés (TestO.filelist.txt), une table
contenant tous les prototypes de fonctions (TestO.sym.dat) et le fichier include contenant les fonctions de prototypes (stdtmc.h). Le fichier
TestO.sym.dat inclut également les prototypes de toutes les fonctions intégrées.
Pour effectuer le second passage, le convertisseur TMCCO est appelé comme dans l'exemple suivant:
Le traitement est effectué de la même manière que lors de la première passe, mais l'analyseur peut déjà distinguer les fonctions des variables et
générer le code de sortie. La machine à états de l'analyseur crée une présentation arborescente du code. Ensuite, la procédure de création de
code parcourt l’arborescence et imprime le code de sortie. Outre les fichiers C cibles, les fichiers d'initialisation d'exécution sont créés
(TestO.globals.c, TestO.globals.h, TestO._init_hash_data.c, TestO.hash_init.dat).
Dans cet exemple, trois types de code de sortie sont présentés: le code C (le plus utile), le code LISP (pour l'analyse du code) et le code ASM (à
des fins de démonstration, peuvent être traités par NASM Assembler et convertis directement en exécutable Win32. !)
La dernière version du convertisseur, des exemples, de la documentation et des outils supplémentaires sont disponibles sur le site de
téléchargement du compilateur TMC (https://sourceforge.net/projects/tmc-m-files-to-c-compiler).
Points d'interêts
Si la tâche consistant à écrire un analyseur de syntaxe est très délicate, la mise en œuvre d'une bibliothèque d'exécution exécutant les opérations
de base l'est encore plus. Le principal défi consistait à éviter toute fuite de mémoire, à optimiser le temps d'exécution et à prendre en charge
différentes plates-formes et compilateurs. L'utilisation de la mémoire a été déboguée à l'aide de mécanismes de débogage MSVC tels que la
_malloc_dbgfonction, les _CrtSetDbgFlagmacros et le comptage précis de la mémoire allouée.
Lorsque le code C était généré, il était difficile de le déboguer car les variables MATLAB étaient stockées dans des objets de structure complexe
alloués dynamiquement. Un simple débogueur a été développé pour afficher toutes les variables créées dans un contexte donné. Pour cela, un
code "de débogage" supplémentaire est généré (en utilisant le commutateur -d dans l'appel de l'analyseur) et la mémoire de processus est lue
par ReadProcessMemoryfonction. Dans l’environnement GNU GCC, la méthode la plus simple était d’utiliser le débogueur GDB et d’appeler
directement la fonction d’exécution tmcdispqui affiche le contenu d’une variable donnée.
Il était intéressant de produire un exécutable directement à partir du m-code. Cela s'est avéré très simple, grâce à l'article "Tiny PE. Création du
plus petit exécutable PE possible". Générer des appels à la bibliothèque d'exécution en C et en assembleur était approximativement de la même
complexité. Ainsi, le code assembleur a été généré par le convertisseur, un en-tête exécutable PE y a été attaché et le fichier de résultat assemblé
par l’assembleur NASM. Cet exercice a été fait uniquement pour le format x86; pour x64, le générateur de code assembleur doit être adapté à la
nouvelle conversion d’appel et les personnes intéressées peuvent l’essayer par lui-même :)
Le livre suivant peut être recommandé pour une compréhension plus approfondie de GNU Bison / Flex:
Remerciements
Je tiens à remercier particulièrement Vladimir Borisenko, de l’Université d’État de Moscou, pour son excellent cours sur les compilateurs.
Merci à Michael Medved pour son travail acharné sur la révision de cet article.
L'histoire
Première version.
Licence
Cet article, ainsi que tout code source et fichiers associés, est sous licence GNU
General Public License (GPLv3)
Partager
GAZOUILLEMENT FACEBOOK
A propos de l'auteur
Shmuel Safonov
Ingénieur Suivre
Israël ce membre
Ingénieur logiciel et expert en systèmes de contrôle chez Digital Feedback Technologies Ltd.
PhD, Université de Tel-Aviv, 2009
MS, Faculté de mécanique et de mathématiques de Lomonossov Université d’État de Moscou, 1994
Page du blog: http://csafonov.blogspot.co.il/
Personnel Projet open-source: compilateur TMC, convertisseur de code MATLAB en C (https://sourceforge.net/projects/tmc-m-files-to-c-
compiler)
Vous pouvez également être intéressé par ...
Flex / Bison sur MSBuild Principes de base de la
programmation iOS 12 avec Swift:
Intro & Chpt1
CQRS "sans serveur" utilisant la grille Écrire votre propre convertisseur RTF
d'événements et les fonctions
durables d'Azure
Commentaires et discussions
Ajouter un commentaire ou une question Search Comments