关于extern_C
通常,在C语言的头文件中经常可以看到类似下面这种形式的代码
#ifdef __cplusplus
extern "C" {
#endif
/**** some declaration or so *****/
#ifdef __cplusplus
}
#endif /* end of __cplusplus */
那么,这种写法什么用呢?实际上,这是为了让CPP能够与C接口而采用的一种语法形式。之所以采用这种方式,是因为两种语言之间的一些差异所导致的。由于CPP支持多态性,也就是具有相同函数名的函数可以完成不同的功能,CPP通常是通过参数区分具体调用的是哪一个函数。在编译的时候,CPP编译器会将参数类型和函数名连接在一起,于是在程序编译成为目标文件以后,CPP编译器可以直接根据目标文件中的符号名将多个目标文件连接成一个目标文件或者可执行文件。但是在C语言中,由于完全没有多态性的概念,C编译器在编译时除了会在函数名前面添加一个下划线之外,什么也不会做(至少很多编译器都是这样干的)。由于这种的原因,当采用CPP与C混合编程的时候,就可能会出问题。假设在某一个头文件中定义了这样一个函数:
int foo(int a, int b);
而这个函数的实现位于一个.c文件中,同时,在.cpp文件中调用了这个函数。那么,当CPP编译器编译这个函数的时候,就有可能会把这个函数名改成_fooii,这里的ii表示函数的第一参数和第二参数都是整型。而C编译器却有可能将这个函数名编译成_foo。也就是说,在CPP编译器得到的目标文件中,foo()函数是由_fooii符号来引用的,而在C编译器生成的目标文件中,foo()函数是由_foo指代的。但连接器工作的时候,它可不管上层采用的是什么语言,它只认目标文件中的符号。于是,连接器将会发现在.cpp中调用了foo()函数,但是在其它的目标文件中却找不到_fooii这个符号,于是提示连接过程出错。extern “C” {}这种语法形式就是用来解决这个问题的。本文将以示例对这个问题进行说明。
首先假设有下面这样三个文件:
在这个头文件中只定义了一个函数,ThisIsTest()。这个函数被定义为一个外部函数,可以被包括到其它程序文件中。假设ThisIsTest()函数的实现位于test_extern_c.c文件中:
/* test_extern_c.c */
#include "test_extern_c.h"
int ThisIsTest(int a, int b)
{
return (a + b);
}
可以看到,ThisIsTest()函数的实现非常简单,就是将两个参数的相加结果返回而已。现在,假设要从CPP中调用ThisIsTest()函数:
/* main.cpp */
#include "test_extern_c.h"
#include <stdio.h>
#include <stdlib.h>
class FOO {
public:
int bar(int a, int b)
{
printf("result=%i\n", ThisIsTest(a, b));
}
};
int main(int argc, char **argv)
{
int a = atoi(argv[1]);
int b = atoi(argv[2]);
FOO *foo = new FOO();
foo->bar(a, b);
return(0);
}
在这个CPP源文件中,定义了一个简单的类FOO,在其成员函数bar()中调用了ThisIsTest()函数。下面看一下如果采用gcc编译test_extern_c.c,而采用g++编译main.cpp并与test_extern_c.o连接会发生什么情况
[cyc@cyc src]$ gcc -c test_extern_c.c
[cyc@cyc src]$ g++ main.cpp test_extern_c.o
[cyc@cyc src]$ ./a.out 4 5
result=9
可以看到,程序没有任何异常,完全按照预期的方式工作。那么,如果将test_extern_c.h中的extern “C” {}所在的那几行注释掉会怎样呢?注释后的test_extern_c.h文件内容如下:
/* test_extern_c.h */
#ifndef __TEST_EXTERN_C_H__
#define __TEST_EXTERN_C_H__
//#ifdef __cplusplus
//extern "C" {
//#endif
/*
* this is a test function, which calculate
* the multiply of a and b.
*/
extern int ThisIsTest(int a, int b);
//#ifdef __cplusplus
// }
//#endif /* end of __cplusplus */
#endif
除此之外,其它文件不做任何的改变,仍然采用同样的方式编译test_extern_c.c和main.cpp文件:
[cyc@cyc src]$ gcc -c test_extern_c.c
[cyc@cyc src]$ g++ main.cpp test_extern_c.o
/tmp/cca4EtJJ.o(.gnu.linkonce.t._ZN3FOO3barEii+0x10): In function `FOO::bar(int, int)':
: undefined reference to `ThisIsTest(int, int)'
collect2: ld returned 1 exit status
在编译main.cpp的时候就会出错,连接器ld提示找不到对函数ThisIsTest()的引用。
为了更清楚地说明问题的原因,我们采用下面的方式先把目标文件编译出来,然后看目标文件中到底都有些什么符号:
[cyc@cyc src]$ gcc -c test_extern_c.c
[cyc@cyc src]$ objdump -t test_extern_c.o
test_extern_c.o: file format elf32-i386
SYMBOL TABLE:
00000000 l df *ABS* 00000000 test_extern_c.c
00000000 l d .text 00000000
00000000 l d .data 00000000
00000000 l d .bss 00000000
00000000 l d .comment 00000000
00000000 g F .text 0000000b ThisIsTest
[cyc@cyc src]$ g++ -c main.cpp
[cyc@cyc src]$ objdump -t main.o
main.o: file format elf32-i386
SYMBOL TABLE:
00000000 l df *ABS* 00000000 main.cpp
00000000 l d .text 00000000
00000000 l d .data 00000000
00000000 l d .bss 00000000
00000000 l d .rodata 00000000
00000000 l d .gnu.linkonce.t._ZN3FOO3barEii 00000000
00000000 l d .eh_frame 00000000
00000000 l d .comment 00000000
00000000 g F .text 00000081 main
00000000 *UND* 00000000 atoi
00000000 *UND* 00000000 _Znwj
00000000 *UND* 00000000 _ZdlPv
00000000 w F .gnu.linkonce.t._ZN3FOO3barEii 00000027 _ZN3FOO3barEii
00000000 *UND* 00000000 _Z10ThisIsTestii
00000000 *UND* 00000000 printf
00000000 *UND* 00000000 __gxx_personality_v0
可以看到,采用gcc编译了test_extern_c.c之后,在其目标文件test_extern_c.o中的有一个ThisIsTest符号,这个符号就是源文件中定义的ThisIsTest()函数了。而在采用g++编译了main.cpp之后,在其目标文件main.o中有一个_Z10ThisIsTestii符号,这个就是经过g++编译器“粉碎”过后的函数名。其最后的两个字符i就表示第一参数和第二参数都是整型。而为什么要加一个前缀_Z10我并不清楚,但这里并不影响我们的讨论,因此不去管它。显然,这就是原因的所在,其原理在本文开头已作了说明。
[cyc@cyc src]$ gcc -c test_extern_c.c
[cyc@cyc src]$ objdump -t test_extern_c.o
test_extern_c.o: file format elf32-i386
SYMBOL TABLE:
00000000 l df *ABS* 00000000 test_extern_c.c
00000000 l d .text 00000000
00000000 l d .data 00000000
00000000 l d .bss 00000000
00000000 l d .comment 00000000
00000000 g F .text 0000000b ThisIsTest
那么,为什么采用了extern “C” {}形式就不会有这个问题呢,我们就来看一下当test_extern_c.h采用extern “C” {}的形式时编译出来的目标文件中又有哪些符号:
[cyc@cyc src]$ g++ -c main.cpp
[cyc@cyc src]$ objdump -t main.o
main.o: file format elf32-i386
SYMBOL TABLE:
00000000 l df *ABS* 00000000 main.cpp
00000000 l d .text 00000000
00000000 l d .data 00000000
00000000 l d .bss 00000000
00000000 l d .rodata 00000000
00000000 l d .gnu.linkonce.t._ZN3FOO3barEii 00000000
00000000 l d .eh_frame 00000000
00000000 l d .comment 00000000
00000000 g F .text 00000081 main
00000000 *UND* 00000000 atoi
00000000 *UND* 00000000 _Znwj
00000000 *UND* 00000000 _ZdlPv
00000000 w F .gnu.linkonce.t._ZN3FOO3barEii 00000027 _ZN3FOO3barEii
00000000 *UND* 00000000 ThisIsTest
00000000 *UND* 00000000 printf
00000000 *UND* 00000000 __gxx_personality_v0
注意到这里和前面有什么不同没有,可以看到,在两个目标文件中,都有一个符号ThisIsTest,这个符号引用的就是ThisIsTest()函数了。显然,此时在两个目标文件中都存在同样的ThisIsTest符号,因此认为它们引用的实际上同一个函数,于是就将两个目标文件连接在一起,凡是出现程序代码段中有ThisIsTest符号的地方都用ThisIsTest()函数的实际地址代替。另外,还可以看到,仅仅被extern “C” {}包围起来的函数采用这样的目标符号形式,对于main.cpp中的FOO类的成员函数,在两种编译方式后的符号名都是经过“粉碎”了的。
因此,综合上面的分析,我们可以得出如下结论:采用extern “C” {} 这种形式的声明,可以使得CPP与C之间的接口具有互通性,不会由于语言内部的机制导致连接目标文件的时候出现错误。需要说明的是,上面只是根据我的试验结果而得出的结论。由于对于CPP用得不是很多,了解得也很少,因此对其内部处理机制并不是很清楚,如果需要深入了解这个问题的细节请参考相关资料。
备注:
1. 对于要在cpp中使用的在c文件中写好的函数func(),只需要在c文件的头文件中添加extern “C”声明就可以了。比如:extern “C” func() { …}
当然,可以使用
#ifdef __cplusplus
extern "C" {
#endif
和
#ifdef __cplusplus
}
#endif
将整个c文件的函数全都括起来
C++保留了一部分过程式语言的特点,因而它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同。
extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。
比如说你用C 开发了一个DLL 库,为了能够让C ++语言也能够调用你的DLL输出(Export)的函数,你需要用extern “C”来强制编译器不要修改你的函数名。
揭秘extern “C”
从标准头文件说起
#ifndef __INCvxWorksh /*防止该头文件被重复引用*/
#define __INCvxWorksh
#ifdef __cplusplus //__cplusplus是cpp中自定义的一个宏
extern "C" { //告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
#endif
/**** some declaration or so *****/
#ifdef __cplusplus
}
#endif
#endif /* __INCvxWorksh */
extern “C”的含义
extern “C” 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。
被extern “C”限定的函数或变量是extern类型的;
1、extern关键字
extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在链接阶段中从模块A编译生成的目标代码中找到此函数。
与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。
2、被extern “C”修饰的变量和函数是按照C语言方式编译和链接的
首先看看C++中对类似C的函数是怎样编译的。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。
_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。 例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。
同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以”.”来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
3、举例说明
(1)未加extern “C”声明时的连接方式
假设在C++中,模块A的头文件如下:
// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif
//在模块B中引用该函数:
// 模块B实现文件 moduleB.cpp
#include "moduleA.h"
foo(2,3);
实际上,在连接阶段,链接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!
(2)加extern “C”声明后的编译和链接方式
加extern “C”声明后,模块A的头文件变为:
// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo( int x, int y );
#endif
在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:
<1>A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;
<2>链接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。
如果在模块A中函数声明了foo为extern “C”类型,而模块B中包含的是extern int foo(int x, int y),则模块B找不到模块A中的函数;反之亦然。
extern “C”这个声明的真实目的是为了实现C++与C及其它语言的混合编程。
应用场合
C++代码调用C语言代码、在C++的头文件中使用
在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:
extern "C"
{
#include "cExample.h"
}
而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern “C”声明,在.c文件中包含了extern “C”时会出现编译语法错误
/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y); //注:写成extern "C" int add(int , int ); 也可以
#endif
/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
return x + y;
}
// c++实现文件,调用add:cppFile.cpp
extern "C"
{
#include "cExample.h" //注:此处不妥,如果这样编译通不过,换成 extern "C" int add(int , int ); 可以通过
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern “C”{}。
在C中引用C++语言中的函数和变量时,C++的头文件需添加extern “C”,但是在C语言中不能直接引用声明了extern “C”的该头文件,应该仅将C文件中将C++中定义的extern “C”函数声明为extern类型
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}
===================================================================
所谓名字修饰约定,就是指变量名、函数名等经过编译后重新输出名称的规则。
比如源代码中函数名称为int Func(int a,int b),经过编译后名称可能为?Func@@YAHHH@Z、?Func@@YGHHH@Z、_Func@8,也有可能与源代码中名称相同为Func。
影响编译后输出的名称通常与名字修饰约定(extern "C"、extern "C++"等)和函数调用约定(__stdcall、__cdecl等)等相关。
首先,用C方式导出两个函数:
Dll1.c
_declspec(dllexport) int __cdecl Func_cdecl(int a,int b){ return 1;}
_declspec(dllexport) int __stdcall Func_stdcall(int a,int b){ return 1;}
导出的两个函数名为:
再以C++方式导出:
Dll1.cpp
_declspec(dllexport) int __cdecl Func_cdecl(int a,int b){ return 1;}
_declspec(dllexport) int __stdcall Func_stdcall(int a,int b){ return 1;}
导出的两个函数名为:
然后,我们再以C++方式导出如下代码中的函数:
extern "C" _declspec(dllexport) int __stdcall Func_C_stdcall(int a,int b){ return 1;}
extern "C++" _declspec(dllexport) int __stdcall Func_CPP_stdcall(int a,int b){ return 1;}
extern "C" _declspec(dllexport) int __cdecl Func_C_cdecl(int a,int b){ return 1;}
extern "C++" _declspec(dllexport) int __cdecl Func_CPP_cdecl(int a,int b){ return 1;}
导出结果如下:
有了以上实验结果,我们再结合以下名字输出规则进行理解:
C方式编译(extern "C"):
__stdcall调用约定:输出名称在原名称前加一下划线,后面再加上一个“@”和其参数的总字节数(_原名称@参数总字节数),如名称int
Func_C_stdcall(int a,int b)输出为_Func_C_stdcall@8;
__cdecl调用约定:与原名称相同,如名称int Func_C_cdecl(int a,int b)输出还是为Func_C_cdecl;
C++方式编译(extern "C++"):
__stdcall调用约定:
输出名称以“?”开始,后跟原名称;原名称后再跟“@@YG”,后面再跟返回值代号和参数表代号,代号表示如下:
- X--void ,
- D--char,
- E--unsigned char,
- F--short,
- H--int,
- I--unsigned int,
- J--long,
- K--unsigned long,
- M--float,
- N--double,
- _N--bool,
- ...
PA--表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0”代替,一个“0”代表一次重复;参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。如名称int Func_CPP_stdcall(int a,int b)编译后的输出名称为?Func_CPP_stdcall@@YGHHH@Z。__cdecl调用约定:与_stdcall调用约定基本一致,只是参数表的开始标识由上面的“@@YG”变为“@@YA”。如名称int
Func_CPP_cdecl(int a,int b)编译后输出名称为?Func_CPP_cdecl@@YAHHH@Z。
有个这个规则,再回头去看我们的实验结果,就很好理解了。
当然,编译C文件和编译CPP文件,不需加extern"C"和extern "C++",
因为编译C文件当然默认的是extern "C",而编译CPP文件则默认的是extern"C++"。
现在我们也能理解为什么导出DLL时通常需要加上extern"C"。试想,如果一个C++导出的dll,没有加extern "C",则导出的名称为extern "C++"约定下的名称。如果这个dll需要提供给用C编写的程序使用,那么这个程序是无法调用这个dll的,因为C写的程序遵循的是extern "C"约定,链接时链接器将按照extern "C"约定的名称去寻找外部名称,这当然找不到,因为dll中的输出名称为extern "C++"约定下的名称。
================================================================
就以为是约定俗成,其实也算是约定俗成,这样做的目的是为了防止符号名冲突,因为在一个程序中往往是包含汇编和C文件的,汇编用于启动部分,C文件用于应用程序,最终通过编译器实现编译,对于编译器来说,汇编和C是一视同仁的,那么就会有个问题,如果在汇编和C文件中使用了同一个名字,这是很可能出现的,毕竟汇编相当于机器码也算是稍微高级的语言,在定义子程序或函数时,也是可以用英文拼写的,而C文件中,更会习惯用英文拼写。
所以为了防止类似的符号名冲突,UNIX下的C语言就规定,C语言的源代码文件中的所有全局变量和函数经过编译后,相应的符号名前面会自动的加上下划线“_”。这样做的好处,就是方便是程序开发人员,不用太小心翼翼的起名,避免了与汇编文件中的符号名的冲突。
==================================================================
有关DLL的问题现在资料很多,但是很多人写DLL时经常出现调用程序无法找到相关的导出函数的问题,这里主要的原因是DLL在声明时出的问题。
在这里主要有两个问题,一个是调用约定的问题,一个是函数名修饰的问题,而这两个问题又是相互影响的。
一:声明为:extern "C" int __declspec(dllexport)add(int x, int y);
这种声明是强制用C语言方式进行修饰,且用C的默认约定,即__cdecl方式。这种方式编译产生的DLL中有一个导出函数:add,不加任何修饰。
二:声明为:extern "C" int __declspec(dllexport) __stdcall add(int x, int y);
这种声明是强制用C语言方式进行修饰,且用stdcall约定,这种方式编译产生的DLL中有一个导出函数:_add@8,即前面有“_”,后面加了参数长。
三:声明为:int __declspec(dllexport) __stdcall add(int x, int y);
这种声明不强制用C语言方式进行修饰,但是用stdcall约定,这种方式编译产生的DLL中有一个导出函数:?add@@YGHHH@Z。这个名字很怪,后面的不好理解。
四:声明为:int __declspec(dllexport) __cdecl add(int x, int y);
这种声明是不强制用C语言修饰,且用cdecl约定,这种方式编译产生的DLL中有一个导出函数:?add@@YAHHH@Z,注意看,和第三种方有一点不同。
实验一:显式调用方式调用DLL中的add函数。
#include <stdio.h>
#include <windows.h>
typedef int(_stdcall *lpAddFun)(int, int); //宏定义函数指针类型
int main(int argc, char *argv[])
{
HINSTANCE hDll; //DLL句柄
lpAddFun addFun; //函数指针
hDll = LoadLibrary("1.dll");
if (hDll != NULL)
{
addFun = (lpAddFun)GetProcAddress(hDll, "add");
if (addFun != NULL)
{
int result = addFun(2, 3);
printf("%d", result);
}
else
printf("No Function");
}
else
printf("NO DLL");
FreeLibrary(hDll);
return 0;
}
方式一:调用成功。另外三种方式全部出错
实验二:隐式调用DLL中的add函数
#include <stdio.h>
#include <windows.h>
#pragma comment(lib,"1.lib")
extern "C" int __declspec(dllimport) add(int x, int y);//声明方式随着DLL中的声明方式改变
int main(int argc, char *argv[])
{
int result = add(2, 3);
printf("%d", result);
return 0;
}
方式一:调用成功。另外发现一个奇怪现象:在调用程序中
声明函数时extern "C" int __declspec(dllimport) add(int x, int y);
写作:extern "C" int __declspec(dllecprot) add(int x, int y);同样成功,将__declspec(…)去掉也同样成功。换句话说,在调用DLL的程序中,导入是没有必要加的。
方式二:调用成功。同样出现上面导入标识可以不加的现象。
方式三:调用成功,同样也出现上面导入标识可以不加的现象。
方式四:调用成功,同样也出现上面导入标识可以不加的现象。
总结:对于DLL导出函数声明的四种写法,在动态调用时,
声明成这样:extern "C" int __declspec(dllimport) add(int x, int y);是最好的,其它声明方式调用都没有成功。但是众所周知,windows默认的调用约定是stdcall方式,如果想别的语言能用DLL的话,最好是将调用约定写成stdcall方式,但是这种方式又不能动态调用。
在隐式调用时,四种声明方式都是可以的,只要调用者的声明方式和DLL声明时的方式一致即可。另外,在调用程序中对于导入的声明是可以去掉的,大量书籍中关于导入、导出的问题都是利用宏来处理的,如:在头文件中写作:
#ifdef DLL_FILE
extern "C" int __declspec(dllexport) add(int x, int y);
#else
extern "C" int __declspec(dlleximport) add(int x, int y);
这样这个头文件既可以用在DLL工程中,又可以用在调用程序中,但是经过实验发现,这个根本就没有必要,在调用者程序中不管是写作__declspce(dllexport)还是写作__declspec(dllimport)或者不写都能成功调用。
关于DEF文件
在DLL工程中引用DEF文件,内容如下:
LIBRARY 1
EXPORTS
add @ 1
通过depends查看导出函数全是add,但是隐式方式调用时,还是要求调用者的声明方式和DLL中声明方式相同。
对于动态调用实验结果:
方式一:成功。方式二:不成功,但是将函数指针改为typedef int(_stdcall *lpAddFun)(int, int);成功,即调用者要声明约定方式与DLL中声明的调用约定方式相同,否则报错。
方式三:同方式二,同样要将函数指针改为typedef int(_stdcall *lpAddFun)(int, int);才成功完成调用。
方式四:成功。
总结:通过DEF文件来导出函数,调用者同样也要声明相同的调用约定,即_stdcall或是_cdecl必须要相同,其中_cdecl是C语言默认方式。
=================================================================
__imp,说明那不是真正的静态库,而是某个动态库的导入库
导入函数和函数自己不同名,所以加__imp
比如__imp_printf忘了有没有后缀了,就是 printf函数
所谓名字修饰约定,就是指变量名、函数名等经过编译后重新输出名称的规则。
比如源代码中函数名称为int Func(int a,int b),经过编译后名称可能为?Func@@YAHHH@Z、?Func@@YGHHH@Z、_Func@8,也有可能与源代码中名称相同为Func。
影响编译后输出的名称通常与名字修饰约定(extern "C"、extern "C++"等)和函数调用约定(__stdcall、__cdecl等)等相关。
口说千遍,不如实际演练一遍。那么,就让我们写代码来测试下。
注意,本文只讨论extern
extern "C"、extern "C++" 和__stdcall、__cdecl相关的约定,其他约定不在本文讨论范围内。
另外,编译的环境为WIN10 + VC2013
extern "C"+__cdecl
gpleadrailcontrol.h
#ifndef _GP_LEAD_RAIL_CONTROL_H_
#define _GP_LEAD_RAIL_CONTROL_H_
#ifdef GPLEADRAILCONTROL_EXPORTS
# define CTRL_API __declspec(dllexport)
#else
# define CTRL_API __declspec(dllimport)
#endif
#ifdef __cplusplus
# define MODBUS_BEGIN_DECLS extern "C" {
# define MODBUS_END_DECLS }
#else
# define MODBUS_BEGIN_DECLS
# define MODBUS_END_DECLS
#endif
MODBUS_BEGIN_DECLS
#define GP_LR_RET_SUCCESS 0 // 返回成功
#define GP_LR_RET_ERROR -1 // 返回错误
#define GP_LR_SET_SUCCESS 1 // 导轨控制设置成功
#define GP_LR_SET_FAILED 2 // 导轨控制设置失败
typedef struct _GP_LR_MOVE_DATA_
{
char chDirection; // 方向
int nSpeed; // 速度等级
int nAcceleration; // 加速度等级
int nPulseCount; // 脉冲数
}GP_LR_MOVE_DATA;
int CTRL_API GP_LR_Init(char* pComNameOrIpAddr, int nBaudRateOrPort);
void CTRL_API GP_LR_Deinit();
int CTRL_API GP_LR_Open(int nDevIndex);
int CTRL_API GP_LR_Close(int nDevIndex);
int CTRL_API GP_LR_Home(int nDevIndex);
int CTRL_API GP_LR_Move(int nDevIndex, GP_LR_MOVE_DATA* pMoveData);
int CTRL_API GP_LR_Stop(int nDevIndex);
int CTRL_API GP_LR_SetSpeed(int nDevIndex,int nSpeed);
int CTRL_API GP_LR_SetAcceleration(int nDevIndex, int nAcceleration);
int CTRL_API GP_LR_GetInfo(int nDevIndex, GP_LR_MOVE_DATA* pstLrPositionInfo);
MODBUS_END_DECLS
#endif // _GP_LEAD_RAIL_CONTROL_H_
gpleadrailcontrol.cpp
// gpleadrailcontrol.cpp : Defines the exported functions for the DLL application.
#include "gpleadrailcontrol.h"
#include "LeadRailCtrl.h"
#include "singleton_instance.hpp"
extern "C"
{
int CTRL_API GP_LR_Init(char* pComNameOrIpAddr, int nBaudRateOrPort)
{
int nRet = base::singleton_instance<LeadRailCtrl>::instance()->Init(pComNameOrIpAddr, nBaudRateOrPort);
return nRet;
}
void CTRL_API GP_LR_Deinit()
{
base::singleton_instance<LeadRailCtrl>::instance()->DeInit();
}
int CTRL_API GP_LR_Open(int nDevIndex)
{
int nRet = base::singleton_instance<LeadRailCtrl>::instance()->OpenLR(nDevIndex);
return nRet;
}
int CTRL_API GP_LR_Close(int nDevIndex)
{
int nRet = base::singleton_instance<LeadRailCtrl>::instance()->CloseLR(nDevIndex);
return nRet;
}
int CTRL_API GP_LR_Home(int nDevIndex)
{
GP_LR_MOVE_DATA stLrMoveData = { 0 };
int nRet = base::singleton_instance<LeadRailCtrl>::instance()->HomeLR(nDevIndex);
return nRet;
}
int CTRL_API GP_LR_Move(int nDevIndex, GP_LR_MOVE_DATA* pMoveData)
{
int nRet = base::singleton_instance<LeadRailCtrl>::instance()->MoveLR(nDevIndex, pMoveData);
return nRet;
}
int CTRL_API GP_LR_Stop(int nDevIndex)
{
int nRet = base::singleton_instance<LeadRailCtrl>::instance()->StopLR(nDevIndex);
return nRet;
}
int CTRL_API GP_LR_SetSpeed(int nDevIndex, int nSpeed)
{
int nRet = base::singleton_instance<LeadRailCtrl>::instance()->SetSpeed(nDevIndex, nSpeed);
return nRet;
}
int CTRL_API GP_LR_SetAcceleration(int nDevIndex, int nAcceleration)
{
int nRet = base::singleton_instance<LeadRailCtrl>::instance()->SetAcceleration(nDevIndex, nAcceleration);
return nRet;
}
int CTRL_API GP_LR_GetInfo(int nDevIndex, GP_LR_MOVE_DATA* pstLrPositionInfo)
{
int nRet = base::singleton_instance<LeadRailCtrl>::instance()->GetCurrentInfo(nDevIndex, pstLrPositionInfo);
return nRet;
}
}
结果gpleadrailcontrol.dll的导入库gpleadrailcontrol.lib
二、extern "C"+__stdcall
如果屏蔽头文件中的//MODBUS_BEGIN_DECLS //MODBUS_END_DECLS编译会报错
__cdecl (/Gd):?xxxx@@YAHPADH@Z 同 代码中有extern"C++" +__cdecl (/Gd)
__stdcall (/Gz):?xxxx@@YGHPADH@Z 同 代码中有extern"C++" +__cdecl (/Gd)
__fastcall (/Gr):?xxxx@@YIHPADH@Z 同 代码中有extern"C++" +__cdecl (/Gd)
__vectorcall (/Gv):?xxxx@@YQHPADH@Z 同 代码中有extern"C++" +__cdecl (/Gd)
代码有extern"C" + __cdecl (/Gd):_xxxx
代码有extern"C" __stdcall (/Gz):_xxxx@8
代码有extern"C" __fastcall (/Gr):@xxxx@8
代码有extern"C" __vectorcall (/Gv):xxxx@@8
标签:__,函数,00000000,约定,C++,int,extern From: https://blog.51cto.com/u_16081664/6224235