C动态库的编写及调用

GCC常用编译选项

1、总体编译选项

选项名称 作用
-c 只是编译不链接,生成目标文件.o
-S 只是编译不汇编,生成汇编代码
-E 只进行预编译,不做其他处理
-g 在可执行程序中包含标准调试信息
-o file 把输出文件输出到file里
-v 打印出编译器内部编译各过程的命令行信息和编译器的版本
-I dir 在头文件的搜索路径列表中添加dir目录
-L dir 在库文件的搜索路径列表中添加dir目录
-static 链接静态库
-llibrary 连接名为library的库文件

2、其他常用编译选项

选项名称 作用
-ansi 支持符合ANSI标准的C程序
-pedantic 允许发出ANSI C标准所列的全部警告信息
-pedantic-error 允许发出ANSI C标准所列的全部错误信息
-Wall 允许发出gcc提供的所有有用的报警信息
-Werror 把所有的告警信息转化为错误信息,并在告警发生时终止编译过程
-O 主要进行线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化
-O2 除了完成所有“-O1”级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等
-O3 还包括循环展开和其他一些与处理器特性相关的优化工作
-static 指示链接器构建一个完全链接的可执行程序,即链接静态库而不链接动态库
-fPIC 指示链接器创建一个共享的目标文件,即so文件
-shared 生成动态库,一般和上面的-fPIC一起使用

编写DLL

extern “c”

__stdcall的调用方式就会在原来函数名上加符号,而extern “c” __cdecl则不会附加额外的符号。

如果导出函数使用了extern”C” __cdecl,那么就不需要再重命名了,这个时候dll里的名字就是原始名字;如果使用了extern”C” __stdcall,这时候dll中的函数名被修饰了,就需要重命名。重命名的方式有两种,要么使用*.def文件,在文件外修正,要么使用#pragma,在代码里给函数别名。

__declspec(dllexport)和__declspec(dllimport)的作用

__declspec还有另外的用途,这里只讨论跟dll相关的使用。正如括号里的关键字一样,导出和导入。__declspec(dllexport)用在dll上,用于说明这是导出的函数。而__declspec(dllimport)用在调用dll的程序中,用于说明这是从dll中导入的函数。

因为dll中必须说明函数要用于导出,所以__declspec(dllexport)很有必要。但是可以换一种方式,可以使用def文件来说明哪些函数用于导出,同时def文件里边还有函数的编号。

而使用__declspec(dllimport)却不是必须的,但是建议这么做。因为如果不用__declspec(dllimport)来说明该函数是从dll导入的,那么编译器就不知道这个函数到底在哪里,生成的exe里会有一个call XX的指令,这个XX是一个常数地址,XX地址处是一个jmp dword ptr[XXX]的指令,跳转到该函数的函数体处,显然这样就无缘无故多了一次中间的跳转。如果使用了__declspec(dllimport)来说明,那么就直接产生call dword ptr[XXX],这样就不会有多余的跳转了。

__stdcall带来的影响

这是一种函数的调用约定方式,压栈方式和最后的清栈的方式不同,__stdcall
是函数自己处理栈__cdecl是函数调用者清理。默认情况下VC使用的是__cdecl的函数调用方式,如果产生的dll只会给C/C++程序使用,那么就没必要定义为__stdcall调用方式,如果要给Win32汇编使用(或者其他的__stdcall调用方式的程序),那么就可以使用__stdcall。这个可能不是很重要,因为可以自己在调用函数的时候设置函数调用的规则。像VC就可以设置函数的调用方式,所以可以方便的使用win32汇编产生的dll。不过__stdcall这调用约定会Name-Mangling,所以我觉得用VC默认的调用约定简便些。但是,如果既要__stdcall调用约定,又要函数名不给修饰,那可以使用*.def文件,或者在代码里#pragma的方式给函数提供别名(这种方式需要知道修饰后的函数名是什么)。

举例:

extern “C” __declspec(dllexport) bool __stdcall cswuyg();

extern “C” __declspec(dllimport) bool __stdcall cswuyg();

#pragma comment(linker,"/export:cswuyg=_cswuyg@0")

*.def文件

指定导出函数,并告知编译器不要以修饰后的函数名作为导出函数名,而以指定的函数名导出函数(比如有函数func,让编译器处理后函数名仍为func)。这样,就可以避免由于microsoft VC++编译器的独特处理方式而引起的链接错误。

也就是说,使用了def文件,那就不需要extern “C”了,也可以不需要__declspec(dllexport)了(不过,dll的制造者除了提供dll之外,还要提供头文件,需要在头文件里加上这extern”C”和调用约定,因为使用者需要跟制造者遵守同样的规则)。

举例def文件格式:

LIBRARY XX(dll名称这个并不是必须的,但必须确保跟生成的dll名称一样)

EXPORTS

[函数名] @ [函数序号]

编写好之后加入到VC的项目中,就可以了。

​ 另外,要注意的是,如果要使用__stdcall,那么就必须在代码里使用上__stdcall,因为*.def文件只负责修改函数名称,不负责调用约定。

也就是说,def文件只管函数名,不管函数平衡堆栈的方式。

如何把*.def文件加入到工程之后,链接的时候并没有自动把它加进去。那么可以这样做:

手动的在link添加:

1)工程的properties Configuration Properties Linker Command Line在“Additional options”里加上:/def:[完整文件名].def

2)工程的properties Configuration Properties Linker Input Module Definition File里加上[完整文件名].def

DllMain函数

每一个动态链接库都会有一个DllMain函数。如果在编程的时候没有定义DllMain函数,那么编译器会给你加上去。

DllMain函数格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
printf("\nprocess attach of dll");
break;
case DLL_THREAD_ATTACH:
printf("\nthread attach of dll");
break;
case DLL_THREAD_DETACH:
printf("\nthread detach of dll");
break;
case DLL_PROCESS_DETACH:
printf("\nprocess detach of dll");
break;
}
return TRUE;
}

编写dll可以使用.def文件对导出的函数名进行命名。

实例DLL

C

1.头文件 test.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#ifndef TEST_H
#define TEST_H

//#define WIN
//#define TEST_EXPORTS
#ifdef WIN
#ifdef TEST_EXPORTS
#define EX_FUN __declspec(dllexport)
#else
#define EX_FUN __declspec(dllimport)
#endif
#define CALL_METHOD __stdcall //__cdecl
#define CALLBACK __stdcall
#else
#define EX_FUN extern "C"
#define CALL_METHOD
#define CALLBACK
#endif //linux

#ifdef __cplusplus
extern "C" {
#endif

EX_FUN void CALL_METHOD test1();
EX_FUN void CALL_METHOD test2();

#ifdef __cplusplus
}
#endif
#endif //TEST_H

2.实现文件 test.cpp

1
2
3
4
5
6
7
8
9
10
11
#include "test.h"
#include <stdio.h>
EX_FUN void CALL_METHOD test1()
{
printf("test1\n");
}

EX_FUN void CALL_METHOD test2()
{
printf("test2\n");
}

3.调用dllmain.h

1
2
3
4
5
6
7
8
#define TEST_EXPORTS
#include "test.h"

void main()
{
test1();
test2();
}

C++

1.头文件 testClass.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef TESTCLASS_H  
#define TESTCLASS_H

#ifdef TEST_CLASS
#define EX_CLASS _declspec(dllexport)
#else
#define EX_CLASS _declspec(dllimport)
#endif
#endif

class EX_CLASS test
{
public:
cls(int i,int j);
int add();
private:
int m;
int n;
};

2.实现文件 testClass.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include "testClass.h"  

test::test(int i,int j)
{
m = i;
n = j;
}

int test::add()
{
return m+n;
}

3.调用dllmain.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#define TEST_CLASS 
#include "testClass.h"
#pragma comment(lib,"DLL.lib")
#include <iostream>
using namespace std;

void main()
{
test *clsObj = new test(2,3);
int num = clsObj->add();
cout<<"result : "<<num<<endl;
system("pause");
}

Linux 动态库和静态库的生成

参考:http://www.latelee.org/programming-under-linux/library-on-linux.html

1.示例代码

1
2
3
4
5
/* lib.h */
#ifndef LIB_H_
#define LIB_H_
void test(int i);
#endif
1
2
3
4
5
6
7
/*lib.c*/
#include <stdio.h>
void test(int i)
{
printf(“hell from %s,num:%dn”, __func__, i);
getchar();
}
1
2
3
4
5
6
7
8
9
/*main.c*/
#include <stdio.h>
#include "lib.h"
int main(void)
{
printf(“hello from %s:n”, __func__);
test(250);
return 0;
}

2.生成静态库

1
2
3
4
5
6
#生成静态库
gcc -c lib.c
ar cr libtest.a lib.o

#查看静态库中的符号
nm libtest.a
1
2
3
4
5
6
7
8
#使用
#测试程序链接到静态库
gcc -o main main.c ./libtest.a
./main

#还可以这么写
gcc -o main main.c -L. -ltest
./main

3.生成动态库

1
2
3
#生成动态库
gcc -c lib.c
gcc -fPIC -shared -o libtest.so lib.o
1
2
3
4
5
6
7
#使用
gcc -o main main.c ./libtest.so
./main

#还可以这么写
gcc -o main main.c -L. -ltest
./main

4.库环境的配置

在/etc/profile中添加库的路径

1
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH