Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.4.6a  Formas de invocación de funciones

§1  Sinopsis

En este epígrafe trataremos ciertos detalles relativos a la forma de efectuarse la invocación de funciones ( 4.4.6b) y al tratamiento de identificadores globales. Advirtiendo que ambas cuestiones están relacionadas, ya que ciertas formas de llamada presuponen determinado tratamiento de los identificadores.

Estos detalles tienen importancia cuando se quiere mezclar código C++ con el generado por otros lenguajes, bien porque necesitemos llamar una rutina en otro lenguaje desde un programa C++, o porque desde otro lenguaje necesitemos utilizar una rutina escrita en C++.  La razón es que todos los lenguajes no se comportan de la misma forma, variando ciertas cuestiones de detalle. Estos detalles se refieren concretamente a:

  • El tratamiento dado a los identificadores .
  • Convención de llamada a utilizar, que compone tres cuestiones: Limpieza de la pila; paso de parámetros, y tratamiento de los identificadores globales .

Los detalles sobre la forma de proceder en cada caso dependen de la plataforma. En la mayoría de compiladores es posible fijar ciertas directrices sobre la forma de proceder en estos casos, tanto a nivel global como a nivel particular de algunos identificadores. Los comentarios que siguen se refieren al compilador Borland C++, aunque salvando algunas cuestiones de detalle, pueden hacerse extensivos al resto de plataformas (MS Visucal C++, GNU g++, etc.)

Nota: en el caso de palabras que utilizan dos guiones bajos, _ _, se han separado con un espacio para mayor legibilidad (en el código deben aparecer juntos).

§2  Tratamiento de identificadores

Una característica que distingue a unos compiladores (lenguajes) de otros, es el tratamiento dado a los identificadores (nombres) de los objetos; lo que se conoce como sistema de codificación de nombres ("name encoding scheme"). De este sistema depende que durante las fases intermedias de la compilación, los identificadores sean guardados tal como los escribe el programador o sufran mutaciones más o menos importantes.

En BC++, cuando está activada la opción -u (lo que ocurre por defecto), el compilador guarda todos los identificadores globales en su grafía original (mayúsculas, minúsculas o mixta), añadiendo automáticamente un guión bajo "_" ("Underscore" ASCII 95 2.2.1a) delante de cualquier identificador global, ya sea de función (todas lo son), o de variable. Para modificar este comportamiento se puede utilizar la opción -u- como parámetro en la línea de comando del compilador ( 1.4.3).

En el caso de identificadores que han sido modificados con el identificador Pascal, no se añade ningún guión, pero el identificador es convertido a mayúsculas. Ejemplos:

En el fuente

En el módulo .obj

int x;

int _x

__pascal int x;

int X


Este tratamiento para los identificadores puede ser también especificado a nivel individual mediante algunos especificadores (palabras clave que se relacionan ) situadas en la declaración. Ejemplo:

int __cdecl x;
int _pascal y;

sin embargo, cuando estos especificadores se aplican a funciones, además de afectar al tratamiento del identificador de la función como símbolo de ámbito global, suponen también una forma concreta en la convención de llamada utilizada. Ejemplos:

int _cdecl y;           // especificador de variable global
int _cdecl func1(int);  // especificador de función


§2.1  La tabla adjunta resume el efecto de un modificador aplicado a una función. Por cada modificador, se muestra el orden en que son colocados en la pila los parámetros de la función. Después se indica si es la función que realiza la invocación ("Caller"), o la función llamada ("Called"), la responsable de sacar los parámetros fuera de la pila. Finalmente, se muestra el efecto en el nombre de una función global.

Modificador Colocación de
parámetros
Quién quita
los parámetros

Cambio de nombre
(solo en C)

__cdecl Derecha a izq. func. invocante se añade '_' como prefijo
__fastcall Izq. a derecha func. invocada se añade '@' como prefijo
__pascal Izq. a derecha func. invocada se convierte a Mayúsculas
__stdcall Derecha a izq. func. invocada Sin cambio

Nota: en C++ __fastcall   y  __stdcall son siempre nombres "planchados". Recordar que existe una directiva de compilación extern "C", que se utiliza en la declaración de funciones para indicar al compilador que el nombre de la función no debe ser planchado ( 1.4.2).

§3  Convención de llamada

En informática se han consagrado diversas formas de invocación de funciones, las más frecuentes son las siguientes: Rápida C Pascal Registro y  Estándar .

Estas convenciones se diferencian en:

  • La forma que cada una utiliza para la limpieza de la pila (stack).
  • El orden de paso de parámetros (derecha a izquierda o a la inversa).
  • El uso o no de mayúsculas y minúsculas, y ciertos prefijos en los identificadores globales.

La especificación de la forma que se utilizará en el programa, puede hacerse a nivel global o solo a nivel particular de algunas funciones específicas.

Para indicarlo a nivel global se utiliza alguno de los comandos específicos del compilador (son los indicados en cada caso). En estos casos, las palabras clave: _ _pascal_ _fastcall, o _ _stdcall pueden utilizarse para declarar que una rutina o función utiliza específicamente una convención distinta de la señalada como general.

La forma de indicarlo a nivel particular es mediante el uso de ciertas palabras reservadas para que sea utilizada una forma específica en lugar de la que tenga asignada el compilador por defecto . Estas palabras deben indicarse en la declaración o prototipo, y delante del especificador de invocación de la función. Son las siguientes:  _ _cdecl, _ _pascal, _ _fastcall, _ _msfastcall y _ _stdcall.

Nota: en la programación Windows estas convenciones de llamada se utilizan a través de sus propios typedefs. En concreto se utilizan las siguientes equivalencias

especificador Windows equivalente Tipo de invocación
CDECL __cdecl Invocación C
WINAPI __stdcall Estándar
CALLBACK __stdcall Estándar

Por ejemplo, en un programa C++ normal:

__pascal int funcionUno(x);    // Error!!

int __pascal funcionUno(x);    // Ok.

int __stdcall main (int argc, char* argv[]) {  // Ok.

   ...

}

En un programa C++ para Windows (que incluye la cabecera <windows.h>):

int PASCAL functionUno(x);     // Ok.

int WINAPI WinMain (HINSTANCE hTh, HINSTANCE hPr, LPSTR lps, int nFu) {  // Ok.

   ...

}

§3.1  Invocación rápida

Esta opción se ha incluido en el compilador BC++ para compatibilidad con el de Microsoft. Indica al compilador que utilice convención de llamada de MS VC++ para todas las funciones que no tengan explícitamente declarada otra forma.

En esta convención, las dos primeras DWORD [1] o argumentos más pequeños pasan a los registros ECX y EDX; todos los demás pasan de derecha a izquierda. La función invocada es responsable de desalojar los argumentos de la pila.

Formas de indicarlo al compilador:

  • Especificador global:  comando de compilación -pm
  • Especificador particular:  especificador _ _msfastcall

Ejemplo:

int __msfastcall funcDos (x, y);

§3.2  Invocación C

Esta opción indica al compilador utilizar la secuencia de llamada estándar C para funciones, es decir: generar guiones de subrayado -guiones bajos-; distinguir mayúsculas de minúsculas (no transformar minúsculas en mayúsculas); pasar parámetros de derecha a izquierda.  En BC++ se utiliza por defecto esta convención de llamada.

Como indicábamos anteriormente , en este tipo de invocación, la colocación de parámetros en la pila se realiza de derecha a izquierda, lo que significa que el último argumento de la función es colocado el primero y el primero es colocado el último. Esto hace que estas funciones puedan utilizar un número variable de parámetros en cada invocación. Es decir, no tienen que pasar necesariamente el mismo número de parámetros en todas las invocaciones que se realicen a dicho código. Las funciones que gozan de esta particularidad son denominadas "variadic" en la literatura inglesa.  La función que realiza la llamada ("caller") es la encargada de limpiar la pila, lo que provoca que el código de este tipo de ejecutables sean ligeramente mayores que en el resto de convenciones, en las que es la función invocada ("called") la que limpia la pila.

Por ejemplo, supongamos que la declaración de una función y su posterior invocación adoptan la forma señalada en el siguiente esquema:

void __cdecl foo(int x, ...);   // foo con numero variable de parámetros

...
int x;
char a;
int b;
double c;
...
foo(x, a, b, c);

En estas condiciones, durante la compilación, el analizador semántico solo puede verificar la concordancia del primer argumento de la invocación de foo con el indicado en la declaración; el resto permanece sin verificar.  Posteriormente, durante la invocación de foo en run-time, el compilador asume que los argumentos c, b, a y x (en este orden) se corresponden con valores situado a partir de cierta distancia ("offset") del principio de la pila.  En este caso, con los valores situados en los desplazamientos  64; 96 (64+32); 104 (96+8) y 136 (104+32).

Formas de indicarlo al compilador:

  • Especificador global: comandos de compilación -pc, -p-  ( 1.4.1a)
  • Especificador particular:  especificadores cdecl_cdecl y _ _cdecl  en la declaración de la función o identificador global (las tres formas son equivalentes).

Ejemplos:

int __cdecl FileCount;
long __cdecl HisFunc(int x);

Nota:  Con frecuencia, en las expresiones C/C++ aparecen los especificadores _RTLENTRY y _USERENTRY.  Se trata de defines establecidos en los ficheros de cabecera del compilador.  El primero indica la convención de llamada utilizada por la RTL ("Run Time Library"). El segundo la convención de llamada que esperan las funciones de la RTL para las funciones compiladas por el usuario ("callbacks"). Generalmente se resuelven en la convención  __cdecl.  Un ejemplo paradigmático de su uso es la función qsort de la RTL de C/C++, que realiza la ordenación de los elementos de una tabla en base a un criterio proporcionado por el usuario en forma de una función que devuelve un entero menor, igual o mayor que cero según que el primero de los elementos comparados sea menor, igual o mayor que el segundo.  Esta función es utilizada por qsort mediante una retrollamada ("callback"), cada vez que tiene que comparar dos elementos. El prototipo de qsort es como sigue (consulte el manual de su compilador para una explicación del significado de los argumentos):

void qsort(void* base, size_t nelem, size_t width,
           int (_USERENTRY *fcmp)(const void*, const void*));

§3.3  Invocación Pascal

Esta opción indica al compilador utilizar la secuencia de llamada de Pascal para las funciones: no generar guiones de subrayado, forzar identificadores a mayúsculas, limpieza de la pila por la función que realiza la llamada y paso de parámetros de izquierda a derecha  ( 1.4.1a).

Generalmente las llamadas de función resultantes son más pequeñas y rápidas que las generadas con la convención C , y también utilizan la pila para el paso de parámetros. En este caso, las funciones deben pasar el número exacto de argumentos y del tipo adecuado. Los identificadores de las funciones declaradas con este modificador están sujetos al planchado de nombres ( 1.4.2).

Nota: en la programación para entornos Windows 3.x se exigía que las llamadas al Sistema utilizaran esta convención.

Formas de indicarlo al compilador:

  • Especificador global: comando de compilación -p
  • Especificador particular:  especificadores pascal, _pascal o _ _pascal  (las tres formas son equivalentes).
§3.4  Invocación Registro

Esta opción indica que se deben generar todas las funciones utilizando la convención de paso de parámetros a registro. Esto supone que los tres primeros parámetros (de izquierda a derecha) se colocan en los registros EAX, EDX y ECX ( H3.2). En caso que se trate de objetos que no quepan en los registros, por ejemplo, si son números fraccionarios o estructuras, no se utilizan los registros, sino la pila como es usual.

Las funciones declaradas con los identificadores _cdecl o _pascal no pueden utilizar simultáneamente este especificador, porque ambas utilizan la pila para el paso de parámetros.

Los identificadores de estas funciones están sujetos al planchado de nombres ( 1.4.2), además el compilador les añade una arroba '@' como prefijo. Las formas de solicitar esta convención son:

  • Especificador global: comando de compilación -pr
  • Especificador particular: especificadores _fastcall_ _fastcall (ambas formas son equivalentes).

Ejemplo:

int _msfastcall funcTres (x, y, z);

§3.5  Invocación estándar

Esta opción indica al compilador que debe utilizar la secuencia de llamada estándar para funciones: no generar guiones bajos; conservar mayúsculas y minúsculas; la función invocada limpia la pila, y colocación de parámetros de derecha a izquierda.

A diferencia de la invocación C, esta convención exige pasar el número y tipo de argumentos exacto. Estas funciones cumplen con la convención de argumentos de Win32, y están sujetas a planchado de nombres ( 1.4.2). Las formas de indicarlo al compilador son:

  • Especificador global: comando de compilación -ps
  • Especificador particular: especificador _ _stdcall. y _stdcall

Ejemplo:

#define WINAPI __stdcall
#include <windows.h>
 
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow) {
   MessageBox (NULL, TEXT ("Hola Windows 98"), TEXT ("Saludo"), 0);
   return 0;
}

Dado que en la programación Windows, la función WinMain es equivalente a la main de C/C++, la sentencia anterior constituye el inicio de cualquier aplicación Windows.  Su sintaxis señala explícitamente al compilador que debe utilizar para ella la convención de llamada estándar.

Tema relacionado:
  • La convención de llamada en la definición de punteros-a-función (  4.2.4a)

  Inicio.


[1]  La programación Windows utiliza sus propios tipos, expresados mediante typedef ( 3.2.1a). Por ejemplo, la definición de DWORD es:

typedef unsigned long DWORD;