2026-10-04
preliminary discussion for possible integration into IS ISO/IEC 9899:202y
| 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 |
CC BY, see https://creativecommons.org/licenses/by/4.0
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.
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.
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.
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
TypeNameContext can omit
the const-qualifiers and
restrict-qualifiers
if deemed necessaryGcc 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.
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.
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.
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.
Thanks to … for review and discussions.