Date: Thu, 7 Nov 2024 10:13:01 +0000
First a quick note on motivation:
Yesterday I was debugging a main program that loads a plugin at
runtime. The main program passed a vector by reference to the plugin,
the plugin populated the vector, and then the main program worked with
the vector contents. It wasn't working properly and I eventually
figured out that the plugin was compiled with LLVM clang++ while the
main program was compiled with GNU g++. The container type
"std::vector" isn't ABI-compatible between the two (or at least not
between the two compiler versions in question).
I thought it might be nice to be able to check this at runtime, something like:
namespace std {
template<typename T>
consteval size_t const *layout_of(void);
}
This function returns a pointer to a null-terminated sequence of
size_t's as follows:
{ alignment, size, alignment, size, alignment, size, alignment,
size, . . . , null terminator }
I'm thinking that the 'size' parameter would be '__datasizeof' rather
than 'sizeof'.
Using the following struct as an example:
struct Monkey {
int a;
char b;
char *p;
alignas(32) char c;
};
So then std::layout_of<Monkey> would come back with:
{ 4, 4, 1, 1, 8, 8, 32, 1 }
By the way if it were 'sizeof' instead of '__datasizeof', then it would be:
{ 4, 4, 1, 1, 8, 8, 32, 32 }
So then the main program, when it loads a plugin, could compare its
own vector to the vector in the plugin, something like:
#include <cstddef> // size_t
#include <cstdlib> // EXIT_FAILURE
#include <vector> // vector
using std::size_t;
extern size_t const *GetVectorLayoutFromPlugin(void); // defined
in the plugin
int main(void)
{
constexpr size_t const *domestic = std::layout_of<
std::vector<void*> >();
size_t const *const exotic = GetVectorLayoutFromPlugin();
if ( 0 != std::char_traits<size_t>::compare( domestic, exotic
) ) return EXIT_FAILURE;
}
Of course there are a few more things to consider such as:
(1) Should the sequence be prefixed with 1 byte to indicate the size
of "size_t" -- just in case it's 4 on one compiler and 8 on another?
(2) It's possible for two classes to have identical layout but to
still have differing and incompatible implementations.
(3) Should the pointer to the VTable have a special marking? I was
thinking that the 'alignment' parameter could be negative for the
VTable pointer, so if you had:
struct Donkey {
virtual ~Donkey(void) = default;
int a;
};
then it would be:
{ -8, 8, 4, 4 }
This would be useful in particular when dealing with the Microsoft
compiler, as the VTable pointer might not be the first item if you
have a polymorphic class inheriting from a non-polymorphic class, such
as the following:
struct Base {
int a;
};
struct Derived : Base {
virtual ~Derived(void) = default;
};
So then on the Microsoft compiler, std::layout_of<Derived>() would
come back with:
{ 4, 4, -8, 8 }
where as on every other C++ compiler in existence, it would be:
{ -8, 8, 4, 4 }
Yesterday I was debugging a main program that loads a plugin at
runtime. The main program passed a vector by reference to the plugin,
the plugin populated the vector, and then the main program worked with
the vector contents. It wasn't working properly and I eventually
figured out that the plugin was compiled with LLVM clang++ while the
main program was compiled with GNU g++. The container type
"std::vector" isn't ABI-compatible between the two (or at least not
between the two compiler versions in question).
I thought it might be nice to be able to check this at runtime, something like:
namespace std {
template<typename T>
consteval size_t const *layout_of(void);
}
This function returns a pointer to a null-terminated sequence of
size_t's as follows:
{ alignment, size, alignment, size, alignment, size, alignment,
size, . . . , null terminator }
I'm thinking that the 'size' parameter would be '__datasizeof' rather
than 'sizeof'.
Using the following struct as an example:
struct Monkey {
int a;
char b;
char *p;
alignas(32) char c;
};
So then std::layout_of<Monkey> would come back with:
{ 4, 4, 1, 1, 8, 8, 32, 1 }
By the way if it were 'sizeof' instead of '__datasizeof', then it would be:
{ 4, 4, 1, 1, 8, 8, 32, 32 }
So then the main program, when it loads a plugin, could compare its
own vector to the vector in the plugin, something like:
#include <cstddef> // size_t
#include <cstdlib> // EXIT_FAILURE
#include <vector> // vector
using std::size_t;
extern size_t const *GetVectorLayoutFromPlugin(void); // defined
in the plugin
int main(void)
{
constexpr size_t const *domestic = std::layout_of<
std::vector<void*> >();
size_t const *const exotic = GetVectorLayoutFromPlugin();
if ( 0 != std::char_traits<size_t>::compare( domestic, exotic
) ) return EXIT_FAILURE;
}
Of course there are a few more things to consider such as:
(1) Should the sequence be prefixed with 1 byte to indicate the size
of "size_t" -- just in case it's 4 on one compiler and 8 on another?
(2) It's possible for two classes to have identical layout but to
still have differing and incompatible implementations.
(3) Should the pointer to the VTable have a special marking? I was
thinking that the 'alignment' parameter could be negative for the
VTable pointer, so if you had:
struct Donkey {
virtual ~Donkey(void) = default;
int a;
};
then it would be:
{ -8, 8, 4, 4 }
This would be useful in particular when dealing with the Microsoft
compiler, as the VTable pointer might not be the first item if you
have a polymorphic class inheriting from a non-polymorphic class, such
as the following:
struct Base {
int a;
};
struct Derived : Base {
virtual ~Derived(void) = default;
};
So then on the Microsoft compiler, std::layout_of<Derived>() would
come back with:
{ 4, 4, -8, 8 }
where as on every other C++ compiler in existence, it would be:
{ -8, 8, 4, 4 }
Received on 2024-11-07 10:13:09