Calling Conventions

From Open Watcom

Jump to: navigation, search

Many programmers are unfamiliar with calling conventions and their rationale; this breeds many misconceptions and leads to errors, making programmers' lives unnecessarily difficult. The following article is an attempt to demystify calling conventions on x86 architecture, explaining the whys and wherefores of their usage.

Contents

What are Calling Conventions?

A calling convention, as the name suggests, describes rules for calling functions (routines), passing arguments, receiving return values, and the environment that needs to be established and/or maintained.

Some programmers, especially those coming from UNIX environments, may be unfamiliar with the concept of calling conventions, or may be unaware of their exact role in software architecture. To explain the rationale for calling conventions, it may be useful to begin with a bit of history.

The UNIX Way

Calling convention do exist in UNIX environments, but most programmers remain blissfully unaware of them. In many cases, a single vendor (such as Sun, IBM, HP, SGI/MIPS) designed the CPU architecture, provided the operating system, and wrote the compilers. The development environment is heavily biased towards C for obvious reasons. The ABI specifies the calling convention (there is typically only one). There is neither a way nor a pressing need to use other calling conventions.

The DOS Way

The situation in the x86 PC environment was markedly different. One company provided the CPU (Intel), another the systems (initially IBM, later hundreds of others), another provided the OS (Microsoft in the case of PC-DOS), a number of companies provided development tools (Microsoft, IBM, Borland, Watcom, Zortech, Lattice, Metaware, and many others). Perhaps more importantly, the development environment was not even remotely C-centric: while every IBM PC came with built-in BASIC in its ROM, a C compiler was not part of the package. Neither the BIOS nor DOS provided interfaces directly callable from a HLL. C was merely one of many programming languages, not 'the' language; Pascal, BASIC, Fortran, even assembler were widely used.

Why Different Calling Conventions?

Given the situation described above, it should be clear that there were at least two major factors that practically guaranteed the proliferation of calling conventions in the DOS environment:

  • Lack of calling standard imposed by the OS. Language vendors were more or less forced to invent their own calling conventions.
  • Multitude of programming languages. Different languages have varying needs; a calling standard that works for one language may not work for another.

The latter point warrants further attention. For instance C supports variadic functions; that forces the calling convention to push arguments on the stack from right to left, and have the callee clean up the stack (the caller knows how many arguments there are at compile time; callee does not). For other languages, it may be advantageous to pass arguments left-to-right, in order to ease left-to-right argument evaluation which may be prescribed by the language definition. Some languages (especially object-oriented ones) may need to pass various hidden arguments.

The Need for Interoperability

None of this would be an issue if programmers hadn't cared about interoperability. But they did; they wanted to be able to use tools from multiple vendors, and they wanted to be able to write programs that consisted of modules written in multiple programming languages. There was a need for the compilers to be able to work with multiple calling conventions, and for the languages to provide a mechanism to designate specific functions as using certain calling conventions.

These mechanisms are naturally language specific. Further discussion will be restricted to C and C++ languages.

Specifying Calling Conventions

The first important fact to realize is that calling conventions are not part of the language standards. Calling conventions are, by nature, platform specific, while C and C++ language standards are not. There are two common de facto standards of designating calling conventions. The traditional way employs type qualifiers; their usage is similar to const or volatile keywords. For example:

extern int __cdecl foo( char * );

In this example, foo is a function taking a pointer to char as an argument and returning int; the function uses the __cdecl calling convention (also known as the C calling convention). Other calling conventions supported by Open Watcom C and C++ compilers are __stdcall, __syscall, __fortran, __pascal, and the default calling convention, __watcall.

The other method of specifying calling convention is using the __declspec type qualifier. The following declaration is equivalent to the one above:

__declspec(__cdecl) extern int foo( char * );

This syntax was first supported in Microsoft C compilers in the early 1990s; most compilers that target Windows support it. Note that the __declspec qualifier has more uses than specifying calling conventions; such uses are not discussed here.

Most of the calling convention specifiers have aliases, for instance _cdecl (for __cdecl) or _System (for __syscall). Use of aliases with no underscores or a single underscore and lowercase letter is discouraged, because such identifiers are not implementation-reserved and may conflict with user code. See the C/C++ User's Guide for further details.

Specifying Calling Conventions the Watcom Way

Open Watcom supports another, entirely implementation-specific way of specifying calling conventions. This method uses the powerful #pragma aux facility. Detailed description may be found in the C/C++ User's Guide (programmers skilled enough to be able to use this method are presumably also sufficiently skilled to read a manual). An example follows:

  #pragma aux __mycall "_!" \ 
                parm caller [eax ecx] \ 
                value [edx] \ 
                modify [eax ecx edx]

In this case, the fictitious __mycall calling convention lowercases symbol names and prefixes them with an underscore, uses registers eax and ecx to pass arguments, uses register edx to return values, and called routines may modify registers eax, ecx, and edx.

This way of describing calling conventions is very flexible and offers much higher level of control than most other compilers. Very few users are likely to need this method, but if it is needed, it can be a lifesaver.

Symbol Decoration

Every calling convention specifies not only how arguments are passed (ie., what the object code should look like), but also how symbol names are decorated. For instance the C calling convention prepends an underscore, Watcom calling convention appends an underscore, and Pascal calling convention uppercases symbol names.

This is not whimsical but quite deliberate, and the decorations are designed to prevent calling convention mismatches at link time. If a function in one module is to call a function in another module, it is obvious that both modules must agree on the calling convention. But because the modules are compiled separately (and could be compiled on different machines or even with different compilers), the compiler itself cannot enforce this.

The linker, on the other hand, does not have -- and should not need -- any knowledge of calling conventions or even specific programming languages. Symbol name decorations are a good solution: If module A calls routine X using the C calling convention, it will reference external symbol _X. If module B implements routine X but uses the Watcom calling convention, it will supply a public symbol X_. The program will not link, which is far preferable to discovering the mismatch at run time; most programmers do not have the experience required to correctly diagnose a calling convention mismatch, especially if the two conventions are similar.

Name Mangling

Name mangling is a C++ specific riff on symbol decoration. Because C++ supports overloading, a function or variable name is not sufficient to unambiguously specify a symbol; type information needs to be encoded in the symbol name. While name mangling is also not part of the C++ language standard, all C++ compilers need to use it in one way or another.

The concept of name mangling is, strictly speaking, orthogonal to that of calling conventions. In practice, there is some overlap as both specify symbol names. Furthermore, C++ modifies the calling conventions for class member functions, because a hidden this argument is passed. However, because C++ object code is generally not interoperable between compilers, or even different versions of the same compiler, this is rarely an issue.

Proper Usage

There are ways to use calling conventions that maximize flexibility and minimize problems. The rule of thumb is simple: Externally callable functions need to have their calling convention explicitly specified, while internal functions do not. Following this rule has two benefits:

  • Externally callable functions will be usable from any compiler that supports the specified calling convention
  • Internal functions may use the optimum calling convention supported by the compiler

Note that the distinction between externally callable vs. internal functions is not at all the same as the distinction between extern and static linkage. In a library, functions declared as extern may not necessarily be visible to library users, and conversely, static functions may be externally callable if their address is taken and made available to outside callers.

When writing portable code, it is advisable to use macros to specify calling conventions. That way, the macro may be empty on systems where calling conventions are irrelevant, and there is a way to use different implementation-specific mechanisms to specify the same calling convention. A real-world example follows (from OpenGL):

GLAPI void APIENTRY glLoadName( GLuint name );

In this case, the macro GLAPI might expand to extern and APIENTRY to __stdcall.

Common Pitfalls

Users often ask questions such as "How do I stop Open Watcom from appending an underscore to function names?". That is entirely wrong question to ask. As explained above, unresolved extern errors while linking do not mean that the symbol names are somehow wrong; these errors signify that there is a calling convention mismatch and at least one of the modules needs to be fixed and recompiled. In these cases, users need to determine which calling convention(s) should be used and appropriately modify the source code.

Less often, there are mismatches between C and C++ symbol naming; a telltale sign of C++ code incorrectly calling C functions is that the linker lists function argument and return types in the error message:

Error! E2028: int near foo( char near * ) is an undefined reference

As explained above, the magic of C++ name mangling ensures that types are encoded in symbol names; this does not happen with C function. In these cases, judicious use of extern "C" is usually called for, in order to tell the C++ compiler that an external function is implemented in C. Similarly, if a function implemented in C++ is to be callable from C, it needs to be declared as extern "C".

Segment Register Usage

In segmented environments (typically 16-bit code), different calling conventions imply different segment register usage. In large model, Open Watcom C/C++ by default considers both ES and DS to be volatile registers. The C calling convention (__cdecl) used by other compilers usually requires DS to be "pegged" to DGROUP. This may cause problems when mixing object code generated by and for different compilers, whether the source code was originally written in C, assembler, or another language.

Open Watcom will make sure that DS points to DGROUP before calling functions declared as __cdecl. As an alternative,

#pragma aux foo parm loadds

can be used to make sure that DS points to DGROUP before calling function foo, without affecting any other aspects of the calling convention.

There are also compiler options which affect the code generated by Open Watcom C/C++ for functions using the default Watcom calling convention. The -zdp switch forces DS to be pegged to DGROUP. DS effectively becomes a read-only register and the generated code doesn't change it. The -zfp/-zgp options are analogs for the FS and GS registers; the ES register is always considered to be floating.

The -r switch on the other hand makes all segment registers non-volatile, which means that a function may modify segment registers but they will be saved and restored as part of function prologue/epilogue.

Further reading

Personal tools