I'm trying to figure out how to detect if a certain c++-pass can be passed directly in a register, or whether an address has to be passed in the x64-calling convention.
For reference, I have my own compiler that generates native code, for a custom scripting-language. This code is interfacing with a c++-application, and currently only supports x64/windows, on the MSVC compiler. Previously, I've used wrappers/abstractions for a lot of things, for example a custom Stack-class for passing and popping parameters, but I'm in the process of handling all calls natively - so I have to get the x64 calling convention right.
There are two cases, passing parameters in, and returning them. Let's start with return, as I think I've got that one mostly right already.
Return-values:
MSDN mentions:
User-defined types can be returned by value from global functions and static member functions. To return a user-defined type by value in RAX, it must have a length of 1, 2, 4, 8, 16, 32, or 64 bits. It must also have no user-defined constructor, destructor, or copy assignment operator. It can have no private or protected non-static data members, and no non-static data members of reference type. It can't have base classes or virtual functions. And, it can only have data members that also meet these requirements. (This definition is essentially the same as a C++03 POD type. Because the definition has changed in the C++11 standard, we don't recommend using std::is_pod for this test.) Otherwise, the caller must allocate memory for the return value and pass a pointer to it as the first argument. The remaining arguments are then shifted one argument to the right. The same pointer must be returned by the callee in RAX.
So they say you cannot use std::pod, but don't offer an alternative. Kay? From just reading those requirements, I've been able to come up with the following trait:
template <typename T>
concept CanBeReturnedInRAX =
std::is_trivially_constructible_v<T> &&
std::is_trivially_destructible_v<T> &&
std::is_trivially_copyable_v<T> &&
std::is_standard_layout_v<T> &&
sizeof(T) <= 8;
So the first question is: Is this trait correct? If not, did I miss something? It seems to be handling a lot of cases that I have correctly, but it does not express the documentation 1:1, and I want to avoid some edge-cases.
Parameter-passing
I'm a bit lost here. MSDN says:
Structs and unions of size 8, 16, 32, or 64 bits, and __m64 types, are passed as if they were integers of the same size. Structs or unions of other sizes are passed as a pointer to memory allocated by the caller. For these aggregate types passed as a pointer, including __m128, the caller-allocated temporary memory must be 16-byte aligned.
Here, no actual requirements are specified, it only says that structs of certain sizes are passed as-if they were integers. But then it talkes about "aggregate types" - so what types does that actually entail? It seems to be less restrictive than the return-values, as the following type seems to be passed in-register (but cannot be returned in one):
struct YieldConfigBase {};
struct AE_API YieldConfigFull final :
public impl::YieldConfigBase
{
constexpr YieldConfigFull(bool overwrite, bool isBlocking) noexcept :
overwrite(overwrite), isBlocking(isBlocking) {}
bool overwrite;
bool isBlocking;
};
Even though it does not fulfill the trait that I assumed could be used due to the description (std::is_aggregate). Though there is definately some restriction, because the following type requires being passed by address:
template<typename Supplier>
class DynamicEnum final
{
public:
using Type = typename Supplier::Type;
static_assert(std::is_integral_v<Type>, "Type must be an integral value.");
constexpr DynamicEnum(void) noexcept :
m_value(0) {}
constexpr DynamicEnum(const DynamicEnum& en) noexcept :
m_value(en.m_value) {}
constexpr DynamicEnum(Type value) noexcept :
m_value(value) {}
template<typename OtherType>
constexpr DynamicEnum(const DynamicEnum<OtherType>& other) = delete;
private:
Type m_value;
};
Now the main difference is both the private member, as well as an existing copy-constructor, as well as some additional constructors.
So my second question: What trait can be used to determine whether a type can be passed in a register in x64? std::is_standard_layout seems not to be the trick, the only thing I can think of right now is std::is_trivially_copyable, at least based on my two types. However, since this is not at all what the documentation says, and since I don't want to have to guess a ton of corner cases myself, I'd appreciate if someone who knows what's expected for the calling convention to clear things up for me.
First of all, the calling convention can obviously differ between compilers/platforms. I assume you are interested only in the MSVC one for x64.
No, it is not correct and it is impossible to determine this property with standard C++ features.
For example,
std::is_standard_layoutdoesn't match the requirements that you are trying to combine.std::is_standard_layoutonly requires that all non-static data members have the same access specifier, not that it must bepublic.std::is_standard_layoutalso permits base classes as long as all non-static data members are directly in the same class in the hierarchy.Furthermore, you check whether the default constructor exists and is trivial, but the requirement states that no constructor must be user-defined. You only check one specific argument list. Also, it doesn't state anywhere that a default constructor must be present and usable at all.
You also check that the type is trivially-copyable, but that isn't actually what is required according to the quote. It says that no copy constructor and copy assignment must be user-defined. The requirements for trivially-copyable are quite different. They also check e.g. move assignment.
Replacing it with
std::is_trivially_copy_constructibleor similar won't help you either, because a type can have a trivial copy constructor but not be trivially copy-constructible (e.g. if the copy constructor is user-declared with parameter typeT&instead ofconst T&and explicitly-defaulted).Your checks will also fail if the tested members functions are
privateorprotected, although nothing requires them to bepublicin order to pass by register.These are only some problems I see. I am sure there are more.
While the requirements you quoted seem to only consider C types and are therefore incomplete, I am pretty sure that again there will be no trait in the C++ standard library that will be able to determine MSVC's ABI decision.
Since C++17 the C++ standard effectively limits what types compilers may pass in registers because of mandatory copy elision rules. The exceptions to the mandatory copy elision are specified in [class.temporary]/3. Effectively only types matching these requirements, i.e. trivial non-deleted destructor, only trivial eligible copy/move constructors and at least one eligible copy or move constructor, may be passed in registers.
But the concrete subset that will be passed in registers is up to the ABI. Unfortunately I am not aware of a complete specification for MSVC's C++ or C ABIs and I don't know them well enough to answer your questions more specifically.