blog · git · desktop · images · contact
struct
aus einer Funktion zurückgibt2020-01-26
In C kann man neben einfachen Dingen wie int
oder char *
auch ganze
struct
s zurückgeben:
struct Foo
{
char name[13];
int age;
};
struct Foo
fill(void)
{
struct Foo out;
...
return out;
}
Fand ich immer etwas eigenartig, weil man Arrays nicht returnen kann –
außer natürlich, sie sind wie oben in einem struct
versteckt. Naja,
nicht abschweifen.
Die Frage ist, was unter der Haube passiert, wenn man ein struct
returned. Gemäß cdecl wird sowas wie ein int
in einem Register
zurückgegeben:
https://en.wikipedia.org/wiki/X86_calling_conventions#cdecl
Ein ganzes struct
passt da meistens nicht rein. Der Wikipedia-Artikel
gibt auf meine Frage eigentlich auch schon die Antwort, aber, faul wie
ich bin, habe ich beim ersten Mal nicht so weit gelesen. :-) Stattdessen
ein kleines Experiment gemacht:
#include <stdio.h>
struct Foo
{
char name[13];
int age;
};
struct Foo
fill(char this, char that, char whatever)
{
struct Foo out;
out.name[0] = this;
out.name[1] = that;
out.name[2] = whatever;
out.name[3] = 0;
out.age = 32;
asm("# fill a");
return out;
asm("# fill b");
}
int
main()
{
struct Foo joe;
asm("# a");
joe = fill('J', 'o', 'e');
asm("# b");
printf("[%s] %d\n", joe.name, joe.age);
return 0;
}
Den Code mit ein bisschen Inline-Assembler gespickt und dann das Ergebnis angeschaut:
clang -O0 -Wall -Wextra -o bla.S -S bla.c
Man sieht diesen Abschnitt:
#APP
# a
#NO_APP
leaq -56(%rbp), %rdi
movl $74, %esi
movl $111, %edx
movl $101, %ecx
callq fill
movl -40(%rbp), %ecx
movl %ecx, -16(%rbp)
movups -56(%rbp), %xmm0
movaps %xmm0, -32(%rbp)
#APP
# b
#NO_APP
Zur Erinnerung: rdi, rsi, rdx und rcx sind die ersten vier Register, um
Parameter an die Funktion zu übergeben. Mein fill()
hat aber auf Ebene
des C-Codes nur drei Parameter, also … hat der Compiler offensichtlich
einen zusätzlichen dort versteckt: leaq -56(%rbp), %rdi
, einen Pointer
auf den lokalen Stack der aufrufenden Funktion. Nachdem fill()
zurückkehrt, werden Daten von dort gelesen und nach -32(%rbp)
kopiert,
einer anderen Stelle auf dem lokalen Stack.
Und das ist die Antwort. Der C-Compiler fügt einen versteckten Parameter ein und die aufgerufene Funktion kann dann dorthin schreiben.
Interessanterweise benutzt clang hier mit xmm0
eines der SSE-Register
für diesen Kopiervorgang. Bei gcc sieht das so aus:
#APP
# 29 "bla.c" 1
# a
# 0 "" 2
#NO_APP
leaq -64(%rbp), %rax
movl $101, %ecx
movl $111, %edx
movl $74, %esi
movq %rax, %rdi
call fill
movq -64(%rbp), %rax
movq -56(%rbp), %rdx
movq %rax, -32(%rbp)
movq %rdx, -24(%rbp)
movl -48(%rbp), %eax
movl %eax, -16(%rbp)
#APP
# 31 "bla.c" 1
# b
# 0 "" 2
#NO_APP
Ein paar Instruktionen mehr. Ist jetzt kein bedeutungsschwangerer
Vergleich, weil wir eh -O0
benutzt haben, aber trotzdem interessant.
Noch eine Randbemerkung zu den Arrays: Ich sehe nicht so richtig einen
Grund, weshalb es – in der Theorie – nicht möglich sein sollte, Arrays
fester Länge zu übergeben. Immerhin geht’s ja, wenn sie in einem
struct
sind, weil die Größen eindeutig definiert sind. In einer
kleinen Mathe-Bibliothek, die ich mal geschrieben habe, benutze ich das
sehr häufig:
struct mat4 {
GLfloat v[16];
};
struct mat4
mat4_identity(void)
{
struct mat4 m = {0};
m.v[0] = 1;
m.v[5] = 1;
m.v[10] = 1;
m.v[15] = 1;
return m;
}
Damit kann man dann irgendwo im Code das Folgende schreiben, was in meinen Augen die Lesbarkeit gegenüber der manuellen Benutzung von Pointern deutlich erhöht:
void
something(void)
{
struct mat4 a;
a = mat4_identity();
}
Oder gar:
void
something(void)
{
struct vec3 r, t;
struct mat4 a;
struct quat4 q;
a = quaternion_to_rotational_matrix(q);
r = mat4_mult_vec3(a, t);
}