Découper le code en plusieurs fichiers

Les simplifications c'est compliqués...

Je veux simplifier un gros code en le découpant en plusieurs fichiers (un fichier pour la gestion du wifi, un pour les capteurs, un pour le serveur web, etc.)

J'ai suivi plusieurs tutos mais je pense que je passe à côté de la logique du truc entre les includes() , les fichiers .h et .cpp

Je ne poste pas tout le code ici (gros moche et bancal de toute façon), j'ai repris un code minimaliste pour essayer de comprendre.

/*
  Code dérivé de Blink
*/

// variable globale : est-ce que l'on peut la mettre dans le fichier externe ?
int var_globale = 0;

void code_a_sortir() {
//  j'aimerai que cette partie soit dans un fichier à part
// dans le vrai code il y a plein de truc
  ++var_globale;
  Serial.println(var_globale);
}

// the setup function runs once when you press reset or power the board
void setup() {
  Serial.begin(115200);

  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)
  delay(100);                      // wait for a second
  digitalWrite(LED_BUILTIN, LOW);   // turn the LED off by making the voltage LOW
  delay(1000);                      // wait for a second

  code_a_sortir();
}

Est-ce que qq'un a un tuto ou des explications claires à me proposer ?
Dans ce code exemple, que doit-on mettre en includes() , .h et .cpp ?

Merci !!

  • soit vous faites plein de .ino et l'IDE se charge de les regrouper avant de compiler et donc c'est comme si vous aviez un gros fichier mais vous ne le voyez pas

  • soit vous appliquez les règles de la compilation séparée du C++

voici à quoi ça ressemblerait

  • le .h commence par un préambule qui empêche de l'importer deux fois au sein du même fichier
  • ensuite on déclare (pas la même chose que définir) les éléments qui doivent être connus du monde extérieur, ici qu'il existe une variable entière du nom de var_globale et une fonction void code_a_sortir() . Pour les variables on utilise le mot clé extern, pour les fonctions c'est une "forward déclaration", on met juste la fonction mais sans les accolades, et suivie d'un point virgule
  • le .cpp est l'implémentation, c'est là qu'on définit les choses (allocation de la mémoire pour la variable, fourniture du code pour la fonction)

Merci, je vais regarder et je pense que c'est ce genre de choses :

#include <Arduino.h> // nécessaire parce que notre .cpp utilise des trucs arduino, ici Serial

pour lesquelles j'étais passé à côté.

Et ceci :

#ifndef __AUTRE_HH__
#define __AUTRE_HH__

ça sert à quoi ?

➜

si votre .ino faisait bêtement

#include "autreFichier.h"
#include "autreFichier.h"

void setup() {}
void loop() {}

Un #include est juste un remplacement textuel dans le fichier source du fichier mentionné donc au premier #include vous avez

#ifndef __AUTRE_HH__
#define __AUTRE_HH__

#include <Arduino.h> // nécessaire parce que notre .cpp utilise des trucs arduino, ici Serial

extern int var_globale; // on dit que cette variable va être définie ailleurs et on donne son type et son nom
void code_a_sortir();   // on dit que cette fonction va être définie ailleurs, on donne sa signature détaillée

#endif

#include "autreFichier.h"

void setup() {}
void loop() {}

le #ifndef dit "si le mot clé __AUTRE_HH__ n'est pas déjà connu alors faire tout ce qui suit

donc on fait #define __AUTRE_HH__ qui aura pour conséquence de définir __AUTRE_HH__

quand on arrive alors au second #include "autreFichier.h" le pré-processeur va ré-injecter le code du .h et on aura

#ifndef __AUTRE_HH__
#define __AUTRE_HH__

#include <Arduino.h> // nécessaire parce que notre .cpp utilise des trucs arduino, ici Serial

extern int var_globale; // on dit que cette variable va être définie ailleurs et on donne son type et son nom
void code_a_sortir();   // on dit que cette fonction va être définie ailleurs, on donne sa signature détaillée

#endif
#ifndef __AUTRE_HH__
#define __AUTRE_HH__

#include <Arduino.h> // nécessaire parce que notre .cpp utilise des trucs arduino, ici Serial

extern int var_globale; // on dit que cette variable va être définie ailleurs et on donne son type et son nom
void code_a_sortir();   // on dit que cette fonction va être définie ailleurs, on donne sa signature détaillée

#endif
#include "autreFichier.h"

void setup() {}
void loop() {}

et lors de l'évaluation à a compilation, comme __AUTRE_HH__ aura été défini par le premier include, le compilateur ne rentre pas dans le #ifndef et donc on ne répète pas les définitions.

bien sûr dans l'absolu on ne mettrait pas deux fois comme cela le include mais ça arrive fréquemment que des bibliothèques que vous incluez inclue par elle même chacune un même fichier (par exemple Arduino.h) ➜ l'avantage donc là c'est que vous ne récupérez à la fin à compiler qu'une seule fois chaque .h

(GCC traite cela aussi avec des pragma donc vous pourrez aussi voir dans certains codes #pragma once mais ce pragma opère sur le chemin absolu du fichier et comme l'IDE déplace les fichiers pour les compiler, ça peut ne pas fonctionner dans l'environnement arduino)

Merci pour toutes ces explications...

Est-ce que c'est vraiment trivial et je suis juste complètement neuneu (possible) ou est-ce que cela vaudrait le coût d'en rédiger un petit tuto dans la section idoine ?

ce n'est pas trivial, il faut lire la doc sur la compilation séparée, c'est un peu avancé comme sujet - mais une fois qu'on a compris c'est simple :slight_smile:

En fait, c'est compliqué par qu'il y a plusieurs façons de faire, mais c'est simple parce que c'est logique.
Dans ton exemple (mettre la fonction code_a_sortir() dans un autre fichier, tu n'est pas obligé d'utiliser un .h
Il suffit dans le fichier principal de décaler, en tête de fichier,
void code_a_sortir();
ce qui est bien une déclaration sans définition. Quand le compile va rencontrer l'appel à la fonction (dans ta loop()), il va comprendre et se dire "ok c'est un fonction, il n'y a pas de paramètres, c'est bon", il place un appel. C'est le linker, un programme qui intervient après les compilations, qui va faire le ciment entre l'appel et la code de la fonction, compilée dans un autre fichier.

Pour les variables globales, l'important est de ne les définir qu'une seule fois. Su tu déclares
int ma_variable_globale;
dans 2 fichiers (ou plus), il va exister 2 variables avec ce nom. Le compilateur ne dira rien (il connait cette variable, dans chacun des fichiers à compiler) mais le linker va gueuler "symbole défini plus d'un fois", révélant le conflit.
Donc ce qu'on fait, c'est que dans un seul fichier, on définit la variable (et on peut lui donner une valeur), et dans les autres fichier on la déclare par
extern int ma_variable_globale;
Le extern disant bien ce qui se passe : la variable existe par ailleurs, ne pas en créer de nouvelle version !

Pour les fichiers .h, il faut se rappeler qu'il sont simplement inclus dans les fichiers .c ou .ino
Donc il faut se figurer ce qui va finalement être présent dans les fichiers où ils sont inclus, en respectant les principes ci-dessus.

Pour regrouper en une seule réponse tout ce qui a été donné dans les posts précédents

plutôt qu’avoir un fichier fleuve où il est difficile de retrouver ses petits il existe la possibilité de découper son code en plusieurs fichiers. Le fichier .cpp contiendra la définition des fonctions , le fichier .h la déclaration. Le fichier principal contiendra essentiellement les appels à ces fonctions. Il devient donc beaucoup plus facile de retrouver ses petits.
Pour cela il faut que le fichier .cpp sache où trouver la fonction dont il a besoin ce qui est fait avec le #include en début de fichier.
C'est déjà ce que vous faites dans votre exemple en incluant ce fichier #include arduino.h il rend ses fonctions disponibles. Ainsi ‘’digital write ‘’ appelle la fonction void digitalWrite(uint8_t pin,uint8_t val dont la déclaration est visible dans le fichier arduino.h.
On voit qu’il est plus facile de comprendre ce que font ces 4 lignes de programmes, du moins, bien mieux que les 60 lignes de codes qu’elles cachent.

Ce qui suit le symbole dièse telle # include, ifndef, endif et autres sont des directives de préprocesseur, il suffit, dans un premier temps de les appliquer comme demandé. Le terme est en général assez évocateur pour comprendre son utilité: include pour inclure, ifndef-define-endif pour si pas défini-définir-fin de si. Le #include arduino.h peut figurer dans 10 fichiers .cpp et il est plus qu'inutile de le compiler 10 fois.

Oui

et si on veut s'amuser à compliquer le tout...

Si on met le petit mot static devant la variable globale ou la fonction, alors elle devient locale à la compilation unit, ce qui peut permettre de ne pas avoir de souci d'espace de noms sans utiliser un espace de nom

par exemple le .ino

static int valeur = 10;
extern void uneFonction();

static void printValeur() { 
 Serial.print("fichier principal .ino ➜ ");  Serial.println(valeur);
}

void setup() {
  Serial.begin(115200);
  printValeur();
  uneFonction();
}

void loop() {}

et un fichier test.cpp

#include <Arduino.h> // pour Serial

static int valeur = 20; // elle est static donc locale à cette unité de compilation

static void printValeur() { // elle est static donc locale à cette unité de compilation
  Serial.print("test.cpp ➜ ");  
  Serial.println(valeur);
}

void uneFonction() {
  printValeur(); // appelle la fonction locale
}

➜ usageStatic - Wokwi ESP32, STM32, Arduino Simulator

Ce n'est pas cela : au moment où il compile un fichier (.cpp ou .ino), le compilateur n'a pas besoin de savoir où il va "trouver" cette fonction. Il a juste besoin de savoir;

  • que le nom qu'il rencontre est celui d'une fonction,
  • que l'appel à cette fonction respecte la prototype de la fonction (nom et type des arguments).

C'est pour cela qu'on inclus la déclaration de la fonction dans le fichier qui l'utilise.
Trouver la fonction, c-à-d le code compilé de la fonction, c'est l'affaire du linker, qui entre en scène après les compilations de tous les fichiers. Il établit le lien entre l'appel à la fonction, resté irrésolu à l'étape de compilation, el le code de la fonction qu'il trouve dans le code objet, généré par le compilateur à la compilation du fichier qui contient la définition de la fonction.

Mon but n’étant pas de compliquer les choses, je n’entre pas dans le fonctionnement des préprocesseur, compilateur,linker, pas plus que sur les mots clés particuliers ou sur les fonctions spéciales qui ne présentent aucun intérêt ici.

Je continue donc à dire, même si cela paraît simpliste, que #include arduino.h sert au fichier.cpp à trouver les fonctions dont il a besoin. Dans l’exemple toutes les fonctions sont retrouvées dans arduino.h (pinMode, digitalWrite,delay, digitalRead) puisqu’il n’y a que ce fichier qui soit inclut.

J’essaie juste de faire voir qu’en incluant arduino.h dans son bout de code Professeur Mephisto utilisait cette simplification de gros code découpé en plusieurs fichiers.

Donc en utilisant la même méthode que arduino avec son fichier arduino.h et arduino.cpp ça marche.

La base est la suivante, je crée un fichier.h et je lui donne un nom le plus descriptif qui soit par exemple gestionled, je crée un fichier.cpp et je lui donne le même nom avec l’extension.cpp.

J’ouvre le fichier gestionled.h et si l’IDE ne le remplit pas j’entre le code minimum qui consiste en ceci :

#ifndef GESTIONLED_H

#define GESTIONLED_H

{

} ;

#endif //GESTIONLED_H

J’ouvre le fichier gestionled.c et si l’IDE ne le remplit pas j’entre le code minimum qui consiste en ceci :

#include <gestionled.h>

{

}

A partir de là il faut juste savoir qu’il faut déclarer ses fonctions dans le .h et les définir dans le .cpp (ex: faire une fonction pour allumer les leds oranges)

Ainsi si on crée un fichier.cpp et .h monsapin il faudra dans fichier monsapin.h faire #include <gestionled.h> à partir de là on peut appeler la fonction qui allume les leds oranges sans être obligé de taper x lignes de code.

L’intérêt paraît minime sur un exemple aussi simple mais en imaginant un soft domotique qui va gérer le sapin de noël mais aussi le chauffage, l’allumage du four en cas d’absence, les lumières etc il est évident que l’on ne va pas aller chercher dans ce fichier gestionled pour faire des modifications concernant le four.

Pas vraiment…

Il faut des guillemets plutôt que les chevrons et pas besoin d’accolades

pinMode, digitalR/W sont dans le fichier wiring_digital.c
Arduino.h rassemble les déclarations des différents fichiers c et cpp plus un certain nombre de constantes comme HIGH = 1

Note : Pour ce qui suit, mes mots ne sont pas ceux des pros, ce sont ceux approximatifs d'un amateur.

En non-programmeur professionnel, je dirais simplement :

  1. Je rappelle que le fichier qui est transmis au compilateur n'est pas le fichier ino mais un vrai fichier cpp, main.cpp, que l'IDE a construit en ajoutant tout ce qui manque au fichier ino, en particulier les pré-déclarations des fonctions.

C'est bien là la source du problème de compréhension pour les débutants, s'ils avaient appris à prédéclarer les fonctions dans le fichier ino, ils leur serait plus aisé de comprendre la programation en fichiers séparés.
Le squelette de ce fichier main.cpp se trouve dans les répertoires de l'IDE.
arduino-1.8.15/hardware/arduino/avr/cores/arduino/main.cpp

  1. Le compilateur doit connaître les noms de fonctions ET les définitions des variables avant qu'il soit mis devant le fait accompli de leur utilisation.
    Les variables globales définies dans un fichier cpp doivent être déclarées avec le mot clé EXTERN dans tous les autres fichiers qui les utilisent.

Exemple qui fonctionne :

int fonction machin()
{
*******
}
void main()
{
     int truc = machin() ;
}

Le compil a trouvé la fonction machin avant de trouver un appel à machin () dans main()

Exemple qui ne fonctionne pas :

void main()
{
     int truc = machin() ;
}
int fonction machin()
{
******
}

Pourquoi parce que dans main() il y a un appel à la fonction machin et le compil n'en a jamais entendu parler, alors il gueule.

Autre forme qui marche : prédéclaration

int fonction machin() ; // pré-déclaration notez le point virgule

void main()
{
     int truc = machin() ;
}
int fonction machin()
{
******
}

Ça marche parce que le compil dès le début sait qu'il verra apparaitre des appels à la fonction machin()

Programmation séparée : C'est la même chose : le compil veut tout connaitre à l'avance.
Quand le compilateur rencontre un appel à une fonction contenue dans un autre fichier il gueule s'il n'a pas été prévenu.

Fichier h
Ils ne sont pas obligatoires.
Les pré-déclarations peuvent être faites à la main dans TOUS les fichiers qui utiliseront les fonctions réparties dans les fichiers cpp.

Comme c'est fastidieux et une source d'erreurs, surtout si le projet est décomposé en plusieurs fichiers, on écrit les pré-déclarations dans un fichier que l'on inclu dans les autres L'extension est h pour "header" ou "En-tête".
Ce n'est qu'une convention, "#include" inclura n'importe quel fichier texte.
On trouve des exemples avec hpp pour faire comme cpp, cela n'apporte rien de plus.

J'espère ne pas avoir écrit trop de conneries et que j'ai, un peu, démystifié la programmation en fichiers séparés

J'ai plus l'habitude du c++ c'est la raison pour laquelle j'ai écrit l'include avec des chevrons et j'ai mis les accolades du fait qu'un .c vide n'a pas trop d'utilité.
J-M-L j'ai une question à te poser concernant un de tes tuto mais j'ouvre demain un nouveau sujet pour cela.

Include avec chevrons, c'est quand le fichier est "garé" dans le répertoire dédié aux bibliothèques.
include avec guillemet, c'est quand le fichier est "garé" dans le répertoire de travail.

Je suppose que c'est la même chose dans tous les langages de programmation dès qu'une EDI gère les bibliothèques.

L'usage du # include est différent en Cpp

Je n'avais pas cette impression, j'avais plutôt l'impression que c'était un sujet qui dépendait de l'IDE puisque cela joue sur l'emplacement où trouver les fichiers avant de dépendre du langage.

Mais comme je l'ai dit, je ne suis qu'un amateur en programmation, je n'en connais pas assez pour contester quoi que soit.

Non ce n'est pas amateur que de ne pas le savoir, on utilise souvent du C avec le C++ ce qui fait que les habitudes du c sont conservés en c++ généralement
En C++

#include<string> //autorisé car librairie standard
#include<string.h> //autorisé car syntaxe c
#include<monFichier.h> // autorisé C++
#include<monFichier> // non autorisé en C++
#include "monFichier.h" //autorisé car syntaxe c

Mais ça peut être différent pour des EDI spécifiques comme arduino ide

Je ne me mets pas dans une situation de "librairies" avec une EDI, ce n'est pas le sujet du fil de discussion.
Je me mets dans une situation de programmation en fichiers séparés qui peuvent être placés n'importe où, même et surtout à des emplacements inconnus de l'EDI.

Dans ce cas, pour ma culture personnelle, comment fait-on en C : guillemet, chevron, autres signes ?

Strictement en C seul les guillemets sont acceptés