Skip to content

Tracking issue for refactoring the way we represent function call ABIs #119183

Open

Description

@RalfJung

View all comments

This is the issue tracking implementation of rust-lang/compiler-team#672. Note that we do not have a final design yet; the best way to represent call ABI, and to disentangle it from the "storage kind" of a type (which is what the Abi type currently largely represents) is yet to be determined.

Note that for the purposes of this, by "call ABI" I mean "the target-independent information that is necessary and sufficient to compute how arguments and returns values are passed between caller and callee". The perfect end state (that we may or may not ever reach) for this would be to say that two types are ABI compatible if and only if the computed call ABI information for them is the same -- that would be very nice for the spec and for MiniRust, anyway.

I do not mean "the target-specific information saying which arguments are passed in which register / on the stack, which are copied and which are passed indirectly". This already exists to some extend as a concept in rustc, called PassMode. It may need some reforming, but that would be a separate discussion.

  • The core need is that a Rust type must have its ABI computed in a manner that is not reimplemented for every target and every codegen backend. e.g. the traversal over types should skip repr(transparent) in essentially every case. Then every target architecture must be ported to it:
    • aarch64
    • arm
    • csky
    • loongarch
    • loongarch64
    • mips
    • mips64
    • powerpc
    • powerpc64
    • riscv32
    • riscv64
    • sparc
    • sparc64
    • x86
    • x86_64
  • layout.abi is not about actual ABI, but only about IR codegen: handling the types as SSA values versus handling them as memory locations.

See in particular this comment.

Implementation history

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Dec 21, 2023
  2. added
    C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFC
    A-ABIArea: Concerning the application binary interface (ABI)
    C-cleanupCategory: PRs that clean code up or issues documenting cleanup.
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFC
    on Dec 21, 2023
  3. bjorn3 commented on Dec 21, 2023

    @bjorn3
  4. RalfJung commented on Dec 21, 2023

    @RalfJung
    Author
  5. added
    C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFC
    on Dec 21, 2023
  6. RalfJung commented on May 29, 2024

    @RalfJung
    MemberAuthor

    Looks like fixing #117480 may be blocked on this.

  7. added
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    on Oct 16, 2024
  8. workingjubilee commented on Oct 16, 2024

    @workingjubilee
    Member

    I've started work on cleaning up ABI code. The first few PRs are just going to be moving stuff around so that we can actually co-locate as much of the ABI code as possible:

    I'll try to implement the MCP proper as I go along.

  9. RalfJung commented on Oct 16, 2024

    @RalfJung
    MemberAuthor

    Awesome. :-)

    If you want feedback on some sketches of what the ABI might look like, feel free to post them here.

  10. added a commit that references this issue on Oct 31, 2024
  11. 11 remaining items

  12. workingjubilee commented on Feb 11, 2025

    @workingjubilee
    Member

    I am aware of that but it seems functionally identical to generating a new extern "{CC}" fn(A) -> R, with CC, A, and R based on but non-identical to the original, and rewriting the call site to call this new function?

    A related concern that has been pointed out to me in the past is that sometimes, even with inlining, the codegen backend can have problems erasing the prologue and epilogue. Ideally, we would find a way to make "handling the ABI of this call" semantically separable from the actual motion of "making this call", so that we can delay addressing the need for a prologue and epilogue as long as possible. Notionally the function call ABI, after all, should be a non-event, as otherwise these functions aren't very functional.

  13. RalfJung commented on Feb 11, 2025

    @RalfJung
    MemberAuthor

    We currently talk a lot about registers in our ABI handling, but that's kind of a lie. All it means is "represent this to LLVM as a scalar / array of scalars" and then LLVM decides whether to put that into registers or on the stack depending on the target's conventions. (Well, there's a flag for forcing things to be in a register -- mixed up with other flags that are irrelevant for the ABI. I don't know the exact semantics of that, it's probably target-specific.)

    I don't think we should go any more low-level than this on the Rust side. I'd rather not have us be in the business of counting how many argument go in registers vs on the stack. That's a lot of work, very hard to test, and AFAIK none of our codegen backends actually give us that level of control anyway.

    The main goal of the original MCP was to make writing our target-specific foreign ABI adjustments easier and less fragile. For that code, the C ABI is exactly the right abstraction, as that's the level the ABI is specified at: we need to tell that code what the given type "looks like as a C type", and then the code needs to compute a corresponding PassMode. So I think that kind of machinery is definitely on the requirements list here. The MCP talks mostly about how to represent the input to this step; you seem to be thinking mostly about the output? To be honest I have not thought about this much; what we have here currently seems to work well enough for both LLVM and cranelift, so it's not entirely terrible.

    A related concern that has been pointed out to me in the past is that sometimes, even with inlining, the codegen backend can have problems erasing the prologue and epilogue. Ideally, we would find a way to make "handling the ABI of this call" semantically separable from the actual motion of "making this call", so that we can delay addressing the need for a prologue and epilogue as long as possible. Notionally the function call ABI, after all, should be a non-event, as otherwise these functions aren't very functional.

    That kind of work needs to start in the backends though; LLVM / cranelift would require a higher-level way to express the ABI so that they don't require a bunch of instructions that must later be optimized away again (e.g. when inlining). This also points towards a rather higher-level than lower-level abstraction, i.e., not "register vs stack".

  14. workingjubilee commented on Feb 11, 2025

    @workingjubilee
    Member

    LLVM does not implement the C ABI for us.

  15. RalfJung commented on Feb 11, 2025

    @RalfJung
    MemberAuthor

    Correct. We need to carry a function for each supported architecture (sometimes more than one per arch) that are given "the C view of the type" and compute the PassMode. For this, obviously we should have a way to represent "the C view of the type" so we can use it as input for that function -- right?

    Regarding the point about multi-register returns, I don't see why we couldn't extend PassMode to support that. But as noted above that is an entirely orthogonal question to how we represent the input to the ABI adjustment functions, which is what the MCP is primarily about.

  16. RalfJung commented on Feb 11, 2025

    @RalfJung
    MemberAuthor

    I think you are talking about a completely different layer than what I am talking about in the MCP and the issue description. We need two "ABI representations":

    • One representation sits between the notion of a Rust type, and the target-specific ABI logic. This is a function of type fn(TyAndLayout) -> AbiRepr, and crucially it must have the property that ABI-compatible Rust types are mapped to the same AbiRepr. This function can and should be target-independent. AbiRepr is a new concept that does not exist in rustc yet but I think having it would greatly improve the correctness of our foreign ABI adjustments.
    • Since codegen backends do not implement the entire C ABI for us (they implement part of it, like knowing how many arguments to put into registers vs the stack, but many other parts have to be handled by the frontend), we also need to have anther layer below this, which is target-and-ABI-specific. This layer consists of a function fn(AbiRepr) -> PassMode. I am sure PassMode can be improved, and in fact PassMode::Cast will have to be extended as currently it cannot represent everything LLVM can represent and that leads to ABI bugs on some targets. But I don't see which problem would be solved by making PassMode so low-level that we have to know how many arguments to put in registers; I think (and @bjorn3 has confirmed in the past) that it sits at a pretty good level of abstraction.

    Currently we have effectively have a function fn(TyAndLayout) -> PassMode for each foreign ABI (except we express this more like fn(TyAndLayout, &mut PassMode), which is even worse). That's bad as it means ABI adjustments can easily accidentally map two ABI-compatible types to different PassMode; I fixed a bunch of those bugs last year. This proposal is about factoring that into two functions with an intermediate abstraction, AbiRepr. I think the vocabulary of C types is not a bad template for this. In particular, using C types here does in no way preclude using multiple return registers; that can still be realized by implementing the "Rust" ABI adjustment to e.g. map a 2-element AbiRepr::Struct to a PassMode that represents a multi-register return.

    I don't know where you went on a different track than my line of thoughts here but clearly we're not talking about the same thing, maybe this helps. :)

  17. workingjubilee commented on Feb 11, 2025

    @workingjubilee
    Member

    It simply does not make any sense to me to talk about things in terms of C types if they do not have to follow the C ABI, so I think about these elements in a more decomposed way?

  18. workingjubilee commented on Feb 11, 2025

    @workingjubilee
    Member

    I think I will simply talk less about these things because it is not very useful for me to try to record my thoughts in forms that are likely to be misunderstood. I understand what you are saying about your primary hope was to make the input forms more sensical, and I do intend to address that, I just am thinking about this from the origin of the demands... "we want programs that do FFI to successfully execute... which means certain things have to go into certain registers... which means that they have to..." etc.

  19. RalfJung commented on Feb 11, 2025

    @RalfJung
    MemberAuthor

    It simply does not make any sense to me to talk about things in terms of C types if they do not have to follow the C ABI, so I think about these elements in a more decomposed way?

    We need some sort of high-level language of types (well, higher-level than PassMode, but lower-level than Rust types) that can serve as input for the target-specific ABI adjustments. I don't care what you call this. I called in AbiRepr above. We don't have to call it "C types" if you don't like that, we can just call it "structs and unions and leaf types" if you prefer 🤷 .

    I just am thinking about this from the origin of the demands... "we want programs that do FFI to successfully execute... which means certain things have to go into certain registers... which means that they have to..." etc.

    That would make sense if we were emitting the assembly ourselves, but we are not. We are targeting the language of our backends in terms of how they represent ABI. PassMode is designed around that. It works well enough; the ABI issues we have seen have (almost all) not been caused by PassMode being inadequate. They have been caused by the fact that the ABI adjustments take as input a Rust type and that makes it extremely easy to map different-but-defined-to-be-ABI-compatible Rust types to different PassMode. We need a normalization pass that runs before invoking the target-specific logic, so that we only have to worry once about mapping different-but-defined-to-be-ABI-compatible Rust types to identical outputs, rather than having to do that for every single target. That is what this issue attempts to fix. I'm not sure what you are trying to fix but maybe it is something else. :)

  20. RalfJung commented on Feb 11, 2025

    @RalfJung
    MemberAuthor

    Maybe I once again fell in the trap of "call ABI" meaning like a dozen different things to different people. For the purposes of this discussion, by "call ABI" I mean "the target-independent information that is necessary and sufficient to compute how arguments and returns values are passed between caller and callee". The perfect end state (that we may or may not ever reach) for this would be to say that two types are ABI compatible if and only if the computed call ABI information for them is the same -- that would be very nice for the spec and for MiniRust, anyway.

    The process of mapping that information to the concrete things you talk about (registers vs stack etc) is obviously highly target- and ABI-specific. But the input to that process seems to me to be expressible in a nice reasonably high-level target-specific way.

  21. bjorn3 commented on Feb 11, 2025

    @bjorn3
    Member

    @RalfJung

    Regarding the point about multi-register returns, I don't see why we couldn't extend PassMode to support that.

    In fact we can already represent 4 register returns on 64bit and 8 register returns on 32bit through PassMode::Pair(i128, i128) And I think even more using PassMode::Cast. LLVM will make up an ABI when you do however and in many cases this made up ABI returns the return value using a return area pointer rather than in registers.

    @workingjubilee

    It simply does not make any sense to me to talk about things in terms of C types if they do not have to follow the C ABI, so I think about these elements in a more decomposed way?

    For lowering the C ABI we very much have to first map Rust types into C types as the C ABI is specified as a mapping from C types to registers and stack locations. Some of the ABI issues we have are precisely because the ABI implementation for each architecture does an implicit ad-hoc mapping from Rust to C types rather than sharing this such that we only have to get it right once. The Rust ABI follows a different code path and as such doesn't necessarily have to use this lowering to C types. It could keep directly operating on Rust types if we want.

  22. workingjubilee commented on Feb 11, 2025

    @workingjubilee
    Member

    And we also have targets with two C-like ABIs.

  23. RalfJung commented on Feb 11, 2025

    @RalfJung
    MemberAuthor
  24. RalfJung commented on Feb 11, 2025

    @RalfJung
    MemberAuthor
  25. RalfJung commented on Sep 8, 2026

    @RalfJung
    MemberAuthor

    FWIW it's not just all the ABI adjustments that have to duplicate similar logic here. There are other parts of the compiler that also need effective ABI information and that are duplicating parts of the same logic, often incorrectly:

    • CFI
    • and the ongoing pointer authentication (PAC) work
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

A-ABIArea: Concerning the application binary interface (ABI)C-cleanupCategory: PRs that clean code up or issues documenting cleanup.C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions