Importing a function with a struct ret seems to produce invalid code

I'm not sure exactly what is causing the problem (swift, the clang importer or the llvm back end) in my case, but somehow a call to a C function that returns a struct is not lowering correctly.

In my imported C header, I have these declarations...

typedef struct {
	unsigned char byte1;
	unsigned char byte2;
	unsigned char byte3;
	unsigned char byte4;
} longRand4Type;

longRand4Type _longRandom4();

When that is imported into Swift, the resulting declarations in IR look like this...

%struct.longRand4Type = type { i8, i8, i8, i8 }
; Function Attrs: optsize
declare void @_longRandom4(%struct.longRand4Type* sret align 1) local_unnamed_addr #1

...a typical call site in my code...
let r = _longRandom4()
...lowers to something like this...
call void @_longRandom4(%struct.longRand4Type* noalias nocapture nonnull sret %4)
...where %4 contains a pointer to an alloca'd 4 byte space on the stack where the result should go.

The call site is lowered into final machine code as something like...

     9c2:	c4 01       	movw	r24, r8
     9c4:	0e 94 2a 05 	call	0xa54	; 0xa54 <_longRandom4>

...where in this case, the pointer to the stack is in the r8/r9 register pair. And in the avr-gcc ABI, r45/r25 is the first parameter passed to a function.

So something in the clang importer/swift/IL etc. is producing code that passes a pointer to _longRandom4 rather than expecting _longRandom4 to return a 32 bit value in r22/r23/r24/r25 (which is what it actually does).

I guess what I'm trying to understand is what is at fault? Why does the clang importer produce IR that seems to return void and take a pointer parameter? Is that the llvm standard somehow? Or is that a bug in my version of the clang importer?

And if it's the expected behaviour, does this mean that there's a bug in the AVR back end, where it should "know" how to lower that IL... perhaps it should always be adding a shim of some sort, like copying the value of the pointer somewhere safe, then after the call to _longRandom4, copying the values of r22-r25 into the indirect referenced memory address?

As ever, if there's an AVR back end bug, I want to alert the maintainer, and also try to work out a pragmatic way forward, like making sure I never import a C function that returns a struct until it's fixed?

That’s a standard part of the C ABI on many platforms: structs are returned by filling in a hidden pointer argument. This allows all structs to be handled uniformly no matter whether they fit in registers. (Swift thinks this is silly and so its ABI on x86_64 and arm64 only does this if the struct does not fit in two registers, I believe.)

It sounds like the correct AVR C ABI behavior (lots of letters there!) is not the same as it is on x86_64 and arm64, but somewhere in the Swift compiler it’s assuming all imported struct-returning C functions do work like that. Unfortunately I’m not sure where that would be…probably IRGen. You could dump the SIL of your program to confirm: has the imported function been converted to void-returning at the SIL level or not?

“sret” (short for “struct return”) is a good thing to search for in the codebase when looking for special-casing.

1 Like

Swift IRGen is definitely not generically assuming that all structs are returned indirectly. Swift should be round-tripping the exact Clang function type it found in the C headers and asking Clang how values are returned for that type. It looks like Clang’s AVR support just used completely the wrong convention for return values until less than three months ago. That commit almost certainly just isn’t yet in the LLVM branch that Swift builds on.

3 Likes

Fantastic answers. Thank you so much @John_McCall and @jrose.

The callee is currently in a static library that is all compiled using avr-gcc (which obeys avr-gcc - GCC Wiki).

Clang and the llvm back end for AVR are supposed to follow that too but it looks like until the commit you mention, they do not.

For simplicity, I didn't include the source code for _longRandom4 in the last post...

longRand4Type _longRandom4() {
	long lr = _longRandom();
	return *((longRand4Type*)(&lr));
}

...where _longRandom is my PRNG API function that returns a long int (32 bit on AVR).

Based on what you found, I tried compiling it with clang (compiled from my llvm tree) instead of GCC.

With GCC...

00000a54 <_longRandom4>:
     a54:	0e 94 4d 0c 	call	0x189a	; 0x189a <_longRandom>
     a58:	08 95       	ret

With clang (built from my tree)...

00000c8c <_longRandom4>:
...PROLOGUE...
     ca4:	8c 01       	movw	r16, r24
     ca6:	0e 94 48 01 	call	0x290	; 0x290 <_longRandom>
     caa:	8b 83       	std	Y+3, r24	; 0x03
     cac:	9c 83       	std	Y+4, r25	; 0x04
     cae:	69 83       	std	Y+1, r22	; 0x01
     cb0:	7a 83       	std	Y+2, r23	; 0x02
     cb2:	8c 81       	ldd	r24, Y+4	; 0x04
     cb4:	f8 01       	movw	r30, r16
     cb6:	83 83       	std	Z+3, r24	; 0x03
     cb8:	8b 81       	ldd	r24, Y+3	; 0x03
     cba:	f8 01       	movw	r30, r16
     cbc:	82 83       	std	Z+2, r24	; 0x02
     cbe:	8a 81       	ldd	r24, Y+2	; 0x02
     cc0:	f8 01       	movw	r30, r16
     cc2:	d8 01       	movw	r26, r16
     cc4:	81 83       	std	Z+1, r24	; 0x01
     cc6:	89 81       	ldd	r24, Y+1	; 0x01
     cc8:	8c 93       	st	X, r24
...EPILOGUE...
     cde:	08 95       	ret

Which is very inefficient, but valid.

So I think in my case there are two ways forward:

  1. make sure any C functions that return a struct are compiled using clang, or
  2. patch my clang, llvm and swift with the cherry pick you mentioned.
2 Likes