How to effect a return of a value from the _calling_ function?

233 views Asked by At

I would like to be able to force a 'double-return', i.e. to have a function which forces a return from its calling function (yes, I know there isn't always a real calling function etc.) Obviously I expect to be able to do this by manipulating the stack, and I assume it's possible at least in some non-portable machine-language way. The question is whether this can be done relatively cleanly and portably.

To give a concrete piece of code to fill in, I want to write the function

void foo(int x) {
    /* magic */
}

so that the following function

int bar(int x) {
    foo(x);
    /* long computation here */
    return 0;
}

returns, say, 1; and the long computation is not performed. Assume that foo() can assume it is only ever called by a function with bar's signature, i.e. an int(int) (and thus specifically knows what its caller return type is).

Notes:

  • Please do not lecture me about how this is bad practice, I'm asking out of curiosity.
  • The calling function (in the example, bar()) must not be modified. It will not be aware of what the called function is up to. (Again in the example, only the /* magic */ bit can be modified).
  • If it helps, you may assume no inlining is taking place (an unrealistic assumption perhaps).
4

There are 4 answers

0
Steve Jessop On BEST ANSWER

The question is whether this can be done relatively cleanly and portably.

The answer is that it cannot.

Aside from all the non-portable details of how the call stack is implemented on different systems, suppose foo gets inlined into bar. Then (generally) it won't have its own stack frame. You can't cleanly or portably talk about reverse-engineering a "double" or "n-times" return because the actual call stack doesn't necessarily look like what you'd expect based on the calls made by the C or C++ abstract machine.

The information you need to hack this is probably (no guarantees) available with debug info. If a debugger is going to present the "logical" call stack to its user, including inlined calls, then there must be sufficient information available to locate the "two levels up" caller. Then you need to imitate the platform-specific function exit code to avoid breaking anything. That requires restoring anything that the intermediate function would normally restore, which might not be easy to figure out even with debug info, because the code to do it is in bar somewhere. But I suspect that since the debugger can show the state of that calling function, then at least in principle the debug info probably contains enough information to restore it. Then get back to that original caller's location (which might be achieved with an explicit jump, or by manipulating wherever it is your platform keeps its return address and doing a normal return). All of this is very dirty and very non-portable, hence my "no" answer.

I assume you already know that you could portably use exceptions or setjmp / longjmp. Either bar or the caller of bar (or both) would need to co-operate with that, and agree with foo how the "return value" is stored. So I assume that's not what you want. But if modifying the caller of bar is acceptable, you could do something like this. It's not pretty, but it just about works (in C++11, using exceptions). I'll leave it do you to figure out how do do it in C using setjmp / longjmp and with a fixed function signature instead of a template:

template <typename T, typename FUNC, typename ...ARGS>
T callstub(FUNC f, ARGS ...args) {
    try {
        return f(args...);
    }
    catch (EarlyReturnException<T> &e) {
        return e.value;
    }
}

void foo(int x) {
    // to return early
    throw EarlyReturnException<int>(1);
    // to return normally through `bar`
    return;
}

// bar is unchanged
int bar(int x) {
    foo(x);
    /* long computation here */
    return 0;
}

// caller of `bar` does this
int a = callstub<int>(bar, 0);

Finally, not a "bad-practice lecture" but a practical warning -- using any trick to return early does not in general go well with code written in C or written in C++ that doesn't expect an exception to leave foo. The reason is that bar might have allocated some resource, or put some structure into a state that violates its invariants before calling foo, with the intention of freeing that resource or restoring the invariant in the code following the call. So for general functions bar, if you skip code in bar then you might cause a memory leak or an invalid data state. The only way to avoid this in general, regardless of what is in bar, is to allow the rest of bar to run. Of course if bar is written in C++ with the expectation that foo might throw, then it will have used RAII for the cleanup code and it will run when you throw. longjmping over adestructor has undefined behavior, though, so you have to decide before you start whether you're dealing with C++ or with C.

0
Joseph Quinsey On

Modify your void function foo() to return a boolean yes/no, and then wrap it in a macro of the same name:

    #define foo(x) do {if (!foo(x)) return 1;} while (0)

The do .. while (0) is, as I'm sure you know, the standard swallow-the-semicolon trick.

Your may also need, in the header file where you declare foo(), to add extra parentheses, as in:

    extern bool (foo)(int);

This prevents the macro (if already defined) from being used. Ditto for the foo() implementation.

1
glglgl On

The only clean way is to modify your functions:

bool foo(int x) {
    if (skip) return true;
    return false;
}

int bar(int x) {
    if (foo(x)) return 1;
    /* long computation here */
    return 0;
}

also can be done with setjmp() / longjmp(), but you'd have to modify your caller as well, and then you can as well do it cleanly.

0
Netch On

There are two portable ways to do this but both require caller function assistance. For C, it's setjmp + longjmp. For C++, it's exceptions usage (try + catch + throw). Both are quite similar in implementation (in essence, some early exceptions implementation was based on setjmp). And, there is definitely no portable way to do this without caller function awareness...