N2901
Transparent Function Aliases

Published Proposal,

Previous Revisions:
N2729 (r0)
Authors:
Paper Source:
GitHub
Issue Tracking:
GitHub
Project:
ISO/IEC JTC1/SC22/WG14 9899: Programming Language — C
Proposal Category:
Change Request, Feature Request
Target:
General Developers, Library Developers, Long-Life Upgradable Systems Developers, ABI Opinion Havers

Abstract

This proposals attempts to solve 2 intrinsic problems with Library Development in C, including its Standard Library. The first is the ability to have type definitions that are just aliases without functions that can do the same. The second is ABi issues resulting from the inability to provide a small, invisible indirection layer. Therefore, this proposal provides a simple, no-cost way to indirect a function’s identifier from the actual called function, opening the door to a C Standard Library that can be implemented without fear of backwards compatibility/ABI problems. It also enables general developers to upgrade their libraries seamlessly and without interruption to stability.

1. Changelog

1.1. Revision 2 - January 31st, 2022

1.2. Revision 1 - September 15th, 2021

1.3. Revision 0 - May 15th, 2021

2. Introduction & Motivation

After at least 3 papers were burned through attempting to solve the intmax_t problem, a number of issues were unearthed with each individual solution([N2465], [N2498], [N2425], [N2525]). Whether it was having to specifically lift the ban that §7.1.4 places on macros for standard library functions, or having to break the promise that intmax_t can keep expanding to fit larger integer types, the Committee and the community at large had a problem providing this functionality.

Thankfully, progress is being made. With Robert Seacord’s "Specific-width length modifier" paper was approved for C23 ([N2680]), we solved one of the primary issues faced with intmax_t improvements, which was that there was no way to print out a integral expression with a width greater than long long. Seacord’s addition to the C standard also prevented a security issue that commonly came from printing incorrectly sized integers as well: there’s a direct correlation between the bits of the supported types and the bits of the given in the formatting string now, without having to worry about the type beyond a sizeof(...) check. This solved 1 of the 2 core problems.

2.1. Remaining Core Problem - Typedefs, ABI, and Macros

Library functions in a "very vanilla" implementation of a C Standard Library (e.g., simply a sequence of function declarations/definitions of the exact form given in the C Standard) have a strong tie between the name of the function (e.g., imaxabs) and the symbol present in the final, compiled binary (e.g., _imaxabs). This symbol is tied to a specific numeric type (e.g., typedef long long intmax_t), which creates a strong relationship between the way the function is called (register usage, etc.) and more. Upgrading that type breaks old binaries that still call the old symbol; for example, _imaxabs being handed a __int128_t instead of a long long as an old application anticipates can result in the wrong registers being used, or worse. Thusly, because the Standard Library is bound by these rules and because implementations rely on functions with typedef-based types in them to resolve to a very specific symbol, we cannot upgrade any of the typedefs (e.g., what intmax_t is) or change anything about the functions (e.g., change imaxabs's declaration in any way consequential fashion).

Furthermore, macros cannot be used to "smooth" over the "real function call" because §7.1.4 specifically states that a user of the standard library has the right to deploy macro-suppressing techniques (e.g., (imaxabs)(24)) to call a library function (unless the call is designated to be a macro). This also includes preventing their existence as a whole with #undef imaxabs: every call after that #undef directive to imaxabs(...) must work and compile according to the Standard. While this guarantees users that they can always get a function pointer or a "real function name" from a given C Standard library function name, it also refuses implementations the ability to provide backwards compatibility shims using the only Standards-applicable tool in C++.

2.2. Liaison Issue - Stopping C++ Improvements

This is both a C Standard Library issue and a C++ Standard Library issue. Not only is it impossible to change the C Standard Library, but because of these restrictions and because the C Standard Library is included by-reference into C++, we cannot make any of the necessary changes to solve this problem in C++. This elevates the level of this problem to a liaison issue that must be fixed if we are to make forward progress in both C and C++.

2.3. Standardizing Existing Practice

While the C Standard Committee struggles with this issue, many other libraries that have binary interfaces communicated through shared or dynamically linked libraries have solved this problem. MSVC uses a complex versioning and symbol resolution scheme with its DLLs, which we will not (and could not) properly standardize here. But, other implementations have been using implementation-defined aliasing techniques that effectively change the symbol used in the final binary that is different from the "normal" symbol that would be produced by a given function declaration.

These techniques, expanded upon in the design section as to why we chose the syntax we did for this proposal, have existed for at least 15 years in the forms discussed before, and longer with linker-specific hacks.

2.4. C Issue - Prevented Evolution

Not fixing this issue also comes with a grave problem without considering C++ at all. We have no way of seamlessly upgrading our libraries without forcing end-users to consider ABI breakage inherit in changes type definitions or having library authors jump through implementation-specific and frightening hoops for creating (transparent) layers of indirection between a function call and the final binary. This means large swaths of C’s standard library, due to §7.1.4, are entirely static and non-upgradeable, even if we write functions that use type definitions that can change.

This is, by itself, a completely untenable situation that hampers the growth of C. If we cannot even change type definitions due to constraints such as linkage names from old code without needing a computer-splitting architectural change (e.g., the change from i686 "32-bit" architectures to x86_64 "64-bit" architectures that allowed for size_t to change), with what hope could be possibly have in getting C to evolve? How can we have it meet current hardware specifications and software needs? Users have been raging on about the lack of an int128_t in C, or a maximum integer type, and some implementers and platform users have stated:

I am unreasonably angry about this, because the intmax_t situation has kept me from enabling completely-working first-class int128 support out of clang for a decade. In that decade, I personally would have used 128b literals more times in personal and professional projects than the entire world has used intmax_t to beneficial effect, ever, in total.

Steve Canon, Mathematician & Clang Developer

At the surface of this issue and as illustrated by the many failed — and one successful — papers for intmax_t is that we need a better way for type definitions to be used for interfaces in the C standard. Underlying it is a wound that has begun to fester in the presence of not having a reason to invent wildly new architectures that necessitate fundamentally recompiling the world. Our inability to present a stable interface for users in a separable and Standards-Compliant way from the binary representation of a function that we cherish so deeply is becoming an increasing liability. If every function (the fundamental unit of doing work) essentially becomes impossible to change in any way, shape, or form, then what we are curating is not a living and extensible programming language but a dying system that is unequivocally doomed to general failure and eventual replacement.

3. Design

Our goal with this feature is to create a no-cost, zero-overhead function abstraction layer that prevents a type definition or other structure from leaking into a binary in a permanent and non-upgradable fashion. From the motivation and analysis above, we need the following properties:

To fulfill these requirements, we propose the a new transparent-alias construct that, in general, would be used like such:

extern long long __glibc_imaxabs228(long long);
extern __int128_t __glibc_imaxabs229(__int128_t);

/* ... */

#if __GNU_LIBC <= 228
	_Alias imaxabs = __glibc_imaxabs228;
#else
	_Alias imaxabs = __glibc_imaxabs229;
#endif

/* ... */

int main () {
	intmax_t x = imaxabs(-50);
	return (int)x;
}

It is composed of the _Alias keyword, followed by an identifier, the equals token, and then another identifier. The identifier on the right hand side must be either a previously declared transparent-alias or name a function declaration. Below, we explore the merits of this design and its origins.

3.1. Transparency - "Type Definitions, but for Functions"

We call this transparent because it is, effectively, unobservable from the position of a library consumer, that this mechanism has been deployed. The following code snippet illustrates the properties associated with Transparent Function Aliases:

#include <assert.h>

int other_func (double d, int i) {
	return (int)(d + i) + 1;
}

int real_func (double d, int i) {
	return (int)(d + i);
}

_Alias alias_func = real_func;

/* The below is a Constration Violation. You cannot redeclare */
/* a function alias with an incompatible signature. */
//void alias_func(void);

/* The is fine. You can redeclare a transparent alias, so long as */
/* the redeclaration is of compatible type. */
/* This, in particular, solves the Standard Library’s issue with */
/* §7.1.4’s permission to redeclare (Standard) Library symbols. */
void alias_func(double d, int i);


/* No Constraint Violation: redeclaration of an alias pointing
/* to the same declaration is fine. */
_Alias alias_func = real_func;

/* Constraint Violation: redeclaration of an alias pointing */
/* to a different declaration than the first one is not */
/* allowed. */
//_Alias alias_func = other_func;

int main ([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
	assert(&alias_func == &real_func); // no Constraint Violation

	typedef int(real_func_t)(double, int);
	real_func_t* real_func_ptr = alias_func; // decays to function pointer of real_func
	real_func_t* real_func_ptr2 = &alias_func; // function pointer to real_func
	[[maybe_unused]] int is_3 = alias_func(2.0, 1); // invokes real_func directly
	[[maybe_unused]] int is_4 = real_func_ptr(3.0, 1); // invokes real_func
	[[maybe_unused]] int is_5 = real_func_ptr2(3.0, 2); // invokes real_func
	assert(is_3 == 3); // no constraint violation
	assert(is_4 == 4); // no constraint violation
	assert(is_5 == 5); // no constraint violation
	assert(real_func_ptr == &real_func); // no constraint violation
	assert(real_func_ptr == &alias_func); // no constraint violation
	assert(real_func_ptr2 == &real_func); // no constraint violation
	assert(real_func_ptr2 == &alias_func); // no constraint violation
	return 0;
}

The notable properties are:

In short, alias_func works like any other function declaration would, but is not allowed to have its own function definition. It is simply an "alias" to an existing function at the language level. Given these properties, no implementation would need to emit a whole new function address for the given type; any binary-producing implementation would produce the same code whether the function was called through alias_func or real_func. It gets around the requirement of not being able to define C functions as macros, while maintaining all the desirable properties of a real C function declaration.

It also serves as a layer of indirection to the "real function", which means function alias definitions and type definitions can be upgraded with one another while improving backwards compatibility.

3.2. Inspiration: Existing Practice

It is not a coincidence in the initial example that we are using __glibc prefixes for the 2 imaxabs function calls. Tying a function to a name it does not normally "mangle" to in the linker is a common implementation technique among more advanced C Standard Library Implementations, such as musl-libc and glibc. It is also a common technique deployed in many allocators to override symbols found in downstream binary artefacts, albeit this proposal does not cover the "weak symbol" portion of the alias techniques deployed by these libraries since that is sometimes limited to specific link-time configurations, binary artefact distributions, and platform architecture.

Particularly, this proposal is focusing on the existing GCC-style attribute and a Clang-style attribute. The GCC attribute ([gcc-attribute]) follows this proposal fairly closely (but not exactly) in its requirements, by effectively allowing for an existing function declaration to have its address made identical to the function it is aliasing:

void __real_function (void) { /* real work here... */; }
void function_decl(void) __attribute__ ((alias ("__real_function")));

This code will set up function_decl to match the address of __real_function. It is common implementation practice amongst compilers that are GCC-compatible and that focus on binary size reduction and macro-less, transparent, zero-cost indirection. It has the small caveat that it requires the target of the attribute to be fully defined before this happens, so it can definitively insert the address. For example, here is documentation from the Keil’s armcc compiler ([keil-attribute]):

static int oldname(int x, int y) {
     return x + y;
}
static int newname(int x, int y) __attribute__((alias("oldname")));
int caller(int x, int y) {
     return oldname(x,y) + newname(x,y);
}

This code compiles to:

AREA ||.text||, CODE, READONLY, ALIGN=2
newname                  ; Alternate entry point
oldname PROC
     MOV      r2,r0
     ADD      r0,r2,r1
     BX       lr
     ENDP
caller PROC
     PUSH     {r4,r5,lr}
     MOV      r3,r0
     MOV      r4,r1
     MOV      r1,r4
     MOV      r0,r3
     BL       oldname
     MOV      r5,r0
     MOV      r1,r4
     MOV      r0,r3
     BL       oldname
     ADD      r0,r0,r5
     POP      {r4,r5,pc}
     ENDP

There are many compilers which implement exactly this behavior with exactly this GCC-extension syntax such as Oracle’s C Compiler, the Intel C Compiler, the Tiny C Compiler, and Clang ([oracle-attribute], [intel-attribute]). Clang also features its own asm-style attribute, where the function’s name is "mangled" to exactly the name given ([clang-attribute]). Microsoft Visual C uses a (slightly more complex) stateful pragma mechanisms and external compiler markup ([msvc-attribute]).

Intermediate object files may still carry the name "function_decl", but the binary in most binary-artefact producing implementations will not and only the target name will appear. This very closely matches the desired behavior: we want to create an entity that, ultimately, has no linkage and is specified in such a way that it will almost always disappear in both intermediate objects and final artefacts, wherever possible, while still providing a level of compile-time indirection. In this way this feature departs from how alias in GCC’s attribute syntax works, but we find this departure to be worth the effort.

3.2.1. Other Existing Practice: asm(...) and #pragma

There are 3 other proofs of practice in the industry today. One was an MSVC-like #pragma behavior/EXPORT-file specification. Another was a Clang-like asm()-attribute that behaved similarly to rename behavior. The third was a #pragma that simply made the linker do a find-and-replace symbol swap. When evaluated as potential alternatives to the syntaxes chosen here, there were a number of deficiencies for providing backwards compatibility. Notably, there are 2 chief concerns at play:

Clang’s asm(...), MSVC’s #pragma-based approach, and Oracle’s #pragma redefine_extname ([oracle-pragma]), are harder to standardize because each mechanism relies too heavily on the linker and the details of binary artefacts. The GCC-style attribute is tied to a front-end entity and, therefore, abstracts away binary changes as a means left to the implementation, Clang and MSVC’s approaches are not tied to any entity that exists in the program in general. Export #pragmas and asm(...) attributes can be used to reference any symbol, by unchecked string, that can be resolved at any later stage of compilation (possibly during linking and code generation). There is no good way to standardize such behavior because there are no meaningful semantic constraints that can be placed on their designs that are enforceable within the boundaries of the Abstract Machine.

Contrast this to GCC’s attribute. It requires that a previous, in-language definition exists. If that definition does not exist, the program has a constraint violation:

#if 0
extern inline int foo () { return 1; }
#endif

int bar () __attribute__((alias("foo")));
// <source>:5:27: error: alias must point to a defined variable or function
// int bar () __attribute__((alias("foo")));
//                           ^

int main () {
	return bar();
}

GCC’s design is more suitable for Standard C, since we do not want to specify this in terms of effects on a binary artefact or "symbols". GCC’s design makes no imposition on what may or may not happen to the exported symbols, only ties one entity to another in what is typically known as an implementation’s "front end". Whether or not final binary artefacts do the right thing is still up to an implementation (and always will be, because the Standard cannot specify such). This gives us proper semantics without undue burden on either specification or implementation.

Unfortunately, GCC’s definition of an alias requires that there exists a definition. We see no reason to keep that constraint as present, and therefore remove that constraint in the design of this feature.

3.2.2. Implementation Experience

An implementation of Transparent Aliases is available at this Clang fork. It provides the feature and, when built and installed, can be used to run this suite of tests.

The test suite sets up a supposed ABI on both Linux, Mac, and Windows by creating a DLL with exported functions. Without redefining the functions, the tests show that taking an old Shared Object or a Windows DLL, upgrading it and doing an in-place replacement of that file with a newly built object that contains new definitions, still lets old applications work while new applications return the correct and proper functionality from the updated code.

The test suite also outputs a version number while doing a comparison test. A negative, custom-defined intmax_t number - which uses bits in all available registers on 2s complement 32-bit and 64-bit systems - is properly returned from a function maxabs that computes its absolute value. That value is further compared equal to the size of a defined intmax_t, which resolves to 8 in the old application (long long) and 16 in the new application (__int128_t):

#include <my_libc/maxabs.h>

#include <stdio.h>

int main() {
  intmax_t abi_is_hard = -(intmax_t)sizeof(intmax_t);
  intmax_t but_not_that_hard = maxabs(abi_is_hard);
  printf("%d\n", my_libc_magic_number());
  return ((int)(but_not_that_hard) == sizeof(intmax_t)) ? 0 : 1;
}

The program only returns 0 (and passes the test suite) if the returned value is properly negated and compares equal to the original size. The test output on all 3 of Linux (Ubuntu and Debian), Mac (Mojave), and Windows (Version 10 and 11) is as follows:

[proc] Executing command: ctest -j10 -C Debug -T test --output-on-failure
[ctest] Cannot find file: transparent-aliases/.cmake/build/DartConfiguration.tcl
[ctest]    Site: 
[ctest]    Build name: (empty)
[ctest] Cannot find file: transparent-aliases/.cmake/build/DartConfiguration.tcl
[ctest] Test project transparent-aliases/.cmake/build
[ctest]       Start  6: discard_warning.compile.clean
[ctest]       Start 10: app_old.lib_new-copy
[ctest]       Start  8: app_old.lib_old
[ctest]       Start  9: app_new.lib_new
[ctest]       Start  3: example3
[ctest]       Start  5: discard_warning
[ctest]       Start  4: example4
[ctest]       Start  2: example2
[ctest]       Start  1: example1
[ctest]  1/11 Test  #6: discard_warning.compile.clean ....   Passed    0.20 sec
[ctest]       Start  7: discard_warning.compile
[ctest]  2/11 Test  #5: discard_warning ..................   Passed    0.12 sec
[ctest]  3/11 Test #10: app_old.lib_new-copy .............   Passed    0.20 sec
[ctest]       Start 11: app_old.lib_new
[ctest]  4/11 Test  #4: example4 .........................   Passed    0.12 sec
[ctest]  5/11 Test  #2: example2 .........................   Passed    0.09 sec
[ctest]  6/11 Test  #1: example1 .........................   Passed    0.06 sec
[ctest]  7/11 Test  #8: app_old.lib_old ..................   Passed    0.22 sec
[ctest]  8/11 Test  #9: app_new.lib_new ..................   Passed    0.19 sec
[ctest]  9/11 Test  #3: example3 .........................   Passed    0.16 sec
[ctest] 10/11 Test #11: app_old.lib_new ..................   Passed    0.03 sec
[ctest] 11/11 Test  #7: discard_warning.compile ..........   Passed    5.56 sec
[ctest] 
[ctest] 100% tests passed, 0 tests failed out of 11
[ctest] 
[ctest] Total Test time (real) =   5.79 sec
[ctest] CTest finished with return code 0

We specifically make sure to turn off typical automatic RPATH handling on *Nix machines, and make sure they load the DLL present in ${ORIGIN} (so that simply copying over the application-local DLL ensures it is loading the DLL associated with the test.

This serves as concrete evidence that existing implementations can use the feature to produce the same point. Users using the test compiler could reproduce these results successfully.

3.3. Why not an [[attribute]]?

At this point in time, one might wonder why we do not propose an attribute or similar for the task here. After all, almost all prior art uses an attribute-like or literal __attribute__ syntax. Our reasons are 2-fold.

3.3.1. Standard Attributes may be ignored.

The ability to ignore an attribute and still have a conforming program is disastrous for this feature. It reduces portability of libraries that want to defend against binary breakages. Note that this is, effectively, the situation we are in now: compilers effectively ruin any implementation-defined extension by simply refusing to support that extension or coming up with one of their own. Somewhat ironically, those same vendors will attend C Committee meetings and complain about binary breakages. We then do not change anything related to that feature area, due to the potential of binary breakages.

The cycle continues and will continue ad nauseum until Standard C provides a common-ground solution.

3.3.2. There is no such thing as "mangled name" or "symbol name" in the Standard.

Any attempt at producing normative text for a "symbol name" construct is incredibly fraught with peril and danger. Vendors deserve to have implementation freedom with respect to their what their implementation produces (or not). Solving this problem must be done without needing to draft specification for what a "binary artefact" or similar may be and how an attribute or attribute-like construct could affect it. If this feature relies primarily on non-normative encouragement or notes to provide ABI protection, then it is not fit for purpose.

Therefore, we realize that the best way to achieve this is to effectively allow for a transparent aliasing technique for functions, similar to type definitions. It must be in the language and it must be Standard, otherwise we can never upgrade any of our type definitions without waiting for an enormous architectural break (like the 32-bit to 64-bit transition).

3.4. Backwards-Compatibility with "Vanilla" C Standard Libraries

One of the driving goals behind this proposal is the ability to allow "vanilla" C Standard Library Implementations to use Standards-only techniques to provide the functions for their end-user. Let us consider an implementation — named vanilla, that maybe produces a vanilla.so binary — that, up until today, has been shipping a extern intmax_t imaxabs(intmax_t value); function declaration for the last 2 decades. Using this feature, we can provide an _entirely backwards compatible_, binary-preserving upgraded implementation of vanilla.so that decides to change it’s imaxabs function declarations. For example, it can use 2 translation units inttypes_compatibility.c and inttypes.c and one header, inttypes.h, to produce a conforming Standard Library implementation that is also backwards-compatible with binaries that continue to link against vanilla.so:

inttypes.c:

#include <inttypes.h>

__int128_t __imaxabs_vanilla_v2(__int128_t __value) {
	if (__value < 0)
		return -__value;
	return __value;
}

inttypes_compatibility.c:

extern inline long long imaxabs(long long __value) {
	if (__value < 0)
		return -__value;
	return __value;
}

inttypes.h:

/* upgraded from long long in v2 */
typedef __int128_t intmax_t;

extern intmax_t __imaxabs_vanilla_v2(intmax_t);

_Alias imaxabs = __imaxabs_vanilla_v2;

As long as inttypes_compatibility.c is linked with the final binary artefact vanilla.so, the presumed mangled symbol _imaxabs will always be there. Meanwhile, the "standard" inttypes.h will have the normal imaxabs symbol that is tied in a transparent way to the "Version 2" of the vanilla implementation, __imaxabs_vanilla_v2. This produces a perfectly backwards compatible interface for the previous users of vanilla.so. It allows typedefs to be seamlessly upgraded, without breaking already-compiled end user code. Newly compiled code will directly reference the v2 functions with no performance loss or startup switching, getting an upgraded intmax_t. Older programs compiled with the old intmax_t continue to reference old symbols left by compatibility translation units in the code.

This means that C Standard Libraries will have a language-capable medium of upgrading their code in a systemic and useful fashion. A working, tested, compiled version of this example is available at []().

3.4.1. Standard Library Redeclaration

One of the bigger problems that comes with this design space is that the C Standard Library specifically allows for a user to redeclare an existing standard library symbol:

Provided that a library function can be declared without reference to any type defined in a header, it is also permissible to declare the function and use it without including its associated header.

That means the following declaration in this translation unit is legal:

extern char* strcpy(char *restrict s1, const char *restrict s2);

int main () {
	return 0;
}

This is a restriction that is special to C. Thankfully, we are not particularly concerned about the ability to upgrade this function: users who are declaring Standard Library functions without including the header like this are doing this strictly as experts. They have a strong expectation of what symbol they are getting from their distribution. Transparent aliases are meant to be used for functions which rely on type definitions or structures defined in the standard library, including (but not limited to):

These are types which are controlled exclusively by the standard library and are known to change on 32-bit and 64-bit systems as well as transition based on locale and other compile-time settings. A single DLL can be distributed on a system and accommodate a variety of preprocessor-based switches or other compile-time information and allow an information to accommodate and/or upgrade a given system’s symbol table (or DLL/SO’s symbol table) without breaking old, original code.

Nevertheless, it is a common practice to redeclare standard library symbols after they are defined by a header. While we do not touch the strong/weak declaration portion of previous existing practice, we do provide wording which allows for a redeclaration of an _Alias as a function declaration. This allows for a common use case in code, even if the standard does not explicitly support redeclaring e.g. imaxabs or strtoimax.

3.5. The _Alias a = b syntax

The primary reason the syntax _Alias a = b; is chosen here is because we want to provide an in-language construct for doing this without requiring that the end-user completely re-declare the function they want to alias. For example, an alternative syntax was considered as follows:

extern void b(int w, double x, struct yy* y, struct zz* z);

void a(int w, double x, struct yy* y, struct zz* z) = b;

This gave the "function" and "redeclaration" feeling to a, but it required that all arguments essentially be reproduced exactly (or risk constraint violations). This introduces fault-intolerance, where function arguments could change and cause breakage in downstream code. This could result in a lot of unnecessary maintenance work for end-users and package maintainers alike responding to library developers and their change in type definitions or similar. It might end up tying future developer’s hands not for binary stability reasons, but for source breakage reasons. While source breaks are preferred over binary breaks, we want to avoid this being a problem altogether. This is an improvement over the GCC alias attribute, where two identical function declarations, differing only in the name, were required.

3.6. The literal word _Alias

_Alias is the safe choice. Originally, we used the word using as there was very little option to create a keyword that more appropriately mirrors "typedef but for functions". funcdef is already a prominent identifier in C codebases, and reusing typedef is not a very good idea for something that does not declare a type.

C++ took the keyword using, and so far it seems to have made most C and C++ developers stay away from the keyword altogether. Nevertheless, the wording uses a stand-in ALIAS-TOKEN. The suggestions we have for the token are as follows, based on not being findable in publicly available codebase sets (either on isocpp.org's code search of package manager code for Linux Distributions, GitHub’s dataset for code, and similar sources):

Various names were also thought of and unfortunately discarded because they exist as macro names and identifier names in publicly available code today:

While we use the word _Alias right now as a stand-in, we would appreciate feedback on what name to pick.

4. Future Directions

There has been expressed want for transparent aliases for non-functions. That is:

int f (int value) { return y + y; }

int main () {
	int x = 1;
	_Alias y = x;
	return &y == &x ? f(y) : 0; // returns f(x) == 2
}

We think this would be a useful general purpose compile-time renaming mechanism. But, that is beyond the scope of this paper at the moment. Various implementation-specific attributes above work with variables but we would like more time to work on the semantics of such before attempting to standardize such a feature. The precedence and usage experience for non-variables is far more important. As it is a constraint violation to do this with functions (and not undefined behavior), we have room to do this in the future.

There were also further suggestions to have the ability to make "_Weak" aliases: that is, the ability to be able to define a compile-time alias, but be able to redeclare over it and completely erase the old declaration in a way that can be replaced, at compile-time, with a new (and potentially incompatible) declaration. This is actually implemented in the current implementation, and it works as expected, but it is not provided in this paper. There is, thankfully, plenty of room in the grammar to allow for this.

5. Wording

The following wording is registered against [N2596].

5.1. Modify "§6.2.1 Scopes of identifiers", paragraph 1

An identifier can denote an object; a function; a tag or a member of a structure, union, or enumeration; a typedef name; a transparent alias name; a label name; a macro name; or a macro parameter.

5.2. Modify "§6.2.1 Scopes of identifiers", paragraph 4

Change every instance of " declarator or type specifier " to be " declarator, transparent alias, or type specifier ".

5.3. Modify "§6.2.1 Scopes of identifiers", paragraph 7

Structure, union, and enumeration tags have scope that begins just after the appearance of the tag in a type specifier that declares the tag. Each enumeration constant has scope that begins just after the appearance of its defining enumerator in an enumerator list. A transparent alias name has a scope that begins after the appearance of the transparent alias target in its definition. Any other identifier has scope that begins just after the completion of its declarator.

5.4. Modify paragraph 6 "§6.2.2 Linkages of identifiers"

The following identifiers have no linkage: an identifier declared to be anything other than an object or a function; a transparent alias; an identifier declared to be a function parameter; a block scope identifier for an object declared without the storage-class specifier extern.

5.5. Add transparent aliases to "§6.2.3 Name spaces of identifiers", paragraph 1, last bullet

  • all other identifiers, called ordinary identifiers (declared in ordinary declarators , transparent aliases , or as enumeration constants).

5.6. Add a new keyword to "§6.4.1 Keywords", Syntax, paragraph 1

keyword: one of

ALIAS-TOKEN

5.7. Modify "§6.7 Declarations" as follows...

5.7.1. §6.7 Syntax, paragraph 1, with a new "declaration" production

declaration:
declaration-specifiers init-declarator-listopt ;
attribute-specifier-sequence declaration-specifiers init-declarator-list ;
static_assert-declaration
attribute-declaration
transparent-alias-declaration

5.7.2. §6.7 Constraints, paragraphs 2 and 3

A declaration other than a static_assert or attribute declaration shall declare at least a declarator (other than the parameters of a function or the members of a structure or union), a tag , a transparent alias , or the members of an enumeration.
If an identifier has no linkage, there shall be no more than one declaration of the identifier (in a declarator or type specifier) with the same scope and in the same name space, except that:
  • a typedef name may be redefined to denote the same type as it currently does, provided that type is not a variably modified type;
  • a transparent alias name may be redefined as specified in 6.7.12; and
  • tags may be redeclared as specified in 6.7.2.3.

5.7.3. §6.7 Semantics, paragraph 5

A declaration specifies the interpretation and properties of a set of identifiers. A definition of an identifier is a declaration for that identifier that:
  • for an object, causes storage to be reserved for that object;

  • for a function, includes the function body;129)

  • for an enumeration constant, is the (only) declaration of the identifier;

  • for a typedef name, is the first (or only) declaration of the identifier . ; or

  • for a transparent alias name, is the first (or only) declaration of the identifier.

5.8. Modify "§6.9 External definitions" paragraphs 3 and 5

There shall be no more than one external definition for each identifier declared with internal linkage in a translation unit. Moreover, if an identifier declared with internal linkage is used directly or indirectly (e.g., through a transparent alias) in an expression (other than as a part of the operand of a sizeof or _Alignof operator whose result is an integer constant), there shall be exactly one external definition for the identifier in the translation unit.

An external definition is an external declaration that is also a definition of a function (other than an inline definition) or an object. If an identifier declared with external linkage is used directly or indirectly (e.g., through a transparent alias) in an expression (other than as part of the operand of a sizeof or _Alignof operator whose result is an integer constant), somewhere in the entire program there shall be exactly one external definition for the identifier; otherwise, there shall be no more than one.176)

5.9. Add a new sub-clause "§6.7.12 Transparent alias"

6.7.12 Transparent alias

Syntax

transparent-alias-declaration:
attribute-specifier-sequenceopt ALIAS-TOKEN identifier attribute-specifier-sequenceopt = identifier ;
Let the identifier on the left hand side be the transparent alias name and the identifier on the right hand side be the transparent alias target.

Constraints

A transparent alias target must refer to a preceding and visible transparent alias or a preceding and visible function declaration. A transparent alias being redefined shall refer to the same function declaration1⭐⭐. A transparent alias being redeclared as a function declaration shall still be considered a transparent alias after the redeclaration. The redeclaration shall still refer to the transparent alias target when called, and the function declaration shall have a compatible type with the transparent alias target.

Semantics

A transparent alias refers to an existing function declaration, either directly or through another transparent alias. A transparent alias does not produce a new function declaration; it is only a synonym for the transparent alias target specified. If the transparent alias target is another transparent alias, it is translated, recursively, until the existing function declaration is determined.
A transparent alias that refers to a function is a function designator (6.3.2.1). If its address is taken with the unary address operator (6.5.3.2) or used in an expression, it is converted to a pointer-to-function whose address is identical to the function declaration that the transparent alias target refers.
The optional attribute specifier sequence after the transparent alias name appertains to the transparent alias name.

EXAMPLE 1 The following program contains no constraint violations and does not call abort:

#include <assert.h>

void do_work(void);
void take_nap(void);

ALIAS-TOKEN work_alias = do_work;
ALIAS-TOKEN nap_alias = take_nap;
ALIAS-TOKEN alias_of_work_alias = work_alias;
ALIAS-TOKEN alias_of_nap_alias = nap_alias;

int main () {
	assert(&do_work == &work_alias);
	assert(&do_work == &alias_of_work_alias);
	assert(&work_alias == &alias_of_work_alias);

	assert(&take_nap == &nap_alias);
	assert(&take_nap == &alias_of_nap_alias);
	assert(&nap_alias == &alias_of_nap_alias);

	assert(&take_nap != &work_alias);
	assert(&do_work != &alias_of_nap_alias);

	ALIAS-TOKEN local_work_alias = alias_of_work_alias;
	assert(&local_work_alias == &alias_of_work_alias);

	do_work();
	work_alias(); // calls do_work
	alias_of_work_alias(); // calls do_work
	local_work_alias(); // calls do_work

	take_nap();
	nap_alias(); // calls take_nap
	alias_of_nap_alias(); // calls take_nap

	return 0;
}

EXAMPLE 2 Valid redeclarations:

int zzz(int requested_sleep_time);

ALIAS-TOKEN sleep_alias = zzz;
ALIAS-TOKEN sleep_alias = sleep_alias;
ALIAS-TOKEN sleep_alias_alias = zzz;
ALIAS-TOKEN sleep_alias = sleep_alias_alias;

void func(void);
int main () {
	// Inner scope: no constraint violation
	_Alias func = func;
}
EXAMPLE 3 Invalid redeclarations:
int zzz(int requested_sleep_time);
int truncated_zzz(int requested_sleep_time);

ALIAS-TOKEN sleep_alias = sleep_alias; // constraint violation: sleep_alias does
                                       // not exist until the
                                       // semicolon is reached
ALIAS-TOKEN zzz = truncated_zzz; // constraint violation: cannot hide
                                 // existing declaration
ALIAS-TOKEN truncated_zzz = truncated_zzz; // constraint violation: cannot change
                                           // function declaration
                                           // to transparent alias

ALIAS-TOKEN valid_sleep_alias = zzz;
double valid_sleep_alias(double requested_sleep_time); // constraint violation:
                                                       // redeclaring a
                                                       // transparent alias with
                                                       // non-compatible type

EXAMPLE 4 An alias can be redeclared as either an alias or a function declaration so long as it is compatible:

double purr(void) { return 1.0; }
ALIAS-TOKEN meow = purr;
double meow(void); // compatible redeclaration, still calls "purr"
ALIAS-TOKEN meow = purr; // compatible redeclaration

int main () {
	double x = meow(); // calls purr
	return (int)(v); // returns 1
}

EXAMPLE 5 Compatible and completed types through aliases:

void otter(int (*)[]);
_Alias water_noodle = otter;
void otter(int (*)[2]);
// water_noodle has type void (int (*)[2])

EXAMPLE 6 Shadowing and compatible types through aliases:

void cookie(int (*)[2]);

int main () {
	_Alias biscuit = cookie;
	{
		int cookie; // Shadow outer declaration.
		{
			void cookie(int (*)[]);
			// biscuit has type void (int (*)[2])
			// due to shadowing
		}
	}
	return 0;
}

EXAMPLE 7 Composite and compatible types through aliases:

void otter(int (*)[], int (*)[2]);
int main () {
	_Alias water_sausage = otter;
	{
		void otter(int (*)[2], int (*)[]);
		// water_sausage and otter have
		// composite type void (int (*)[2], int (*)[2])
	}
}
1⭐⭐) If the transparent alias target points to another transparent alias, then the alias target is first resolved. The resolution occurs recursively until a function declaration is the alias target. Equality between two alias names determines whether or not they ultimately refer to the same function declaration. Resolution of a transparent alias target happens before the synonym is declared or redeclared, meaning a transparent alias name may refer to itself when it is being redeclared, but not when it is first declared.

Recommended Practice

Implementations and programs may use aliases as a way to produce stability for translation units which rely on specific function declaration and definitions being present while aliasing a common declaration name for a more suitable interface. It may be particularly helpful for function declarations which use type definitions (6.7.8) in return and parameter types. Programs may update and upgrade alias definitions alongside type definitions to preserve entities and symbols in a given program while letting newer programs take advantage of upgraded functionality.

EXAMPLE 8 Versioning of a function call imaxabs while keeping old externally-defined function definitions available within the program.

extern intmax_t __imaxabs_32ish(__int32 value);
extern intmax_t __imaxabs_64ish(__int64 value);
extern intmax_t __imaxabs_128ish(__int128 value);

#if VER0
	typedef int intmax_t;
	ALIAS-TOKEN imaxabs = __imaxabs_32ish;
#elif VER1
	typedef long long intmax_t;
	ALIAS-TOKEN imaxabs = __imaxabs_64ish;
#elif VER2
	typedef __int128_t intmax_t;
	ALIAS-TOKEN imaxabs = __imaxabs_128ish;
#endif

References

Informative References

[CLANG-ATTRIBUTE]
LLVM Maintainers. Attributes: asm. August 15th, 2021. URL: https://clang.llvm.org/docs/AttributeReference.html#asm
[GCC-ATTRIBUTE]
GNU Compiler Collection. Function attributes: alias. August 16th, 2021. URL: https://gcc.gnu.org/onlinedocs/gcc-4.0.2/gcc/Function-Attributes.html#Function-Attributes
[INTEL-ATTRIBUTE]
Intel Compiler Collection. Documentation: attribute. June 28th, 2021. URL: https://software.intel.com/content/www/us/en/develop/articles/download-documentation-intel-compiler-current-and-previous.html
[KEIL-ATTRIBUTE]
Keil. __attribute__((alias)) function attribute. December 31st, 2019. URL: https://www.keil.com/support/man/docs/armcc/armcc_chr1359124973698.htm
[MSVC-ATTRIBUTE]
Microsoft Visual C++. EXPORTS | Microsoft Docs. September 7th, 2018. URL: https://docs.microsoft.com/en-us/cpp/build/reference/exports
[N2425]
Jens Gustedt. intmax_t, a way out v.2. February 10th, 2020. URL: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2425.pdf
[N2465]
Robert Seacord. intmax_t, a way forward. February 10th, 2020. URL: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2465.pdf
[N2498]
Martin Uecker. intmax_t, again. February 9th, 2020. URL: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2498.pdf
[N2525]
Philipp Klaus Krause. Remove the `fromfp`, `ufromfp`, `fromfpx`, `ufromfpx`, and other intmax_t functions. May 11th, 2020. URL: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2525.htm
[N2596]
ISO/IEC JTC1 SC22 WG14 - Programming Languages, C; JeanHeyd Meneide; Freek Wiedijk. N2596: ISO/IEC 9899:202x - Programming Languages, C. December 11th, 2020. URL: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2596.pdf
[N2680]
Robert Seacord. Specific-width length modifier. March 9th, 2021. URL: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2680.pdf
[ORACLE-ATTRIBUTE]
Oracle Solaris Studio. 2.9 Supported Attributes. December 30th, 2012. URL: https://docs.oracle.com/cd/E24457_01/html/E21990/gjzke.html#scrolltoc
[ORACLE-PRAGMA]
Oracle Solaris Studio. 2.12.22 redefine_extname. July 1st, 2017. URL: https://docs.oracle.com/cd/E77782_01/html/E77788/bjaby.html#OSSCGbjacu