Out-of-band transfer into and out of function calls

Jens Gustedt, INRIA and ICube, France

2026-10-04

target

preliminary discussion for possible integration into IS ISO/IEC 9899:202y

document history

document number date comment
n3976 202610 Original proposal
document number title
n3930 Interfaces with contracts alongside Annex K
N2787 Wide Function Pointer Types for Pairing Code and Data

license

CC BY, see https://creativecommons.org/licenses/by/4.0

1 Motivation

For the implementation of many programming languages other than C the need arises to provide context for function calls, in particular for any forms of closures that gain access to values and variables of the context in which they are defined. Most (all?) modern architectures provide tools in their ABI to send such information along side of the normal function arguments from a caller to a callee. Generally, a small number of scratch hardware registers are reserved for this, that is hardware registers that are neither caller nor callee saved; these are then written closely before a call and read closely after entering into the function to transfer the address of a context.

We think that this common practice should be interfaced by the C standard library.

On the side of function return, the need of a consistent convention for returning control (mostly error) information is even more pressing. Currently the C library has a whole variety of methods to return errors, several in-band conventions on return values (negative value, special values, zero, non-zero, struct return member), thread local state (errno) or per argument state (accessed e.g with the feof and ferror functions). This current state of things is tedious to learn, error prone, and distracts focus from the primary objective of a given function.

With this paper we propose that the same mechanism as above (for context propagation) is used as a backchannel to send supplementary information back from a function call to its caller.

2 The proposed interface

2.1 Low level

In the following we assume that the implementation has types [u]intptr_t. (The precise wording then uses _BitInt(sizeof(void*)*CHAR_BIT) and its unsigned variant to wiggle around the lack of such types, if necessary.) We propose a set of four low-level macro interfaces with synopsis as follows

#include <stdlib.h>
// send and receive unsigned integer from caller to callee ...
void stdc_caller_sendu(uintptr_t);
uintptr_t stdc_callee_recvu(void);
// ... and back
void stdc_callee_sendu(uintptr_t);
uintptr_t stdc_caller_recvu(void);

The idea is that a caller sends into and receives from the call by using the macros with *caller* in their name, and the callee those with *callee*. This distinction between caller and callee interface is made because on some architectures the scratch register that is used has a different name when seen from the caller or the callee. Thus hardware instructions to access such a register must be different for the two contexts.

We use uintptr_t as a base type of this communication channel, because we want it to be wide enough to pass pointers along, in particular from caller to callee, such that a callee may access a whole calling context that is provided through an address. This is captured by a second set of interfaces that uses void const* instead of uintptr_t:

// send and receive pointer to const-qualified state from caller to
// callee ...
void stdc_caller_sendp(void const*);
void const* stdc_callee_recvp(void);
// ... and back
void stdc_callee_sendp(void const*);
void const* stdc_caller_recvp(void);

These macros can then simply constructed from the previous ones by casting a pointer value to and from uintptr_t. Note that these interfaces use a const-qualified target type. This is to ensure that context pointers may point to statically allocated unmutable objects (such as string literals) without violating a const-contract. Also using const-qualification eases design and verification of functions with [[reproducible]] attributes that use the out-of-band communication as described here.

Traditional C error handling often uses signed types to express error state. Therefore we also provide an interface for signed data using intptr_t.

// send and receive unsigned integer from caller to callee ...
void stdc_caller_sends(intptr_t);
intptr_t stdc_callee_recvs(void);
// ... and back
void stdc_callee_sends(intptr_t);
intptr_t stdc_caller_recvs(void);

A subtle difficulty when implementing this on top of uintptr_t is that conversion between intptr_t and uintptr_t is only well defined one way (from signed to unsigned). We take care of this by specifying explicitly that uintptr_t to intptr_t conversion here is achieved by reinterpreting the bit representation.

For all these macros it is important that they are invoked just before or after a function call (for the caller side) or at the very beginning and end of the function call (for the callee side). This is because by their nature values in scratch registers are transient and might be overwritten by other code in the function.

Also implicitly the proposed wording assumes that there would be a clear notion what “a last evaluation before a function returns” would be. This is a concept that probably needs further development. For a first and simplified approach let us assume that the _Defer feature has been adopted and that the “last evaluation before a function returns” is the last evaluation in the first top-level _Defer clause in the function body. A typical pattern of a function that uses context and sends a control word back would look as follows:

int f(... parameters ...) {
    // ...................... first evalution in function
    myType const*volatile myContext = stdc_callee_recvp();
    ...
    // first _Defer in the function
    _Defer {
        // compute some control value CVAL to be sent back
        ...
        // last evalution in function
        stdc_callee_sends(CVAL);
    }
    ...
    return something_complicated.
}

A pattern as above would commonly interpret the caller provided data as a pointer value and the callee provided value as an integer. The choices for the types are made that this is possible and the wording takes care that mixing these different interfaces never, by itself, leads to undefined behavior.

2.2 Higher level

The low-level interfaces that insist on an certain position of a macro invocation within the enclosing function are a bit fragile and not very user friendly. Therefore we also provide two pre-defined identifiers (__context__ and __control__) and a set of higher-level macros for function calls, for example

#define stdc_call_with_context(CNTXT, FUNC, ...)

Here, an invocation of stdc_call_with_context(Ecntxt, Efunc, Ea, Eb, ..., Ex) can be seen as a macro interface to the existing gcc extension __builtin_call_with_static_chain, see below. It is equivalent to a comma expression (stdc_caller_sendu((U)Ecntxt), Efunc(Ea, Eb, ..., Ex)) that has type typeof(Efunc(Ea, Eb, ..., Ex)), only that the function pointer expression Efunc and argument expressions Ea, Eb, ..., Ex are evaluated and stored into temporaries func, a, b, ..., x before the invocation of stdc_caller_sendu((U)Ecntxt). Only thereafter the function call itself func(a, b, ..., x) is evaluated.

The pre-defined identifiers __context__ and __control__ are as if defined as

void const*restrict const __context__ = stdc_callee_recvp();
intptr_t __control__ = {};

even before the parameters of the corresponding function. Thus the call context is guaranteed to be present in __context__ for the whole function call. The value of __control__ is set (or not) by the user anywhere during the function call, the last value is then send back as if by stdc_callee_sends(__control__) immediately before the call ends.

Note that these predefined variables are not the out-of-bounds channel themselves, they only serve as backup for the information that is transmitted, there. Such an interface is much easier to handle for applications and avoids the fragility when using the low-level macros directly.

For examples of the usage of the interfaces we refer to the examples in the proposed wording.

2.3 A possible extension

Our pre-processor platform eĿlipsis implements the interfaces that are proposed here. It also has a possibility to add the context and control information to a function prototype. This is done in a similar way as contracts by adding annotations of the form

_Context()
_Context(TypeNameContext)
_Control()
_Control(TypeNameControl)

after the parameter list of a function declaration. Here, forms without TypeName result in __context__ and __control__ with types as described above. If a TypeName is given they have the corresponding type as in

typeof(TypeNameContext) const __context__ = stdc_callee_recvp();
typeof(TypeNameControl) __control__ = {};

TypeNameContext has to be a pointer type (usually with const-qualified target), TypeNameControl has to be an integer type that is at most as wide as uintptr_t.

This extension

3 State of the art

Gcc implements passing of context to specialized functions since a long time. The only exposure to this feature to user space is the builtin __builtin_call_with_static_chain.

__builtin_call_with_static_chain (call_exp, pointer_exp)

Here call_exp is a function call expression that indicates the instance of the evaluation of a function call that is to receive a context, e.g a call atan2_with_context(a, b) as for the function defined in Example 2. pointer_exp is a void* pointer expression that refers to such a context, similar to the &contex pointer in the example. With our proposed interface, the functionality of __builtin_call_with_static_chain is very close to stdc_return_with_control. On this platform we could just have the macro definition.

#define stdc_call_with_context(CNTRL, FUNC, ...)              \
    __builtin_call_with_static_chain(FUNC(__VA_ARGS__), CNTRL)

So for the example the expansion would be

__builtin_call_with_static_chain(atan2_with_context(a, b), &context)

Gcc does not provide direct builtins to access such a context from within a C function. Their main application of this feature is to call closures written in other programming languages, for example Go, or in their own C extension, nested functions. How these then would gain access to the context is left to the implementation of the target language and not exposed as a C function.

In the gcc extension, a nested function is a local function LF that is declared within another function F. Recently in version 17, gcc gained two builtins (written by Martin Uecker) that can be used to split LF into a static function pointer (so a function pointer value that is determined at compile time) and dynamic data pointer that provides the context.

function_pointer_t LF_static = __builtin_call_code_address(LF);
void* definition_context = __builtin_call_static_chain(LF);

These two pointers can then be used to call LF transparently by using the __builtin_call_with_static_chain as shown above.

This example from gcc shows that they have an existing and proven mechanism on all of their supported architectures that allows to pass context information out-of-band into the call of a “static” function.

3.1 A partial implementation of an extension for gcc

As said, gcc already has a builtin that implements stdc_call_with_context. To be able to capture the transferred pointer value we have extended gcc to implement the predefined variable __context__. With the code that is already present in the gcc sources, such an extension was direct and without any challenges so far. With this extension, it should be possible to implement language extensions such as closures as macros or similar replacement techniques.

First test show that this extension is functional. Also, because the __builtin_call_with_static_chain extension is present almost since the beginning of gcc and is well integrated for all architectures that gcc supports, a combination with __context__ optimizes well, in particular in places where such a call is inlined.

3.2 Interfaces for existing ABI

As we already observed in N2787, from all 51 architectures for which support can be found in the GCC source tree, all seem to support passing a static chain pointer, that is an out-of-band pointer argument. 48 of them always use a hardware register (internally for the most called STATIC_CHAIN_REGNUM for the caller and possibly STATIC_CHAIN_INCOMING_REGNUM for the callee, if needed in addition) and three may use memory or choose a hardware register or memory-object depending on the specific circumstances.

For existing ABI, implementations of the low-level macros using the common assembler extension could look as simple as the following (for the x86_64 platform)

#define stdc_caller_sendp(VALUE)                               \
  __asm__ volatile ("movq %q0, %r10" : : "rm" (VALUE) : "%r10")

That is, an assembler instruction that moves the hardware register or memory argument VALUE into the hardware register %r10, which is the scratch register on that architecture. On this architecture the scratch register is the same for caller and callee, and so all of the low-level macros look basically the same. Similar solutions should exist for other common architectures.

Nevertheless, to cover a large variety of architectures it is probably easier to introduce a new set of builtins into compilers that already support gcc’s static chain extension.

4 Proposed wording

7.25.x Out-of-band operations

7.25.x.1 General

1 The out-of-band operation macros provide a communication channel of STDC_OOB_WIDTH bits between a function call instance and all function calls that are evaluated by it; here, if the implementation provides uintptr_t the macro STDC_OOB_WIDTH expands to the same value as UINTPTR_WIDTH and otherwise it expands to the value sizeof(void*)*CHAR_BIT.

2 In this subclause, if the implementation provides the types uintptr_t and intptr_t, the types U and S are these types. Then, when a reinterpretation between a value of void const* and one of the integer types is needed, the usual conversion takes place.

3 Otherwise the types U and S are the unsigned and signed versions of _BitInt(STDC_OOB_WIDTH), respectively. Then, when needed the bit representation of a void const* pointer is reinterpreted as the value representation of an integer of type U or S.

4 Similarly, the reinterpretation of an value of type U as type S and vice versa reuses the same value bits with the other type.

7.25.x.2 Low-level out-of-band operation macros

Synopsis

#include <stdlib.h>

void stdc_caller_sendu(U context);
U stdc_callee_recvu(void);
void stdc_callee_sendu(U control);
U stdc_caller_recvu(void);

void stdc_caller_sends(S context);
S stdc_callee_recvs(void);
void stdc_callee_sends(S control);
S stdc_caller_recvs(void);

void stdc_caller_sendp(void const* context);
void const* stdc_callee_recvp(void);
void stdc_callee_sendp(void const* control);
void const* stdc_caller_recvp(void);

Description

2 The send operations are lock-free atomic stores with release memory ordering and the recv operations are lock-free atomic loads with acquire memory ordering. All explicit invocations of caller macros within a function call instance FCI and all explicit invocations of callee macros used by direct callees of FCI access the same object of type _Atomic(U) volatile.Foot) Different threads do not access the same object; it is unspecified if a given thread uses one or several objects for different function call instances.

Foot) The type of the object has a volatile qualification to indicate that the value of the object changes in ways that are not predictable. This provides the implementation with the opportunity to repurpose the object during the same function call outside the use as indicated in this subclause.

3 In the following, an evaluation E1 is said to be sequenced closely before another evaluation E2, if E1 is sequenced before E2, if E1 is an atomic store operation or if E2 is an atomic load operation (or both), and if every other evaluation that is sequenced between E1 and E2 is an lvalue conversion or a simple assignment of (or into) an lvalue that does not have atomic type. An implementation may require that not more than an implementation-defined number LCSA of such lvalue conversions and simple assignments are sequenced between E1 and E2. If such a limit LCSA is imposed, it shall be at least 3.

4 With X one of the suffixes u, s or p, an evaluation of stdc_callee_recvX() that is sequenced first within a function body of a function F is said to provide a call context for F. If an invocation of stdc_caller_sendX(ARG) for some value ARG is sequenced closely before a call to F, the corresponding invocation of stdc_callee_recvX() results in the value ARG.FTN) Similarly, an evaluation of stdc_callee_sendX(ARG) that is sequenced closely before the return from a function F is said to return a control word from F. If an evaluation of stdc_caller_recvX() is sequenced closely after a call to F that evaluated stdc_callee_sendX(ARG) before the return, the result is ARG.

FTN) If prior to a call of F no context is send, the received value is unspecified.

5 If macros with different suffixes are combined, but otherwise used as indicated the source value is reinterpreted within the target type as described.

6 After any recv operation the value of the underlying object becomes unspecified. Evaluations of recv operations that are not sequenced closely to a function call as described previously in this clause result in the value that happens to be stored in the corresponding object and are otherwise unspecified; evaluations of stdc_caller_sendX(ARG) or stdc_callee_sendX(ARG) that are not sequenced closely to function calls as previously described in this clause have type void and have no visible effect on the program state other than setting the value of the underlying object.

7 NOTE 1 The out-of-band operations access objects in the execution state that are otherwise not accessible. In general a function in which these operations are used is not reproducible (see 6.7.12.8.1) because after the usage these objects are left in an unspecified state. Nevertheless, when storing values that have been probed previously this state is restored.

auto volatile backup   = stdc_caller_recvu();
auto volatile backdown = stdc_callee_recvu();
...
/* use out-of-band operations */
...
stdc_callee_sendu(backdown);
stdc_caller_sendu(backdup);

Here, the caller and callee value are probed and restored in reverse order. This is to ensure that the unique original value is restored, if they refer to the same object.

8 NOTE 2 An implementation that has a low implementation-defined limit on the number of lvalue conversions and simple assignments for two evaluations to be considered closely sequenced, a combination of calls

    stdc_caller_sendp(cntxt);
    double result = func(a, b, c, d);

may already exceed that value and thus not provide the context to the call of func. Using the macro stdc_call_with_context (7.25.x.4) as in the following does not have that limitation.

    double result =
        stdc_call_with_context(
            cntxt,
            func,
            a, b, c, d);

9 NOTE 3 Other than the names of these macros indicate, sending into and out-of a function call are not symmetric. In particular, if a function call expects a context and is called without sending such a context as described, the information that is retrieved is in general not valid and may lead to undefined behavior. On the other hand, in many cases a control word that is send back by the callee but that is then not inspected by the caller does not impact the execution more than by the loss of the encoded information.

10 Recommended practice It is recommended that implementations provide macros that use as few resources as possible. In particular it is recommended to limit the overhead of these macros to only the use of some hardware registers and instructions. If such hardware registers are not available, it is recommended that implementations provide the functionality by using a single object of type _Atomic(U) volatile with thread storage duration (if threads are provided) or with static storage duration (if threads are not provided).

11 EXAMPLE 1 Consider the following code

double atan2_with_errors(double a, double b) {
    // backup error state in errno0 and excepts0
    int errno0 = 0;
    int excepts0 = 0;
    if (math_errhandling & MATH_ERRNO) {
        errno0 = errno;
        if (errno0) errno = 0;
    }
    if (math_errhandling & MATH_ERREXCEPT) {
        excepts0 = fetestexcept(FE_INVALID | FE_DIVBYZERO));
        if (excepts0) feclearexcept(FE_INVALID | FE_DIVBYZERO);
    }
    _Defer {
        // query error state, store in report_back and restore old value
        typeof(stdc_caller_recvs()) report_back = 0;
        if (math_errhandling & MATH_ERRNO) {
            report_back = errno;
            if (report_back != errno0)
                errno = errno0;
        }
        if (math_errhandling & MATH_ERREXCEPT) {
            if (int excepts1 = fetestexcept(FE_INVALID | FE_DIVBYZERO)) {
                report_back = (excepts1 & FE_INVALID) ? EDOM : ERANGE;
                if (excepts1 != excepts0)
                    feraiseexcept(excepts0);
            }
        }
        stdc_callee_sends(report_back);
    }
    return atan2(a, b);
}

void f() {
    ...
    double atan_val = atan2_with_errors(aval, bval);
    switch (stdc_caller_recvs()) {
        case 0:
            /* all is fine */
            break;
        case ERANGE:
            /* something */
            break;
        case EDOM:
            /* something */
            break;
        default:
            /* something unpredicted happened */
            break;
    }
    assert(!errno);
    ...
}

The function atan2_with_errors returns math errors as a control word. According to the value of math_errhandling, error information (from errno or the floating point state) is intercepted and that value is sent back to the caller as a signed control word. The error handling code is placed in a _Defer statement to ensure that the invocation of stdc_callee_sends is the last evaluation in the call. Before that, the C library error states are restored, such that a call has no other visible side effect than to write a value into the out-of-band storage.

7.25.x.3 Pre-defined objects for context pointer and control word

Synopsis

void const*restrict const __context__ = stdc_callee_recvp();
S __control__ = {};

2 The pre-defined identifiers __context__ and __control__ are as if defined in the synopsis as objects of automatic storage duration. They have a lifetime and scope of visibility as if declared as additional parameters of the containing function at the beginning of the parameter list.

3 The underlying object of __context__ is initialized once immediately after execution enters a function call and as if by an invocation of the macro stdc_callee_recvp().

4 The underlying object of __control__ is default initialized and its value is sent immediately before a function call ends as if by an invocation of the macro stdc_callee_sends(__control__).

5 EXAMPLE 1 The function atan2_with_context receives a call context that is interpreted, if non-null, as a pointer to a factor that is applied to the arguments before atan2 is called.

double atan2_with_context(double a, double b) {
    if (__context__) {
        double factor = *(double const*)__context__;
        return atan2(factor * a, factor * b);
    } else {
        return atan2(a, b);
    }
}

6 EXAMPLE 2 Without _Defer and using __control__ instead, the function atan2_with_errors can be written in the following form

double atan2_with_errors(double a, double b) {
    // backup error state in errno0 and excepts0
    int errno0 = 0;
    int excepts0 = 0;
    if (math_errhandling & MATH_ERRNO) {
        errno0 = errno;
        if (errno0) errno = 0;
    }
    if (math_errhandling & MATH_ERREXCEPT) {
        excepts0 = fetestexcept(FE_INVALID | FE_DIVBYZERO));
        if (excepts0) feclearexcept(FE_INVALID | FE_DIVBYZERO);
    }
    double ret = atan2(a, b);
    // query error state, store in __control__ and restore old value
    if (math_errhandling & MATH_ERRNO) {
        __control__ = errno;
        if (__control__ != errno0)
            errno = errno0;
    }
    if (math_errhandling & MATH_ERREXCEPT) {
        if (int excepts1 = fetestexcept(FE_INVALID | FE_DIVBYZERO)) {
            __control__ = (excepts1 & FE_INVALID) ? EDOM : ERANGE;
            if (excepts1 != excepts0)
                feraiseexcept(excepts0);
        }
    }
    return ret;
}

7 EXAMPLE 3 The following use of stdc_callee_sends does not provide a control word for the return from a function call, because the evaluation of the return expression h(c) is sequenced between the evaluation of stdc_callee_sends and the return from the function.

    cntrl = 78;
    ...
    stdc_callee_sends(cntrl);
    return h(c);

Instead of a the explicit use of stdc_callee_sends, replacing the user defined object cntrl by the predefined object __control__ guarantees that h(c) is evaluated before the (now implicit) evaluation of stdc_callee_sends().

    __control__ = 78;
    ...
    return h(c);

7.25.x.4 High-level out-of-band operations for function calls

Synopsis

#include <stdlib.h>

#define stdc_call_with_context(CNTXT, FUNC, ...)
#define stdc_call_with_control(LVALUE, FUNC, ...)
#define stdc_call_with_context_and_control(CNTXT, LVALUE, FUNC, ...)

Description

2 The macro stdc_call_with_context combines the functionality of a stdc_caller_sendp macro with a function call to FUNC. Here, FUNC evaluates to a function designator or function pointer to a function

T f(void);

if there are no additional arguments, and otherwise

T f(Targ1, , TargN);

where Targ1, …, TargN are types that have implicit conversions from the types of the argument arg1, …, argN of the invocation. A call to stdc_call_with_context is then as-if a comma expression

(stdc_caller_sendp(CNTXT), FUNC())

or

(stdc_caller_sendp(CNTXT), FUNC(arg1, …, argN))

is evaluated, only that the context expression, function designator and other argument expressions are evaluated prior to the stdc_call_with_context operation which then is considered as a single evaluation.FOO)

FOO) Thus the implementation-defined limit of a maximal separation between evaluations with lvalue accesses does not apply to this operation.

3 For stdc_call_with_control, LVALUE provides an lvalue into which the control word sent by the callee is stored as if by assignment. The function designator and other argument expressions are evaluated prior to the stdc_call_with_control operation which then is considered as a single evaluation with the result value of the function call.

4 The operation stdc_call_with_context_and_control sends a context CNTXT to a function call as described above and provides an lvalue LVALUE into which the control word sent by the callee is stored as if by assignment. The context expression, the function designator and other argument expressions are evaluated prior to the stdc_call_with_context_and_control operation which then is considered as a single evaluation with the result value of the function call.

5 EXAMPLE 1 The following use of stdc_caller_sendp does not provide a context for the call to atan2_with_context.

    stdc_caller_sendp(cntxt);
    double result = atan2_with_context(f(a), g(b));

This is because the evaluations of the argument expressions f(a) and g(b) are sequenced before atan2_with_context is effectively called. In contrast, the use of stdc_call_with_context as in

    double result =
        stdc_call_with_context(
            cntxt,
            atan2_with_context,
            f(a),
            g(b));

warrants that all argument evaluations are sequenced before the underlying calls to stdc_caller_sendp and atan2_with_context.

6 EXAMPLE 2 To successfully receive a control word from a function call, the following code is in general not sufficient

double result = 2.0*atan2_with_errors(a, b);
S my_control = stdc_caller_recvs();

This is because the evaluation of the multiplication is sequenced between the two calls. In contrast to that in

S my_control;
double result = 2.0*stdc_call_with_control(my_control, atan2_with_errors, a, b);

no evaluation and no load or store is sequenced between the call of atan2_with_errors and the assignment of the control word into my_control.

Acknowledgments

Thanks to … for review and discussions.