blog · git · desktop · images · contact & privacy · gopher
struct
from a function2020-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);
}