NXX26
_Any_func - A Universal Function Pointer Storage Type

Draft Proposal,

Previous Revisions:
None
Authors:
Paper Source:
GitHub
Issue Tracking:
GitHub
Project:
ISO/IEC 9899 Programming Languages — C, ISO/IEC JTC1/SC22/WG14
Proposal Category:
Change Request, Feature Request
Target:
C2y

Abstract

1. Revision History

1.1. Revision 0 - May 6th, 2025

2. Introduction & Motivation

The majority of the motivation is the same as it was 30 years ago, 20 years, and 10 years ago. In [n2230], Martin Sebor captured the core of the motivation:

... A void pointer is not convertible to a function pointer, and conversely, function pointers aren’t convertible to void. It is conceivable that an implementation may (and some, in fact, do) define function pointers to have a different size or representation than void, and such conversions could result in the corruption of the opriginal value.

However, C does not specify an analogous generic function pointer. Unlike object types, a pointer to a function of one type can be converted to a pointer to a function of any other type and back with no change. This is a useful guarantee that makes function pointers safely interchangeable (except to call a function, of course). At the same time, the absence of a generic function pointer analogous to void* has led to a proliferation of workarounds in applications, libraries, and implementations alike. ...

Popular distributions, operating systems, and platforms such as POSIX, Windows/NT, and many other platforms require that a data pointer (void*) is the same size as any function pointer (R(*)(Args...)). But, for many other platforms, function pointers are not the same size as data pointers altogether, either because there are multiple data segments and the data pointers are larger or -- less common but still present -- because function pointers are larger than data pointers. Rather than try to force void* to change to patch this hole (which would be a very fundamental ABI break on many platforms), this proposal focuses instead on following in [n2230]'s footsteps and proposing a (core language) type to satisfy this need properly.

3. Design

The goal of this paper is to provide a single type, _Any_func, from the core language. It’s a core language type that is incomplete by itself, much like void. Additionally, a pointer to _Any_func has the requirement that it is large enough to hold any function pointer type, and is implicitly convertible from and to any such pointer type, much like its void* counterpart. It is modeled directly after void* and modifies the same places in the wording.

3.1. Is It Callable?

It is NOT callable.

KnR functions had a serious flaw where despite being usable as "omni" function pointer types (ignoring the issue of return values and potential mismatches there): KnR functions had a different ABI than ... functions and were not required to have the same ABI as other function types. For example, a function int func(int arg0, double arg1); was not required to have the exact same calling convention, register allocation, and other important binary details as a KnR function int func2();. Similarly, a function int func3(...); or int func4(int arg0, ...); was not required to have the same ABI details as either func or func2, even under the circumstances that they were all called like so:

int a = 0;
double b = 1;
func(a, b);
func2(a, b);
func3(a, b);
func4(a, b);

Storing func in a KnR function pointer int(*)() and then calling it as such was not required to work in any version of C (it’s explicitly undefined behavior in every version of C, see "6.3.3.3 Pointers" in the latest working draft and similar paragraphs in C23, C17, and further back). It just so happens that it did happen to work (though technically can and could fail in invisible-to-the-Abstract-Machine ways) for the vast majority of existing C code, due to how most ABIs tend to be designed. (There were ABI issues between func2 and func3, in particular when people would define time_t time() as a KnR function on some platforms while competing with time_t time(time_t* arg) and similar definitions on various extern-and-exported-interface libraries.)

3.2. Library Modifications

The library needs further modification to allow for passing this new pointer type in, since %p by itself is specified to handle just void*. Since function pointer types and void* may not be the same size, a new entry is required to not taint the ABI of these functions, particularly of fprintf and fscanf.

There were a bunch of potential candidates but a lot of them had problems both because of existing uses in implementations or down-stream, or they were novel new modifiers. For example, %lp is already being used by CHERI, while %q is already taken by typical POSIX shell-based printf for quotable strings. To further disadvantage %q, it -- despite being a logical new letter next to %p -- is taken by BSD and still has some existing uses, even though the type it’s used for (quad_t) has long since been aliased to a "proper" type by now. Trying to sneak in %Lp is awkward thanks to implementation already moving in on %lp, especially if printing address spaces as part of pointers because a thing where length modifiers and precision get used to represent it. %Ip seems nice for the mnemonic "instruction pointer", but i is already a normal conversion specifier and I is novel in this space as a modifier and a not a standalone conversion specifier.

Thusly, we settled for %jp in this proposal. It can be read mnemonically as "jump pointer", which is reminiscent of assembly jmp and for assembly junkies gives the feeling of both jmp and call. j is also, already, a length modifier and has no known uses in the wild in combination with %p. It is the best we can do given the Wild Wild West of specifiers and modifiers in the C-and-derivative ecosystem, at this point.

3.3. ...ptr In the Name or a Pointer to Incomplete Type?

The goal of this design was to feel as close to void* as possible, since that is what this type represents. This means that we wanted to have the * present to have it look and feel just like a regular pointer type under regular pointer rules, rather than having it be part of the name. Since this is a core language type and a not a library definition, we preferred to define it closer to void than anything else. It also helps in putting this type into the specification, as all we have to do is mirror exactly the specification style and places that "A pointer to void" and "void*" exist in the current standard. This eases integration and is -- after first explanation -- immediately understandable to those who have been using void* as the omni-object pointer.

3.4. Giving void* Additional Powers

Furthermore, as this is defined in the core language, we can also provide a core language predefined macro for whether or not void* -> some_function_type* and vice-versa is an allowable, legal translation. This also allows us to put wording in the core language that blesses such an (implicit) conversion, given that it’s highly prevalent on a wide variety of architectures. It is often seen as a "gotcha" that this conversion doesn’t work, and users have been asking for this change for a very long time. We obviously do not want to make things worse for embedded platforms or make them non-conforming; they are an important bedrock of C.

Implementations which define the conditionally-supported environment macro __STDC_IMPLICIT_ANY_FUNC_PTR_TO_VOID_PTR__ enables the void* 🔄 some_function_type* 🔄 _Any_func* chain of changes. This would simply not be defined for implementations that could not handle it (e.g. 16-bit Texas Instrument DSPs or 8-bit multi-address-space chips that have 21 bit function pointers but 16 bit memory addresses). Most modern implementations -- Windows/NT since forever and POSIX since the 90s with XSI extensions (and just simply required by POSIX 2018) -- would have this defined, and users could #error inside of an #if check if their code happens to rely on this property.

3.5. Naming

The name chosen here is the one that had the no clashes in both its reserved form _Foo_bar and it’s header form foo_bar. The following other names were considered and searched for in public (and some private) code repositories for their prevalence as possible name candidates:

Many of these had issues with existing standard library type names (e.g. _Fn and _Func for C++ type names in templates and typedefs for standard or standard-adjacent libraries) or had common names that, even if hidden behind a header include, would result in undue naming burden on potential downstream users (fn_ptr_t or fn or func_t). Other permutations of these names, including ones with _Storage_/_storage or _Void and _void, either clashed with existing code or became undesirably long.

4. Wording

The following wording is against the latest draft of the C standard.

4.1. Modify §6.2.5 "Types"

6.5.2Types

...

...

The _Any_func type comprises the concept of a function; it is an incomplete object type that cannot be completed.

...

A pointer to void shall have the same representation and alignment requirements as a pointer to a character type.41) Similarly, pointers to qualified or unqualified versions of compatible types shall have the same representation and alignment requirements. A pointer to _Any_func shall have an alignment and representation suitable for the storage of any function pointer type. All pointers to structure types shall have the same representation and alignment requirements as each other. All pointers to union types shall have the same representation and alignment requirements as each other. Pointers to other types may not have the same representation or alignment requirements.

4.2. Modify §6.3.3.3 "Pointers"

6.3.3.3Pointers
...
...

A pointer to _Any_func can be converted to or from a pointer to any function type. A pointer to any function type can be converted to a pointer to _Any_func and back again; the result shall compare equal to the original pointer.

If and only if __STDC_IMPLICIT_ANY_FUNC_PTR_TO_VOID_PTR__ is defined and has a strictly positive value, a pointer to _Any_func can be converted to or from a pointer to void. A pointer to void can be converted to a pointer to _Any_func and back again; the result shall compare equal to the original pointer.

If and only if __STDC_IMPLICIT_ANY_FUNC_PTR_TO_VOID_PTR__ is defined and has a strictly positive value, a pointer to void can be converted to or from a pointer to any function type. A pointer to any function type can be converted to a pointer to void and back again; the result shall compare equal to the original pointer.

4.3. Modify §6.4.2 "Keywords" to add a new keyword

6.4.2Keywords
Synopsis

keyword: one of

...           ...           ...           ...           while

...           ...           ...           ...           _Any_func

...           ...           ...           ...           _Atomic

...           ...           ...           ...           ...

4.4. Modify §6.5.16 "Conditional operator"

6.5.16Conditional operator
...
Constraints
...

One of the following shall hold: ...

  • ... ; or

  • one operand is a pointer to an object type and the other is a pointer to a qualified or unqualified version of void . ;

  • if and only if __STDC_IMPLICIT_ANY_FUNC_PTR_TO_VOID_PTR__ is defined and has a strictly positive value, one operand is a pointer to a function type and the other is a pointer to a qualified or unqualified version of void; or
  • if and only if __STDC_IMPLICIT_ANY_FUNC_PTR_TO_VOID_PTR__ is defined and has a strictly positive value, one operand is a pointer to _Any_func and the other is a pointer to a qualified or unqualified version of void; or
  • one operand is a pointer to a function type and the other is a pointer to a qualified or unqualified version of _Any_func.
...

If both the second and third operands are pointers, the result type is a pointer to a type qualified with all the type qualifiers of the types referenced by both operands; if one is a null pointer constant (other than a pointer) or has type nullptr_t and the other is a pointer, the result type is the pointer type; if both the second and third operands have nullptr_t type, the result also has that type. Furthermore, if both operands are pointers to compatible types or to differently qualified versions of compatible types, the result type is a pointer to an appropriately qualified version of the composite type; if one operand is either a null pointer constant or a pointer to qualified or unqualified _Any_func , the result has the type of the other operand; otherwise, one operand is a pointer to void or a qualified version of void, in which case the result type is a pointer to an appropriately qualified version of void.

4.5. Modify §6.5.17.2 "Simple assignment"

6.5.17.2Simple assignment
Constraints

One of the following shall hold: ...

  • ...

  • the left operand has atomic, qualified, or unqualified pointer type, and (considering the type the left operand would have after lvalue conversion) one operand is a pointer to an object type, and the other is a pointer to a qualified or unqualified version of void, and the type pointed to by the left operand has all the qualifiers of the type pointed to by the right operand;

  • if and only if __STDC_IMPLICIT_ANY_FUNC_PTR_TO_VOID_PTR__ is defined and has a strictly positive value and the left operand is atomic, qualified, or unqualified pointer to _Any_func, and the other is a pointer to a qualified or unqualified version of void, and the type pointed to by the left operand has all the qualifiers of the type pointed to by the right operand;
  • the left operand has atomic, qualified, or unqualified pointer type, and (considering the type the left operand would have after lvalue conversion) one operand is a pointer to a function type, and the other is a pointer to a qualified or unqualified version of _Any_func, and the type pointed to by the left operand has all the qualifiers of the type pointed to by the right operand;
  • if and only if __STDC_IMPLICIT_ANY_FUNC_PTR_TO_VOID_PTR__ is defined and has a strictly positive value, the left operand has atomic, qualified, or unqualified pointer type, and (considering the type the left operand would have after lvalue conversion) one operand is a pointer to an function type, and the other is a pointer to a qualified or unqualified version of void, and the type pointed to by the left operand has all the qualifiers of the type pointed to by the right operand;
  • ...

4.6. Modify §6.10.10.3 "Environment macros"

6.10.10.3Environment macros

The following macro names are conditionally defined by the implementation:

... ...
__STDC_IMPLICIT_ANY_FUNC_PTR_TO_VOID_PTR__ A strictly positive integer constant expression if an implementation supports converting any function pointer type, or a pointer to _Any_func, to void* implicitly and vice-versa (6.3.3.3, 6.5.17.2).
... ...

4.7. Modify §7.24.6.2 "The fprintf function"

7.24.6.2The fprintf function
Synopsis
#include <stdio.h>
int fprintf(FILE * restrict stream, const char * restrict format, ...);
Description

...

The length modifiers and their meanings are:

... ...
j Specifies that a following b, B, d, i, o, u, x, or X conversion specifier applies to an intmax_t or uintmax_t argument; or that a following n conversion specifier applies to a pointer to an intmax_t argument ; or, a following p conversion specifier applies to an _Any_func* argument .
... ...

The conversion specifiers and their meanings are:

... ...
p The argument shall be a pointer to void or ,a pointer to a character type or a pointer to _Any_func . The value of the pointer is converted to a sequence of printing characters, in an implementation-defined manner.
... ...

4.7.1. NOTE: IDENTICAL CHANGES TO fwprintf!

4.7.2. NOTE: UPDATE ANNEX J UNDEFINED BEHAVIOR (Number 34)!

4.8. Modify §7.24.6.3 "The fscanf function"

7.24.6.3The fscanf function
Synopsis
#include <stdio.h>
int fscanf(FILE * restrict stream, const char * restrict format, ...);
Description

...

The length modifiers and their meanings are:

... ...
j Specifies that a following b, d, i, o, u, x, X, or n conversion specifier applies to an argument with type pointer to intmax_t or uintmax_t ; or, a following p conversion specifier applies to an argument with type pointer to pointer of _Any_func .
... ...

In the following, the type of the corresponding argument for a conversion specifier shall be a pointer to a type determined by the length modifiers, if any, or specified by the conversion specifier. The conversion specifiers and their meanings are:

... ...
p Matches an implementation-defined set of sequences, which should be the same as the set of sequences that may be produced by the %p or %jp conversion of the fprintf function. The corresponding argument shall be a pointer to a pointer of void ; or, with the j length modifier, a pointer to pointer of _Any_func . The input item is converted to a pointer value in an implementation-defined manner. If the input item is a value converted earlier during the same program execution, the pointer that results shall compare equal to that value; otherwise the behavior of the %p or %jp conversion is undefined.
... ...

4.8.1. NOTE: IDENTICAL EDITS TO fwscanf!

4.8.2. NOTE: UPDATE ANNEX J UNDEFINED BEHAVIOR (Number 36)!

References

Informative References

[N2230]
Martin Sebor. n2230 - Generic Function Pointer. March 26th, 2018. URL: https://www.open-std.org/JTC1/SC22/WG14/www/docs/n2230.htm