Function Parameters
CS 301 Lecture, Dr. Lawlor
Here's some C++ code that calls an external function "bar". Note
that this code gives a link error when you try to run it in NetRun,
because "bar" is never defined.
The "extern "C"" tells C++ to just look for a
C-style plain function "bar", instead of a fancy overloaded C++
function "bar(int,int,int)".
extern "C" int bar(int a,int b,int c);
int foo(void) {
return bar(0xA0B1C2D3, 0xE0E1E2E3, 0xF0F1F2F3);
}
(executable NetRun link)
We can actually write this "bar" function in assembly, like this:
global bar
bar:
mov eax,edi
ret
(Try this in NetRun now!)
When we get called, our first parameter is sitting in register edi as usual.
The "global" keyword in assembly tells the assembler to make a symbol, in this case bar, visible from outside the file.
The "Link With:" box tells NetRun to link together two different projects, in this case one in C++ and the other in assembly.
Argument Passing
C++ is kinda asymmetrical, because "int" or "long" parameters are
placed
directly in registers (like edi or rdi), while arrays are always passed
via pointer (rdi contains the *address* of the
array, not the actual integers in the array). C/C++ do this
because you don't have enough registers to store the entire array, plus
it'd be expensive to copy the array into and out of registers.
Fortran, curiously, passes *everything* via pointer--if a Fortran
function takes an int parameter, what gets pushed on the stack is a
*pointer* to an int, not the int itself!
To summarize:
When passing...
|
In C++, you...
|
In Fortran, you...
|
an int
|
put the int into edi
|
put a pointer to the integer into rdi
|
a long
|
put the long into rdi
|
put a pointer to the long into rdi |
an array
|
put a pointer to the first element of the array into rdi
|
put a pointer to the first element of the array into rdi (same as C++)
|
a char
|
put an int containing the character's value into edi
|
put a pointer to the character into rdi
|
Fortran 1D arrays are indexed using round brackets, like
"myarr(i)". And the index of the first array element in Fortran
is "myarr(1)", not "myarr[0]" like C/C++. But beyond those small
differences, arrays work exactly the same in Fortran as in C++, and
in fact it's not always possible from looking at the generated assembly
code whether the original code was written in plain C, C++, Fortran, or
Assembly!
Name Mangling, plain C, and extern "C"
C++ "mangles" the linker names of its functions to include the data
types of the function arguments. This is good, because it lets you
overload function names; but it's bad, because plain C and assembly don't
do anything special to the linker names of functions.
In plain C or assembly, a function "foo"
shows up as just plain "foo" in the linker. In C++, a function foo shows
up as "foo()" or "foo(int,double,void *)". (Check out the disassembly to
be sure how your linker names are coming out.)
So if you call C or assembly code from C++,
you have to turn off C++'s name mangling by declaring the
C or assembly routines 'extern "C"', like this:
extern "C" void some_assembly_routine(int param1,char *param2);
or wrapped in curly braces like this:
extern "C" {
void one_assembly_routine(int x);
void another_assembly_routine(char c);
}
In fact, it's common to provide a "magic" header file for C code that
automatically provides 'extern "C"' prototypes for C++, but just works
normally in plain C:
#ifdef __cplusplus /* only defined in C++ code */
extern "C" {
#endif
void one_assembly_routine(int x);
void another_assembly_routine(char c);
#ifdef __cplusplus /* only defined in C++ code */
}
#endif
Definitely try these things out yourself:
Plain C bar routine:
int bar(int i,int j) {
printf("bar(%d,%d)\n",i,j);
return i;
}
(executable NetRun link)
C++ foo routine that calls bar:
extern "C" int bar(int i,int j);
int foo(void)
{
return bar(2,3);
}
(executable NetRun link)
Try:
- Remove the 'extern "C"' from the bar prototype. Note the
link error is looking for "bar(int,int)", which means it's looking for
a C++ bar; but bar is C.
- Make bar C++. Without extern "C", everything works, because C++ is calling C++.
- Make bar C++, and add 'extern "C"' back to foo's prototype. It won't link, because it's looking for the "C" bar.
- Make bar C++, but add 'extern "C"' to bar's declaration in *both*
routines. Now you're linking the C++ bar using the C name.
- Make foo C, but leave 'extern "C"' in bar's implementation.
- Make both routines C, and remove the extern "C"s. Everything works fine, because C is calling C.
Code written in
|
With name
|
Has linker name
|
C++
|
int bar(int a,int b)
|
bar(int,int) <- But "mangled" to be alphanumeric...
|
C++
|
extern "C" int bar(int a,int b) |
bar |
C
|
int bar(int a,int b)
|
bar |
Assembly
|
global bar
bar:
|
bar
|
Fortran
|
SUBROUTINE bar()
|
bar_, BAR, BAR_, bar__, or some such. Disassemble to be sure...
|
Bottom line: to call code written in anything else (C, Assembly,
Fortran) from C++, or to call C++ from anything else, add extern "C" to
the C++ code!
Fun With Fortran!
CS 301 isn't a computer languages course, but I think it's pretty interesting to look
at old-school Fortran, a language from 1956. Note how this little function returns 10,
like you'd expect. And the assembly code is pretty much exactly
what you'd get from C/C++!
function foo()
INTEGER foo
i = 7;
foo = i + 3;
end function
(executable Net Run link)
Here's a "do loop" (the Fortran equivalent of C/C++ "for"):
function foo()
INTEGER foo
do i=1,10
CALL print_int(i)
end do
foo = i + 3;
end function
(executable NetRun link)
Note that "print_int" is defined in NetRun's "inc.c" as:
CDECL void print_int__(int *i) {print_int(*i);}
Here,
- "CDECL" is a NetRun macro that expands to extern "C" in C++. This prevents C++ from screwing up the name.
- "print_int__"
(note the extra underscores!) is the function name Fortran will try to
link with. Sometimes there's only one underscore, sometimes two,
sometimes all capitals, sometimes capitals and an underscore, sometimes
leading underscores. Different compilers, or different versions
of the same compiler, will work differently!
- Fortran "int" is passed via a pointer, so you have to accept a pointer in C++.
- The Fortran interface for print_int just calls the C++ interface.
This sort of Fortran/C/C++ interfacing is really common in big
projects, mostly because engineers like Fortran, hardcore assembly
hackers prefer plain C, and GUI designers and general coders tend to
use C++.