Repository navigation
Conversation
In this patch, the type of `ContinuationHandler` changes from a function pointer type to a struct containing a pointer to the handler and a pointer to an internal thunk that invokes the handler with the correct data type. This is a backwards-incompatible ABI change, exposed to plugins via `TSCont`. Additionally, this patch removes the constraint that `Continuation` must be listed as the first base class in multiple inheritance. As far as I can tell, that constraint was never required by the semantics, but was likely added because of a false positive GCC warning. This patch _does not_ provide a way for the event system to pass a strongly typed argument. The handler object accepts a `void*` as before, but casts it to the correct type before invoking the handler function. The new design has some similarities to the standard C++ function `std::mem_fn`. The return type of `std::mem_fn` is unspecified, so we cannot use that type here. The design also borrows functional programming terminology: a `thunk` is a deferred computation. In this context, the thunk is the deferred handler invocation. The previous code contained two bugs which are addressed in this patch: 1) Calling a method through a different method type is is undefined behavior 2) Comparing method pointers through the wrong type is undefined behavior TLDR: this patch makes it possible to use `int f(int, ExactType)` as an event handler without undefined behavior.
|
Thanks for tackling this. Fixing the UB in handler dispatch and comparison is worthwhile. Two things: 1. The QUIC handlers still go through a mismatched member-pointer type These calls still cast to
SET_HANDLER((NetVConnHandler)&QUICNetVConnection::acceptEvent);2. An alternative: a per-handler trampoline You may have already considered this, but here's an alternative. Every in-tree using ContinuationHandler = int (*)(Continuation *, int, void *);
template <typename> struct handler_traits;
template <typename D, typename T> struct handler_traits<int (D::*)(int, T *)> {
using cls = D;
using data = T;
};
template <auto M>
int
trampoline(Continuation *c, int event, void *data)
{
using Tr = handler_traits<decltype(M)>;
return (static_cast<typename Tr::cls *>(c)->*M)(event, static_cast<typename Tr::data *>(data));
}
#define SET_HANDLER(_h) (this->handler = &trampoline<_h>)
// comparison: handler == &trampoline<&CacheVC::dead>What this gets us:
The trade-offs:
|
|
I ruled out the trampoline alternative because of the code folding optimization, which I did not understand at the time. But the implementation in this patch carries the same code folding risk. I think the trampoline version is both simpler and less wasteful with memory. I will try it. |
|
This PR adds a second dispatch function call to every event. It has a cost in both size and runtime dispatch(extra function call). I agree with Masaori that this could be done statically without either cost. I'm not convinced a change here is necessary, since the type of the second argument is also dependent on the event passed (some handlers will cast it to something else depending on the event anyway) and I don't think there is any combination of types that will cause the thunk template function to produce different code. The old code comments acknowledge the potential for UB if used outside of the intent, but if every use only passes a handler that is a member of a subclass of Continuation, its gonna be fine. |
|
@cmcfarlen The UB is not a latent risk caused by AI misuse. Dereferencing a function pointer cast to a different function pointer type is UB regardless. Every call to a Continuation handler is UB, even if the data argument has the correct effective type. I agree that this won't be observable in practice on the architectures we support, but I think it's better to write well defined code, or else write it in assembly. At the very least, it needs a comment that says "we know this isn't correct C++ but we are doing it anyway because of X reason". |
Note: this PR incorrectly uses the term "thunk". Credit to @cmcfarlen for catching this.
In this patch, the type of
ContinuationHandlerchanges from a function pointer type to a struct containing a pointer to the handler and a pointer to a function that invokes the handler with the correct data type. This is a backwards-incompatible ABI change, exposed to plugins viaTSCont.Additionally, this patch removes the constraint that
Continuationmust be listed as the first base class in multiple inheritance. As far as I can tell, that constraint was never required by the semantics, but was likely added because of a false positive GCC warning.This patch does not provide a way for the event system to pass a strongly typed argument. The handler object accepts a
void*as before, but casts it to the correct type before invoking the handler function.The new design has some similarities to the standard C++ function
std::mem_fn. The return type ofstd::mem_fnis unspecified, so we cannot use that type here.The previous code contained two bugs which are addressed in this patch:
Calling a method through a different method type is is undefined behavior
Comparing method pointers through the wrong type is undefined behavior
TLDR: this patch makes it possible to use
int f(int, ExactType)as an event handler without undefined behavior.