Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Fonctions imbriquées en C

33 réponses
Avatar
Alain BARTHE
Bonsoir,

Une petite question sur les fonctions en C : depuis quand peut-on en C
imbriquer la définition d'une fonction à l'intérieur d'une autre ?

Je fais du C depuis plusieurs années et je suis tombé récemment sur un
truc du genre :

#include <stdio.h>

void main()
{
int i;

int f (int x)
{
return x * 3;
}

for (i = 0; i < 100; i++) printf ("%d = %d\n", i, f(i));
}

Ca peut-être assez pratique, pour ne pas polluer le code avec des
fonctions qui ne servent que dans une fonction donnée.

Je faisais des trucs du style en Pascal il y a bien longtemps, et je
savais que c'était autorisé en C++, mais je n'aurais jamais eu la
curiosité de le tester en C.

Pour info, ce code a été testé sous Linux avec gcc 4.1.2

10 réponses

1 2 3 4
Avatar
Alain BARTHE
Pour info, j'ai un vieux compilateur Sun sur Solaris 9 qui refuse cette
syntaxe :

$ : cc -V
cc: Forte Developer 7 C 5.4 2002/03/09
usage: cc [ options] files. Use 'cc -flags' for details

$ : uname -a
SunOS cisnew 5.9 Generic_117171-12 sun4u sparc SUNW,Sun-Fire-V250


Le Sat, 31 Jul 2010 23:05:56 +0000 (UTC),
Marc Espie écrivait :
In article <4c54a520$0$16565$,
Alain BARTHE wrote:
Bonsoir,

Une petite question sur les fonctions en C : depuis quand peut-on en C
imbriquer la définition d'une fonction à l'intérieur d'une autre ?


C'est une extension specifique a gcc.

Ca pose d'eventuels problemes. C'est implemente avec une technique appelee
"trampolines", qui necessite dans pas mal de cas de rendre la pile executable,
ce qui la rend nettement plus sensible aux attaques de type buffer-overflow...



Que ce soit une extension, peut-être, je ne me suis jamais penché
sur ce problème. En revanche, je ne connais pas de compilo qui
refuse les fonctions imbriquées. gcc 2.7 l'autorisait déjà, DEC C
aussi, le compilo Sun n'est pas gêné... Aurais-tu des exemples ? Je
pose la question parce que pour une histoire de lisibilité,
j'utilise ça dans quelques programmes et je n'ai jamais eu un
utilisateur qui soit venu râler parce que la bibliothèque en
question refusait de compiler...

Cordialement,

JKB

Avatar
Xavier Roche
Marc Espie a écrit :
Tiens, je savais pas.
http://pax.grsecurity.net/docs/emutramp.txt
explique en detail ce que ca fait... terrain connu, et solutions connues.



Ah, j'ai pigé la raison du trampoline.

Exemple:

#include <stdio.h>
static int callme(int (*f)(int), int i) {
return f(i);
}

void main()
{
int i;

int f (int x) {
return i * x * 3;
}

for (i = 0; i < 100; i++) printf ("%d = %dn", i, callme(f, i));
}

Ce hack permet en fait de trimballer artificiellement un contexte dans
un pointeur de fonction, ce qui peut être assez pratique.

[Sauf quand le noyau est prévu pour refuser ce genre d'exotisme
évidemment. Sur un tel environnement, le programme ci-dessus va crasher
avec quelque chose comme:

PAX: From 192.168.1.1: execution attempt in: <anonymous mapping>,
59160000-59181000 bffdf000
PAX: terminating task: /tmp/a.out(a.out):15067, uid/euid: 1003/1003, PC:
591801c4, SP: 591801ac
PAX: bytes at PC: b9 c0 01 18 59 e9 e2 81 ec ae 18 59 f0 01 18 59 f4 ef
68 4f
PAX: bytes at SP-4: 591801b8 08048405 00000000 080495f8 591801e8
08048469 00000000 1801c0b9 81e2e959 5918aeec 591801f0 4f68eff4 59180268
4f562c76 08048450 00000000 59180268 4f562c76 00000001 59180294 5918029c
]
Avatar
Erwan David
Xavier Roche écrivait :

Marc Espie a écrit :
Tiens, je savais pas.
http://pax.grsecurity.net/docs/emutramp.txt
explique en detail ce que ca fait... terrain connu, et solutions connues.



Ah, j'ai pigé la raison du trampoline.

Exemple:

#include <stdio.h>
static int callme(int (*f)(int), int i) {
return f(i);
}

void main()
{
int i;

int f (int x) {
return i * x * 3;
}

for (i = 0; i < 100; i++) printf ("%d = %dn", i, callme(f, i));
}

Ce hack permet en fait de trimballer artificiellement un contexte dans
un pointeur de fonction, ce qui peut être assez pratique.



Ça fait une closure comme en programmation fonctionnelle quoi.

--
Le travail n'est pas une bonne chose. Si ça l'était,
les riches l'auraient accaparé
Avatar
espie
In article <4c5599dc$0$10129$,
Samuel DEVULDER wrote:
La convention d'appel de cette fonction imbriqué n'est donc pas du tout
similaire à celle des autres fonctions C. Aussi quand il faut envoyer un
pointeur sur cette fonction, gcc alloue sur la pile un peu de place, et
y écrit un code ASM qui 1/ push les bons pointeurs supplémentaires sur
le contexte 2/ appelle la fonction statique correspondante. L'addresse
du code ainsi généré dynamiquement peut alors être utilisé comme un
pointeur sur fonction classique.



C'est a la fois joli et totalement mauvais.

Ca sent trop l'implementation de type "tiens je suis en train de faire
ma these sur les implementations de clotures, si j'en mettais un bout
dans gcc".

Outre le fait que les systemes modernes n'ont plus la pile en executable,
c'est quand meme de la generation de code au vol, ce qui sur tous les
processeurs decents (c'est-a-dire pas intel et ses cradouilleries)
necessite d'invalider des lignes de caches... donc pas du tout terrible
en performance (sur intel, il y a de la logique dans le processeur pour
invalider les lignes en question automatiquement dans ce contexte, ca
n'empeche pas que c'est pas trop bon en perfs, un peu comme le fait d'acceder
a de la memoire pas alignee fonctionne, mais rame un peu).

"la solution" serait sans doute similaire a celle utilisee pour les
gestionnaires de signaux: a savoir parametrer un peu plus le bout de code
genere au vol, de facon a ne plus avoir a le generer au vol, et quitte
a se taper un petit bout d'indirection supplementaire, a finalement
appeler du code standard, qui ne foutra pas le bordel dans le cache...
Avatar
Samuel DEVULDER
Marc Espie a écrit :
In article <4c5599dc$0$10129$,
Samuel DEVULDER wrote:
La convention d'appel de cette fonction imbriqué n'est donc pas du tout
similaire à celle des autres fonctions C. Aussi quand il faut envoyer un
pointeur sur cette fonction, gcc alloue sur la pile un peu de place, et
y écrit un code ASM qui 1/ push les bons pointeurs supplémentaires sur
le contexte 2/ appelle la fonction statique correspondante. L'addresse
du code ainsi généré dynamiquement peut alors être utilisé comme un
pointeur sur fonction classique.



C'est a la fois joli et totalement mauvais.



Possible, je n'ai fait que zieuter au code ASM que GCC générait sur x86.

Ca sent trop l'implementation de type "tiens je suis en train de faire
ma these sur les implementations de clotures, si j'en mettais un bout
dans gcc".



C'est comme ca que ca s'est vraiment passé dans gcc tu penses?
J'aimerais bien connaitre qui a eu cette idée d'implémenter les
fonctions embarquées, leur limitations et la motivation derrière
(callbacks dans des GUI?)

Outre le fait que les systemes modernes n'ont plus la pile en executable,
c'est quand meme de la generation de code au vol, ce qui sur tous les



Oui.. Et c'est ca que je trouve suspect dans ce que j'ai vu du code
asm.. En même temps la fonction embarquée quand i=3 (la vars locale
référencée), n'est pas identique à la fonction quand i=4. Les deux
fonctions peuvent être utilisées simultanément et ne pas se mélanger les
pieds. Ca me semble nécéssiter une notion de génération dynamique de
"quelque chose d'executable" qui capture l'état de la pile du prog à ce
moment là.

En fait chaque référence utilisée de la fonction a potentiellement un
contexte différent. On a donc affaire à plusieurs fonctions distinctes
les unes des autres du point de vu "bas niveau" à chaque référence d'une
telle fonction.

processeurs decents (c'est-a-dire pas intel et ses cradouilleries)
necessite d'invalider des lignes de caches... donc pas du tout terrible
en performance (sur intel, il y a de la logique dans le processeur pour
invalider les lignes en question automatiquement dans ce contexte, ca
n'empeche pas que c'est pas trop bon en perfs, un peu comme le fait d'acceder
a de la memoire pas alignee fonctionne, mais rame un peu).



oui tout à fait.

"la solution" serait sans doute similaire a celle utilisee pour les
gestionnaires de signaux: a savoir parametrer un peu plus le bout de code
genere au vol, de facon a ne plus avoir a le generer au vol, et quitte
a se taper un petit bout d'indirection supplementaire, a finalement
appeler du code standard, qui ne foutra pas le bordel dans le cache...



Est-ce que ca signifie changer la convention d'appel des fonctions C en
général pour non seulement passer un pointeur sur le code, mais en plus
un pointeur sur la stack-frame de l'appelant? La notion même de
stack-frame pourrait être déplacée de la pile du CPU vers le tas pour
n'empiler que ces deux pointeurs en question. En effet de bord ca
aiderait à réduire la consommation de pile pour les cpu qui en ont peu
(et aussi dans le cadre de multithread massif).

sam.
Avatar
espie
In article <4c55a950$0$23440$,
Samuel DEVULDER wrote:
Est-ce que ca signifie changer la convention d'appel des fonctions C en
général pour non seulement passer un pointeur sur le code, mais en plus
un pointeur sur la stack-frame de l'appelant? La notion même de
stack-frame pourrait être déplacée de la pile du CPU vers le tas pour
n'empiler que ces deux pointeurs en question. En effet de bord ca
aiderait à réduire la consommation de pile pour les cpu qui en ont peu
(et aussi dans le cadre de multithread massif).



Ca peut etre une des raisons pour declarer tes fonctions en static:
si ta fonction est purement locale a l'unite de compilation (static, et pas
de pointeur de fonction qui s'echappe), le compilo peut en profiter pour
s'affranchir de l'ABI de ton processeur, et faire exactement ce qu'il a
envie avec les appels a ta fonction. C'est ce que font certains compilos
recents.

Une des raisons pour lesquelles ca n'est pas plus repandu que ca est que
C a une longue tradition de langage "non fonctionnel", ou le programmeur
optimise "a la main" son code (et faut dire qu'il y a quinze ans, la
technologie des compilateurs le necessitait), et donc les optimisations
globales de ce genre n'ont pas un si fort impact que ca sur le code existant.
Avatar
Gabriel Dos Reis
Xavier Roche writes:

| Marc Espie a écrit :
| > Tiens, je savais pas.
| > http://pax.grsecurity.net/docs/emutramp.txt
| > explique en detail ce que ca fait... terrain connu, et solutions connue s.
|
| Ah, j'ai pigé la raison du trampoline.

Il faut distinguer une implémentation de sa spécification.
« trampoline  » est une technique d'implémentation.
Avatar
Gabriel Dos Reis
Erwan David writes:


[...]

| > Ce hack permet en fait de trimballer artificiellement un contexte dans
| > un pointeur de fonction, ce qui peut être assez pratique.
|
| Ça fait une closure comme en programmation fonctionnelle quoi.

Comme Pascal.
Avatar
Gabriel Dos Reis
Xavier Roche writes:

| Erwan David a écrit :
| > Ça fait une closure comme en programmation fonctionnelle quoi.
|
| Du coup j'imagine que gcc recopie le contexte avec le pointeur ?
| Comment est géré le cycle de vie de la frame en question du cou p ? A

Comme C -- la durée de vie est déterminée par la durée dynamique de la
fonction englobante.
Avatar
Gabriel Dos Reis
(Marc Espie) writes:

| Outre le fait que les systemes modernes n'ont plus la pile en executable,
| c'est quand meme de la generation de code au vol, ce qui sur tous les
| processeurs decents (c'est-a-dire pas intel et ses cradouilleries)
| necessite d'invalider des lignes de caches... donc pas du tout terrible
| en performance (sur intel, il y a de la logique dans le processeur pour
| invalider les lignes en question automatiquement dans ce contexte, ca
| n'empeche pas que c'est pas trop bon en perfs, un peu comme le fait d'acc eder
| a de la memoire pas alignee fonctionne, mais rame un peu).
|
| "la solution" serait sans doute similaire a celle utilisee pour les
| gestionnaires de signaux: a savoir parametrer un peu plus le bout de code
| genere au vol, de facon a ne plus avoir a le generer au vol, et quitte
| a se taper un petit bout d'indirection supplementaire, a finalement
| appeler du code standard, qui ne foutra pas le bordel dans le cache...

C++0x autorise des lambdas avec possibilité de capture de variable
locales par référence. Le tout est réecrit comme si on avait une classe
qui surcharge l'operateur d'appel de fonction, les variables capturées
étant données membres. Pas besoin de pile/tas éxécutabl e.
1 2 3 4