blog · git · desktop · images · contact


How C returns a struct from a function

2020-01-26

In C, you can return simple things like int or char *, but also a whole struct:

struct Foo
{
    char name[13];
    int age;
};

struct Foo
fill(void)
{
    struct Foo out;

    ...

    return out;
}

I’ve always found this to be a bit peculiar, because you can not return an array – well, unless it’s wrapped in a struct, as shown above. Anyway, let’s not get distracted.

The question is, what happens under the hood when returning a struct? According to cdecl, simple things like an int are returned in a register:

https://en.wikipedia.org/wiki/X86_calling_conventions#cdecl

A whole struct most likely does not fit into a register. That wikipedia article already gives the answer to my question, but lazy as I am, I didn’t read that far the first time. :-) So I did a little experiment:

#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;
}

Inject some inline assembler and then generate said assembly code:

clang -O0 -Wall -Wextra -o bla.S -S bla.c

We get to see this segment:

#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

Remember: rdi, rsi, rdx, rcx are the first four registers to pass parameters to a function. My fill() only has three parameters in the C code, so … the compiler obviously added another hidden parameter: leaq -56(%rbp), %rdi, a pointer to the local stack of the calling function. After fill() has returned, data is read from that area and copied to -32(%rbp), another location on the local stack.

And that’s the answer. The C compiler adds a hidden pointer to which the called function can write.

Interestingly, clang uses one of the SSE registers xmm0 to copy the data. gcc creates this code:

#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

A couple more instructions. Not really a meaningful comparison since we’re using -O0, but still interesting.

Another quick note on arrays: I don’t really see a reason why – in theory – it shouldn’t be possible to return arrays of fixed sizes from a function. After all, if wrapped in a struct, it works, because sizes are well defined. In a little math library I once wrote, I use this a lot:

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;
}

Allows you to do this somewhere in your code, which, I think, is really neat and greatly improved readability over manually handling pointers:

void
something(void)
{
    struct mat4 a;

    a = mat4_identity();
}

Or even:

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?