You made an interpretation of the proposal that has nothing to do with the OP’s proposal.
Please re-read it.
I might have skipped a few steps in the reasoning, I give that to you; however, I suggest we go through OP's mail together (see below) so you'll see how these are connected.
OP would like two things:
1. To be able to export-import, possibly in DLLs, a function under the same name as another function in an existing DLL, and to be able to write that function without having to care about signature.
2. To be able to jump to a function instead of calling it, where jump means as-if we replaced the call to interceptor (say, f_i(...))
in the caller with (f_i(...), f(...)) where f is the function we jump to, expect that arguments to f(...) are kept alive (and set by) f_i(...) call.
Now, 1. has little to do with C++ Standard, as it doesn't define DLLs (or even linking, btw.), it's an ABI-thing. In the code OP wrote, it's also Windows-specific, but that's not a necessary requirement to implement this. How a linker (if the implementation
uses a linker) names functions in a DLL (if the target platform uses DLLs) is ABI-specific, so my understanding is that OP either needs Itanium ABI changes, or - more likely - needs a simple tool that allows for mangling symbols manually in their DLL.
That's not our focus here, so I skipped that part in the comment.
Let's check what's on our table from point 1, by going through the code samples posted:
> On 26 Jul 2024, at 13:22, Frederick Virchanza Gotham via Std-Proposals <std-proposals@lists.isocpp.org> wrote:
> typedef int (*FuncPtr_FlushPipe)(void *pipe, int flags);
>
> template<typename... Params>
> decltype(auto) FlushPipe(Params&&... args)
> {
> WriteLog( "Function called 'FlushPipe'");
> auto const h = LoadLibraryA("monkey.dll");
> auto const pf = (FuncPtr_FlushPipe)GetProcAddress(h, "FlushPipe");
> return pf( static_cast<Params&&>(args)... );
> }
It's entirely possible to make this work via the new goto (see later how) if the linker exposes this function under the same name as the original DLL does (i.e., FlushPipe). Problem is, in real world, templates aren't necessarily reaching linker,
in fact, you need to ODR-use or instantiate the template to make sure it's compiled at all (e.g. into the interceptor DLL). Therefore, the code suggests that OP will request a 'base case' for template functions which is always instantiated. Hypothesis here
is, it's the case where all we do to arguments is perfect forwarding to tail call / jump, if anything at all.
(Technically, we could allow perfect forwarding and extending on the back.)
Let's check that hypothesis on code samples 2 and 3:
> typedef int (*FuncPtr_FlushPipe)(void *pipe, int flags);
>
> extern "C" int FlushPipe(void *const pipe, int const flags)
> {
> WriteLog( "Function called 'FlushPipe'");
> auto const h = LoadLibraryA("monkey.dll");
> auto const pf = (FuncPtr_FlushPipe)GetProcAddress(h, "FlushPipe");
> return pf( pipe, flags );
> }
This, as I read, is OP's way of telling that manual instantiation or ODR-use would have been required, therefore OP resorted to expanding the code here. Nothing's wrong with that, essentially it's still the same code.
> auto Func(auto) interceptor
> {
> WriteLog( "Function called 'FlushPipe'");
> auto const h = LoadLibraryA("monkey.dll");
> auto const pf = (void(*)(void))GetProcAddress(h, "FlushPipe");
> goto pf;
> }
This, as I read, is a proposed syntax for a template that's auto-instantiated for forwarding. It also shows an example of the proposed goto syntax (point 2), we can return to that later.
Keep in mind, that, from OP's perspective it's still the same thing as example 1, i.e., OP is using it in a situation where it's called with
exactly the same arguments as in example 1. Unspoken here, but we're talking about a monad defined by function arguments. Now, probably OP is not insisting on this particular syntax, but a solution to achieve this. In particular, we could keep the perfect
forwarding syntax, or any other syntax, as long as we allow perfect forwarding in tail calls and gotos (or, if we were to go wild, in any calls). This is nothing new: on a meta-level, you can refer to the particular instantiation of a template class by name
inside the class definition (i.e., in template<typename T> class A;, you don't need to write A<T>, you can write A). For goto, this is unambiguous; for tail call, you could use the classic forwarding syntax.
IMHO best were to keep the classic syntax:
auto Func(auto&& args...) /* interceptor - no notation needed */
{
WriteLog( "Function called 'FlushPipe'");
auto const h = LoadLibraryA("monkey.dll");
auto const pf = (void(*)(void))GetProcAddress(h, "FlushPipe");
////// use either of these:
// return pf(std::forward<decltype(args)>(args)...); // this calls pf first, then destructs any locals (tail call)
// goto pf(std::forward<decltype(args)>(args)...); // this destructs any locals, except function arguments, then calls pf
goto pf; // this is the same as the previous, shorthand syntax
}
As long as we mandate that the compilation unit should export this as a symbol regardless of arguments, I think it works for OP for point 1. And this can be made to work on stack-based implementations, for the stack effect of a call to Func is the same
as a call to pf; therefore, goto will work. Tail call version might or might not be made to work that simple, as it needs locals to be destructed, therefore a new stack frame, etc, but all these we don't have with the jump version.
For point 2, you can already see that goto pf; is a shorthand for goto pf(std::forward<decltype(args)>(args)...); and that it's
unambiguous. It simply means to:
- extend the lifetime of arguments (to pf call - in this case, it's the same as the original call) to where pf returns
- destruct any locals, restore exception handlers as-in caller
- jump to pf
The important thing here is, we don't need to know the arguments; similarly to that we don't need to have exact info of the entire monad inside a mapping, or we don't need to list all the member variables in a member function, or we don't need to list
template arguments to refer to the particular instantiation in the template class definition. We simply take it as 'context', and it works. Without additional costs, in fact, with further optimisation as these jumps unroll unnecessary parts of the stack.
As for the rest of the mail:
> (1) An exception must not propagate outside of an interceptor before
> the jump takes place. If this occurs then either (1.a) The terminate
> handler is called, or (1.b) undefined behaviour.
I don't think it's necessary, we might simply say that exception handlers of the caller of 'interceptor' (caller of Func) apply from the point where the jump takes place. That is expected to be the simplest to implement.
> (2) If control reaches the end of an interceptor function without a
> "goto" statement, then it's as though the interceptor ended with "goto
> std::abort".
I'd skip this, simply making it the same as-if control reaches the end of a non-void function not returning a value (which is UB in general, but many implementations simply allow it as uninitialized value if return type is POD).
So, all-in-all, I think it's doable (with some adjustments) and it brings the continuations, monads closer to C++.
Thanks for reading,
-lorro