关于GetProcAddress返回错误代码为127的解决方案

关于GetProcAddress返回错误代码为127的解决方案

关于GetProcAddress返回错误代码为127的解决方案

运行期间显示地去加载DLL库,当LoadLibrary可以返回当前DLL模块的句柄时,而GetProcAddress取得函数的地址为空时。通过GetLastError去获取错误代码为127,很有可能是动态链接库工程里面函数声明的没有加extern “C”。

解决方案

针对自己写的dll项目提供解决方案
如果是拿的第三方的动态链接库出这样的问题,不在此解决方案中
通常写dll工程的时候,头文件都会有类似与这样的

#ifndef PCH_H
#define PCH_H
#else
#endif //PCH_H

当我们想要显示的导出自己的函数在其他工程中使用时,需要使用extern "C"去导出自己的函数。
关于为什么要用extern“C”,貌似是C++方面没什么标准,不同的编译器可能产生不同的修饰
加入了extren “C”,不会有额外的修饰,这样就统一了。
在头文件应该这样写

#ifndef PCH_H
#define PCH_H_API extern "C" __declspec(dllexport)
#else
#define PCH_H_API __declspec(dllimport)
#endif //PCH_H

PCH_H_API void ExportFunc(LPCTSTR pszContent);//这里就是需要导出的函数,增加了extern "C"的修饰

这里用到了简单的宏,不然的话,全部写出还是有点点多的

extern "C" {
	__declspec(dllexport) void ExportFunc(LPCTSTR pszContent);
} //这个代码块等同于上面的PCH_H_API void ExportFunc(LPCTSTR pszContent)

然后在另一个项目中测试

int main()
{
	HMODULE hModule = ::LoadLibrary("..\\DllTest\\Debug\\DllTest.dll");
	if (hModule != NULL)
	{
		PFNEXPORTFUNC mExportFunc = (PFNEXPORTFUNC)::GetProcAddress(hModule, "ExportFunc");
		int n = GetLastError();
		if (mExportFunc != NULL)
		{
			mExportFunc("大家好");
		}

		::FreeLibrary(hModule);
	}
	return 0;
}

这样GetLastError()就不会返回127,可以调用dll中的mExportFunc函数了。以上皆是主要代码,部分其他的代码如Dll中mExportFunc实现的代码没有列出。

extern关键字的使用

C++ 为了与C兼容,C++可以使用关键字extern “C”来声明或者定义一个C符号:

extern "C"{
    int func(int);
    int var;
}

C++ 会将extern “C”的大括号内的代码当做C语言代码(.c文件)处理;
亦即在大括号中C++ 的名称修饰机制将不起作用,不同的编译器有不同的名称修饰方法:

1.如果是VC++平台,上述代码中的fun和var会被修饰(名称修饰)为_func和_var;
2.但是如果是linux下的gcc编译器,前面的_也会被去掉,extern “C”里面的符号就是修饰后符号(为func和var)。

【补充】
gcc编译.c文件,因为是按照c方式编译,所以函数名不变;gcc编译.cpp文件,g++ 编译.c文件,g++ 编译.cpp文件,因为是按照c++ 方式编译,所以函数名才会加上了附加信息。(当前编译器编译出来的目标文件和库文件(目标文件)应该是同样的编译器编译出来的)
ps:
单独声明某个函数或者是变量是C的符号,可以直接
extern “C” int func(int);

extern “C”带来的问题
  经常遇到的情况:有些头文件声明了一些C语言的函数和变量,但是这个头文件可能被C语言或者是C++ 语言包含。例如:
对于函数 void *memset (void *, int, size_t) 而言,如果不使用extern “C”,考虑一以下两种情况:

1.在C语言程序包含string.h的时候,如果用到了memset这个函数,编译器会进行正确的函数名称修饰_memset(C语言的名称修饰,前面加_),也可以正确的连接到C语言库中的memset符号。
2.在C++ 程序中包含了memset函数,编译器编译器会认为这是一个C++ 函数,函数名就会被修饰为_Z6memsetPvii(C++的名称修饰可自行了解),这样的话,链接器就没办法在C语言的库中找到对应的符号地址。此时就必须使用extern “C”。

符号修饰和函数签名

C++符号修饰
函数签名包含一个函数的信息,包括函数名、它的参数类型、它所在的类和名称(命名)空间及其他信息。
如例子所示:

int func(int);
float func(float);
 
class C {
    
    
    int func(int);
    class C2 {
    
    
        int func(int);
    }
};
 
namespace N {
    
    
    int func(int);
    class C {
    
    
        int func(int);
    };
};

针对如上代码,有6个func。
上面6个函数在GCC编译器下,相应的修饰后名称如下:

image

GCC的基本C++ 名称修饰方法如下:所有的符号都以"_Z"开头,对于嵌套的名字(在名称空间或在类里面的),后面紧跟"N",然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以"E"结尾。比如N::C::func经过名称修饰以后就是_ZN1N1C4funcE。对于一个函数来说,它的参数列表紧跟在"E"后面,对于int类型来说,就是字母"i"。所以整个N::C::func(int)函数签名经过修饰为_ZN1N1C4funcEi。

签名和名称修饰机制不光被使用到函数上, C++中的全局变量和静态变量也有同样的机制。对于全局变量来说,它跟函数一样都是一个全局可见的名称,它也遵循上面的名称修饰机制,比如一个名称空间foo中的全局变量bar,它修饰后的名字为:_ZN3foo3barE。 值得注意的是,变量的类型并没有被加入到修饰后名称中,所以不论这个变量是整形还是浮点型甚至是一个全局对象,它的名称都是一样的。
名称修饰机制也被用来防止静态变量的名字冲突。比如main()函数里面有一个静态变量叫foo,而func()函数里面也有一个静态变量叫foo。为了区分这两个变量,GCC会将它们的符号名分别修饰成两个不同的名字_ZZ4mainE3foo和_ZZ4funcvE3foo,这样就区分了这两个变量。

C与C++ 的兼容问题的实现

方法是使用C++ 的宏__cplusplus

#ifdef __cplusplus
extern "C" {
#endif
// 代码
#ifdef __cplusplus
}
#endif

含义是,如果是C++代码,那么memset会在extern “C”中被声明,
如果是C代码就直接声明。

关于extern在函数指针使用情况下的使用举例
一个例子:
(1)工程下面 global.h 中

...
typedef void(*xivc_picture_data_t)(img_params *img);
...
extern xivc_picture_data_t decode_image;  // important
...

(2)工程下面 ldecode.c 中

#include "global.h"
...
xivc_picture_data_t decode_image;  // important
...
decode_image = picture_decode_mt;
...

(3)工程下面 image.c 中方可使用decode_image

#include "global.h"
...
decode_image(img);
...

等价如下
【ldecode.c】
typedef void(*xivc_picture_data_t)(img_params *img);
...
xivc_picture_data_t decode_image;  // important
...
decode_image = picture_decode_mt;
...
【image.c】
extern xivc_picture_data_t decode_image;  // important
...
decode_image(img);
...

记录

上述情况下的extern的使用是基本的使用方式
在头文件中:

extern int g_Int;

它的作用就是声明函数或全局变量的作用范围的关键字,其声明的函数和变量可以在本模块或其他模块中使用,记住它是一个声明而不是定义
比如,在源文件A里定义的函数,在其它源文件里是看不见的(即不能访问)。为了在源文件B里能调用这个函数,应该在B的头部加上一个外部声明:
extern 函数原型;
这样,在源文件B里也可以调用那个函数了。注意这里的用词区别:
在A里是定义,在B里是声明。
一个函数只能(也必须)在一个源文件里被定义,但是可以在其它多个源文件里被声明。
定义引起存储分配,是真正产生那个实体。而声明并不引起存储分配
打一个粗俗的比方:在源文件B里声明后,好比在B里开了一扇窗,让它可以看到A里的那个函数。

基本规则

extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明。

注意

另外一种方法是在公共的*.h文件中使用全局变量,两个*.c文件同时include该.c文件,就可以避免使用extern,也可以做到函数的更新同步。上述例子中,直接在global.h文件中使用 xivc_picture_data_t decode_image; 即可。但是,上述方法可能会引起重复定义的问题,最好的方法还是使用extern,养成好的习惯。

补充

在一个.c文件的最前面使用extern int f(); 和int f();是等效的,
都说明了f函数可能在其他函数中定义,而在当前函数中只是声明一下有待使用。但是最好还是使用extern关键字,避免误解。

猜你喜欢

转载自blog.csdn.net/u010523811/article/details/124603624
今日推荐