On Friday, 20 May 2022 17:51:58 PDT Thiago Macieira via Std-Proposals wrote:
> I don't think that's sufficient. The change needs to be opt-in, not opt-out.
> Otherwise, we're going to have silent breakages because of some libraries
> recompiled and others not.
There may be a way not to have a break at all, but it comes at a cost. This
discussion probably needs to go to the cxx-abi group . There are also
problems I haven't seen yet and there's one that I have and don't have a
Let's say a library currently has a function:
void func(Type t1);
That gets mangled as _Z4func4Type. If Type is not trivially destructible, then
it's up to the caller today to call Type::~Type on the t1 object, but that
makes it impossible for func() to relocate from it. This is the central point
of the discussion.
So what if we split the difference?
For every call site where that function is called, instead of:
leaq 15(%rsp), %rdi ; reload saved address of t1
The compiler emits instead:
This "Tr" marker indicates that the function will take charge of destroying
any parameters before returning. When compiler the call site, the compiler
emits this function as a weak thunk, which does exactly what the code used to
do until now. As a weak symbol, it will only be emitted into the binary if it
doesn't already exist elsewhere as a non-weak one. Even if it is emitted into
the binary, at runtime if the non-weak version is present, it won't get used.
If the library providing func() hasn't been recompiled, then it doesn't have
the new symbol, meaning the thunk gets used. But since it hasn't been
recompiled, that means it can't be relocating from the any of its parameters,
so the thunk will be doing its job correctly. But this is the extra cost: for
every out-of-line function that takes any non-trivial parameter by value,
there's an extra stack frame between caller and callee. Total code size is
roughly the same, because the amount of work is the same, including that of
stack unwinding due to an exception.
If the library *has* been recompiled, then the compiler may emit both symbols.
If the function is not relocating from any of its parameters, the new symbol
is optional. Whether the compiler emits two completely separate function
bodies (one that destroys the parameters and one that doesn't, like it does
for constructors and destructors, see ) or merges the two in some way is an
implementation question. That's probably something the optimiser should
decide. Either way, it makes no difference in behaviour.
If the library has been recompiled and the function is relocating any of its
parameters, then obviously the old function symbol is unusable. The ABI folks
should find a way so that the symbol remains in existence but any attempt to
directly call it results in linker error.
There is an alternative to making it unusable; per Sébastien, as long as the parameter is movable, a second function body can be emitted that uses move to relocate. But perhaps this is too much of a behavioral change to have dependent on how a function is called.
It needs to retain the old symbol because of function pointers. Code that has
been recompiled and code that hasn't must still agree that the address of that
function is the same, however the psABI materialises pointers. But that brings
me to the problem I haven't solved yet: just how does one make an indirect
call through a function pointer?
It's a great mechanism, but I think that function pointers are something of a sticking point; since you don't know who will be invoking a function pointer, those will always have to use the old ABI, which means this is a penalty even for code compiled entirely to the new ABI (unless the compiler provides a flag to disable it, for use in standalone builds). And also you won't be able to form a function pointer to a function that performs relocation on its parameters, unless you emit a version (as above) using move constructor to relocate.
Still, it's good that we've been able to come up with two solutions to the ABI problem; this gives me confidence that it shouldn't be a blocker.