Hi! I'm adding structs to my toy compiler (custom frontend, cranelift as backend) and I was wondering about how to pass structs by value when calling functions.
As far as I understand, the specific requirements of how to call functions are up to the ABI. I know it's often either splitting up the struct into multiple arguments or creating a copy of the struct in the caller's stack, and passing a pointer to that value (depending on size). In LLVM, you don't need to care about all this because struct types are part of the IR, but with cranelift, it seems things are a bit more low level since there are no structs in the IR.
So, my question is. Do I need to have platform-specific code that adheres to the calling conventions of each platform when passing structs by value? Is there some helper code in cranelift already to achieve this?
Setzer22 has marked this topic as resolved.
Setzer22 has marked this topic as unresolved.
Cranelift doesn't have any helper code to handle calling conventions. It only exposes the primitives necessary like the StructArgument
atgument purpose for values that have been determined to be passed as a struct on the stack. You need your own code to determine if the struct should be passed on the stack with StructArgument
or put in registers.
You could look at compiler/rustc_target/src/call
in https://github.com/rust-lang/rust to see what it would roughly be. The code for sysv x86_64 is somewhat complex, but most other archs have simpler code.
Thanks! I'll have a look :)
One question, though. When you say "Cranelift doesn't have any helper code to handle calling conventions", I'm assuming cranelift is still doing some work for me, right? For instance, picking the right registers in the right order, and fetching the return value from the right place too? Basically, how much does cranelift do for me here, and what do I need to care about on my end?
Cranelift handles register assignment and in case of too much registers the placement of arguments on the stack. You are responsible for specifying the correct argument extension, argument purpose and lowering structs into primitive parts or using the struct argument argument purpose depending on the type. In addition for returns depending on the type you either add returns or add an extra pointer argument with the struct return argument purpose and write to this pointer on the callee side and pass a pointer to the location to put the return value as argument.
Just to make it extra-clear, I would say it as: Cranelift does handle calling conventions, but only for the concepts that exist in its IR. Primitive types (i32/64, f32/64, 128-bit vectors) exist, so Cranelift can pass them as args and return them. Structs do not exist at the Cranelift level, so there's no ABI handling; you're responsible for that lowering Hopefully that's a useful way to think about it
Oops, forgot about replying. Thanks a lot for the answers! :) Things are a lot clearer now
I'm finally delving into the intricacies of calling conventions :sweat_smile: I had a few questions about ArgumentPurpose
.
First, there's StructArgument
, which IIUC is necessary whenever the calling convention requires me to pass a struct via pointer to the stack (e.g. systemv requires this for structs with the MEMORY type). I don't understand why this takes a size argument. I thought I was expected to allocate a stack slot in the caller and pass its address as a pointer, but grepping through the code it seems cranelift-codegen itself is handling the allocation of that stack slot for me whenver I specify an argument with purpose StructArgument(size)? So my question is: Who is responsible from allocating that stack slot? And in case it's cranelift and not me, how do I figure out the address where I have to copy the struct data before the function call?
I'm also quite confused about StructReturn
for similar reasons. StructReturn is used for large return types that don't fit in registers. In those cases, the caller allocates space on the stack, and passes a pointer as a hidden first argument. The callee then writes the result in that stack slot. Here, again, who allocates this stack slot? There's no size argument here, which makes me think it's the caller who allocates it.
Also about StructReturn
. There's a note in the docs about some ABIs requiring it as a return type. But in the case of SystemV, the StructReturn argument has to be the first argument to the function, right?
Thanks again! And hope you don't mind I'm asking so much stuff
how do I figure out the address where I have to copy the struct data before the function call
Okay, after looking into it a bit more, it seems cranelift-codegen is also handling the memcpy for me. So in that case, it looks like I should simply pass any pointer to the struct. Could someone confirm this is correct? :smile:
cc @bjorn3 who has implemented this for cg_clif
For StructArgument the argument is a pointer to the value you want to pass. Cranelift will copy it to the location on the stack where the callee expects it and on the callee side, the parameter will be a pointer to this copy.
Thanks! So it seems I was on the right track in the end
And in the case of StructReturn, I guess I'm responsible for allocating the satck space and providing a pointer argument, correct?
Indeed. For StructReturn the caller passes a pointer to a chunk of memory large enough to hold the return value and then the callee can write to this memory. The only reason that it is a separate argument purpose is that some calling conventions pass the struct return pointer in a register which isn't used for regular arguments.
AArch64 for example passes it in x8, but uses x0-x7 for regular args. x86 however simply puts it as first argument. Note that Cranelift doesn't currently handle this on x86, so you need to ensure that the StructReturn argument is the first argument.
Another thing I wanted to ask about is packing struct fields in registers. Let's say I have a {i32, i32, i64} struct. Do I have two pass that as three AbiParams (two with types::I32, and one with types::I64), or do I have to follow the calling convention and pack the first two into an i64 AbiParam (and thus, have a signature with just two arguments)? Basically, who is responsible for doing that, me or cranelift? (my example assumes SystemV)
You have to pack it if the calling convention specifies this. Cranelift doesn't have native struct types. It only exposes the primitives necessary to handle them around the calling convention, but it is the responsibility of the clir ir producer to use those primitives according to the requirements of the calling convention.
So, back to my example, this means if the ABI tells me I have to pack the first two ints into a single register, the signature I build in CLIF should have two arguments, not three, and I have to do some bit-fiddling on the frontend to actually produce the packed value. Did I get that right?
Yes
Ok, seems I finally got it working! :grinning_face_with_smiling_eyes:
image.png
Thanks a lot for bearing with me! :smile: Can't promise I won't be back with more questions, but in exchange, if you think there's a good place in the cranelift docs where I could write a piece on passing / returning structs by value, I think that could be valuable to future readers (and I'd be up for the task)
@Setzer22 that's great, congrats!
Off the top of my head, we don't really have many good documents on "ways to use Cranelift to implement common language features" -- a section on that (i.e. a new Markdown file in cranelift/docs/) might be really nice. It could eventually be a part of a user's/embedder's guide, or example/tutorial language implementation, etc. In any case, please feel free to write up something and we can hammer it into the shape we want incrementally!
Alright! I'll write up something and let you know
bjorn3 said:
You could look at
compiler/rustc_target/src/call
in https://github.com/rust-lang/rust to see what it would roughly be. The code for sysv x86_64 is somewhat complex, but most other archs have simpler code.
Seems there is no longer a call
dir in compiler/rustc_target/src
... where should I look these days for an example of lowering structs?
Setzer22 said:
Alright! I'll write up something and let you know
Did you write up something? or do you have some code online? :)
I'm guessing this is codegen for struct field accesses? https://github.com/rust-lang/rust/blob/498553fc04f6a3fdc53412320f4e913bc53bc267/compiler/rustc_codegen_cranelift/src/value_and_place.rs#L8
Taylor Holliday said:
bjorn3 said:
You could look at
compiler/rustc_target/src/call
in https://github.com/rust-lang/rust to see what it would roughly be. The code for sysv x86_64 is somewhat complex, but most other archs have simpler code.Seems there is no longer a
call
dir incompiler/rustc_target/src
... where should I look these days for an example of lowering structs?
Was missing abi/
in between. The right link is https://github.com/rust-lang/rust/tree/master/compiler/rustc_target/src/abi/call
Taylor Holliday said:
I'm guessing this is codegen for struct field accesses? https://github.com/rust-lang/rust/blob/498553fc04f6a3fdc53412320f4e913bc53bc267/compiler/rustc_codegen_cranelift/src/value_and_place.rs#L8
Yeah. That code is not really relevant to the calling convention though.
The code around https://github.com/rust-lang/rust/blob/498553fc04f6a3fdc53412320f4e913bc53bc267/compiler/rustc_codegen_cranelift/src/abi/pass_mode.rs#L78 is relevant. cg_clif uses rustc_target for calculating the abi to use and this code actually applies the calculated abi.
Setzer22 said:
Alright! I'll write up something and let you know
ever manage to get around to doing this :) ?
Last updated: Dec 23 2024 at 12:05 UTC