blog · git · desktop · images · contact


Wie C ein struct aus einer Funktion zurückgibt

2020-01-26

In C kann man neben einfachen Dingen wie int oder char * auch ganze structs 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);
}

Comments?