Comment rendre incompatibles deux types de pointeurs identiques par ailleurs

Sur certaines architectures, il peut être nécessaire d’avoir différents types de pointeur pour des objects identiques par ailleurs. En particulier pour un processeur d’architecture Harvard, vous pouvez avoir besoin de quelque chose comme:

uint8_t const ram* data1; uint8_t const rom* data2; 

En particulier, voici à quoi ressemblait la définition des pointeurs vers ROM / RAM dans MPLAB C18 (maintenant abandonnée) pour les PIC. Il pourrait définir même des choses comme:

 char const rom* ram* ram strdptr; 

Ce qui signifie un pointeur dans la RAM vers des pointeurs dans la RAM pointant vers des chaînes dans la ROM (utiliser ram n’est pas nécessaire, car par défaut, ce compilateur contient des éléments dans la RAM, il suffit de tous les append pour plus de clarté).

La bonne chose dans cette syntaxe est que le compilateur est capable de vous alerter lorsque vous essayez d’affecter de manière incompatible, comme l’adresse d’un emplacement de la ROM, à un pointeur vers la RAM (quelque chose comme data1 = data2; ou en passant une ROM). pointeur vers une fonction utilisant un pointeur RAM générerait une erreur).

Contrairement à cela, dans avr-gcc pour l’AVR-8, il n’existe pas de sécurité de ce type car elle fournit plutôt des fonctions permettant d’accéder aux données de la ROM. Il n’y a aucun moyen de distinguer un pointeur sur la RAM d’un pointeur sur une ROM.

Il existe des situations où ce type de sécurité serait très utile pour détecter les erreurs de programmation.

Existe-t-il un moyen d’append des modificateurs similaires aux pointeurs (par exemple, par préprocesseur, extension à quelque chose qui pourrait imiter ce comportement) pour atteindre cet objective? Ou même quelque chose qui met en garde sur un access inapproprié? (dans le cas de avr-gcc, essayer d’extraire des valeurs sans utiliser les fonctions d’access à la ROM)

Une astuce consiste à envelopper les pointeurs dans une structure. Les pointeurs à structurer ont une meilleure sécurité de type que les pointeurs sur les types de données primitifs.

 typedef struct { uint8_t ptr; } a_t; typedef struct { uint8_t ptr; } b_t; const volatile a_t* a = (const volatile a_t*)0x1234; const volatile b_t* b = (const volatile b_t*)0x5678; a = b; // comstackr error b = a; // comstackr error 

Vous pouvez encapsuler le pointeur dans une structure différente pour la RAM et la ROM, rendant le type incompatible, mais contenant le même type de valeurs.

 struct romPtr { void *addr; }; struct ramPtr { void *addr; }; int main(int argc, char **argv) { struct romPtr data1 = {NULL}; struct romPtr data3 = data1; struct ramPtr data2 = data1; // <-- gcc would throw a compilation error here } 

Pendant la compilation:

 $ cc struct_test.c struct_test.c: In function 'main': struct_test.c:12:24: error: invalid initializer struct ramPtr data2 = data1; ^~~~~ 

Vous pourriez bien sûr typedef la structure par souci de concision

Ayant reçu plusieurs réponses qui offrent différents compromis sur la fourniture d’une solution, j’ai décidé de les fusionner en une seule, en soulignant les avantages et les inconvénients de chacune. Vous pouvez donc choisir le plus approprié à votre situation

Espaces d’adresses nommés

Pour le problème particulier de la résolution de ce problème, et uniquement dans ce cas de pointeurs de ROM et de RAM sur un micro AVR-8, la solution la plus appropriée est la suivante.

Il s’agissait d’une proposition pour C11 qui ne figurait pas dans la norme finale. Cependant, certains compilateurs C la prennent en charge, notamment avr-gcc utilisé pour les AVR 8 bits.

La documentation associée est accessible ici (partie du manuel en ligne de GCC, incluant également d’autres architectures utilisant cette extension). Cela est recommandé par rapport à d’autres solutions (telles que les macros de type fonction dans pgmspace.h pour l’AVR-8), car le compilateur peut effectuer les vérifications appropriées, sans quoi l’access aux données pointées par rest clair et simple.

En particulier, si vous rencontrez un problème similaire de portage à partir d’un compilateur offrant une sorte d’espace adresse nommé, tel que MPLAB C18, c’est probablement le moyen le plus rapide et le plus propre de le faire.

Les pointeurs portés depuis le haut ressemblent à ceci:

 uint8_t const* data1; uint8_t const __flash* data2; char const __flash** strdptr; 

(Si possible, on pourrait simplifier le processus en utilisant les définitions appropriées du préprocesseur)

(Réponse originale de Olaf)

Encapsulation des structures, pointeur à l’intérieur

Cette méthode vise à renforcer le typage des pointeurs en les enveloppant dans des structures. L’utilisation prévue est que vous transmettiez les structures elles-mêmes à travers des interfaces, grâce auxquelles le compilateur peut effectuer des contrôles de type sur elles.

Un type de “pointeur” vers des données d’octet pourrait ressembler à ceci:

 typedef struct{ uint8_t* ptr; }bytebuffer_ptr; 

Les données pointées peuvent être consultées comme suit:

 bytebuffer_ptr bbuf; (...) bbuf.ptr = allocate_bbuf(); (...) bbuf.ptr[index] = value; 

Un prototype de fonction acceptant un tel type et renvoyant un pourrait ressembler à ceci:

 bytebuffer_ptr encode_buffer(bytebuffer_ptr inbuf, size_t len); 

(Réponse originale par dvhh)

Encapsulation des structures, pointeur extérieur

Semblable à la méthode ci-dessus, il vise à renforcer le typage des pointeurs en les enveloppant dans des structures, mais de manière différente, en fournissant une contrainte plus robuste. Le type de données à désigner est celui qui est encapsulé.

Un type de “pointeur” vers des données d’octet pourrait ressembler à ceci:

 typedef struct{ uint8_t val; }byte_data; 

Les données pointées peuvent être consultées comme suit:

 byte_data* bbuf; (...) bbuf = allocate_bbuf(); (...) bbuf[index].val = value; 

Un prototype de fonction acceptant un tel type et renvoyant un pourrait ressembler à ceci:

 byte_data* encode_buffer(byte_data* inbuf, size_t len); 

(Réponse originale de Lundin)

Que devrais-je utiliser?

Les espaces d’adressage nommés à cet égard n’ont pas besoin de beaucoup de discussion: ils constituent la solution la plus appropriée si vous souhaitez uniquement gérer une particularité de vos espaces d’adressage de traitement cible. Le compilateur vous fournira les vérifications à la compilation dont vous avez besoin, sans que vous ayez à inventer.

Si, pour d’autres raisons, vous êtes intéressé par le wrapping de la structure, vous voudrez peut-être examiner ces questions:

  • Les deux méthodes peuvent être optimisées parfaitement: au moins, GCC générera un code identique, de l’un à l’autre, en utilisant des pointeurs ordinaires. Vous n’avez donc pas à prendre en compte les performances: elles devraient fonctionner.

  • Le pointeur interne est utile si vous avez des interfaces tierces pour servir les pointeurs de la demande, ou peut-être si vous refactorisez quelque chose d’aussi gros que vous ne pouvez pas faire en un seul passage.

  • Le pointeur extérieur offre une sécurité de type plus robuste car vous renforcez lui-même le type pointé: vous avez un type distinct, que vous ne pouvez pas convertir facilement (accidentellement) (conversion implicite).

  • Le pointeur extérieur vous permet d’utiliser des modificateurs sur le pointeur, tels que l’ajout de const , ce qui est important pour la création d’interfaces robustes (vous pouvez créer des données destinées à être lues uniquement par une fonction const ).

  • Gardez à l’esprit que certaines personnes pourraient ne pas aimer l’une ou l’autre de ces méthodes. Par conséquent, si vous travaillez en groupe ou créez du code pouvant être réutilisé par des parties connues, discutez-en d’abord avec eux.

  • Cela devrait être évident, mais gardez à l’esprit que l’encapsulation ne résout pas le problème de l’exigence d’un code d’access spécial (par exemple, par les macros pgmspace.h sur un AVR-8), en supposant qu’aucun espace d’adressage nommé ne soit utilisé parallèlement à la méthode. Il fournit uniquement une méthode permettant de générer une erreur de compilation si vous essayez d’utiliser un pointeur à l’aide de fonctions opérant sur un espace d’adressage différent de celui sur lequel il est destiné.

Merci pour toutes les réponses!

Les véritables architectures harvard utilisent différentes instructions pour accéder à différents types de mémoire, tels que du code (Flash sur AVR), des données (RAM), des registres de périphériques matériels (IO) et éventuellement d’autres. Les valeurs des adresses dans les plages se chevauchent généralement, c’est-à-dire qu’une même valeur accède à différents périphériques internes, en fonction de l’instruction.

En C, si vous souhaitez utiliser un pointeur unifié, cela signifie que vous devez non seulement coder l’adresse (valeur), mais également le type d’access (“espace d’adressage” dans la suite) dans la valeur du pointeur. Cela peut être fait en utilisant des bits supplémentaires dans la valeur d’un pointeur, mais également en sélectionnant l’instruction appropriée au moment de l’exécution pour chaque access . Cela constitue une surcharge significative pour le code généré. De plus, la valeur “naturelle” ne contient souvent pas de bits de réserve pour au moins certains espaces adresse (par exemple, les 16 bits du pointeur sont déjà utilisés pour l’adresse). Donc, des bits supplémentaires sont nécessaires, au moins un octet vaut. Cela augmente également l’utilisation de la mémoire (principalement de la RAM).

Les deux sont généralement inacceptables sur les MCU typiques utilisant cette architecture, car ils sont déjà assez limités. Heureusement, pour la plupart des applications, il est absolument inutile (ou facilement évitable du moins) de déterminer l’espace d’adressage au moment de l’exécution.

Pour résoudre ce problème, tous les compilateurs d’une telle plate-forme prennent en charge un moyen d’indiquer au compilateur dans quel espace adresse et quel object réside. Le projet de norme N1275 pour le futur C11 proposait une méthode standard utilisant des “espaces d’adressage nommés”. Malheureusement, il n’a pas été intégré à la version finale, nous n’avons donc que des extensions de compilateur.

Pour gcc (voir la documentation pour les autres compilateurs), les développeurs ont implémenté la proposition standard d’origine. Comme les espaces d’adresses sont spécifiques à la cible, le code n’est pas portable entre différentes archives, mais c’est normalement le cas pour le code intégré nu-métal, de toute façon, rien n’est vraiment perdu.

En lisant la documentation d’AVR, un espace d’adressage est simplement utilisé, similaire à un qualificatif standard. Le compilateur émettra automatiquement les instructions correctes pour accéder au bon espace. Il existe également un espace d’adressage unifié qui détermine la zone au moment de l’exécution, comme expliqué ci-dessus.

Les espaces adresse fonctionnent de la même manière que les qualificatifs. La compatibilité est donc soumise à des contraintes plus ssortingctes, par exemple lors de l’affectation de pointeurs de différents espaces adresse. Pour une description détaillée, voir la proposition, chapitre 5 .

Conclusion:

Les espaces d’adressage nommés sont ce que vous voulez. Ils résolvent deux problèmes:

  • Assurez-vous que les pointeurs sur des espaces adresse incompatibles ne peuvent pas être atsortingbués l’un à l’autre sans se faire remarquer.
  • Indiquez au compilateur comment accéder à l’object, c’est-à-dire quelles instructions utiliser.

En ce qui concerne les autres réponses proposant des struct , vous devez de toute façon spécifier l’espace d’adressage (et le type de void * ) une fois que vous avez accédé aux données. L’utilisation de l’espace d’adressage dans la déclaration conserve le rest du code propre et permet même de le modifier ultérieurement à un emplacement unique dans le code source.

Si vous êtes après la portabilité entre chaînes d’outils, relisez leur documentation et utilisez des macros. Il est fort probable que vous devrez simplement adopter les noms réels des espaces d’adressage.

Note: L’exemple PIC18 que vous citez utilise en fait la syntaxe des espaces d’adressage nommés. Seuls les noms sont obsolètes, car une implémentation doit laisser tous les noms non standard libres pour le code de l’application. D’où les noms qualifiés de soulignement dans gcc.

Disclaimer: Je n’ai pas testé les fonctionnalités, mais je me suis appuyé sur la documentation. Commentaires utiles dans les commentaires appréciés.