背景:编写一个模块的动态库,提供了必要的头文件和demo。
思考:首先弄清楚,要编写的库对外及提供几个接口。读懂相关头文件的结构体和自己写的库要用的函数。
在实际项目开发当中,有很多的库文件,尤其是在不同企业之间防止源码的公开,都是以库的形式提供给对方,然后提供必要的头文件。为了进一步加强源码的保密性,这种情况也普遍存在于同一个公司的不同项目组之间,将各个项目组,各个开发人员独立开来,级别低的开发者就接触不到公司核心源码,掌握公司的业务全貌。
本文介绍了,函数注册与回调机制,使用Makefile编译制作动态链接库,以及使用Makefile调用动态库。
“回调-注册”机制的目的可以实现模块化编程的低耦合的目标,利于项目的开发和维护。
在查资料时,找到一篇对 “注册函数与回调模型” 解释的很好的文章:回调函数(callback)是什么? - 知乎 (zhihu.com)
什么是回调函数?
我们绕点远路来回答这个问题。
编程分为两类:系统编程(system programming)和应用编程(application programming)。所谓系统编程,简单来说,就是编写库;而应用编程就是利用写好的各种库来编写具某种功用的程序,也就是应用。系统程序员会给自己写的库留下一些接口,即API(application programming interface,应用编程接口),以供应用程序员使用。所以在抽象层的图示里,库位于应用的底下。
当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。
打个比方,有一家旅馆提供叫醒服务,但是要求旅客自己决定叫醒的方法。可以是打客房电话,也可以是派服务员去敲门,睡得死怕耽误事的,还可以要求往自己头上浇盆水。这里,“叫醒”这个行为是旅馆提供的,相当于库函数,但是叫醒的方式是由旅客决定并告诉旅馆的,也就是回调函数(注意库函数和回调函数不是一回事儿,标注)。而旅客告诉旅馆怎么叫醒自己的动作,也就是把回调函数传入库函数的动作,称为登记回调函数(to register a callback function)。如下图所示(图片来源:维基百科):
(通过上图也可以看出,回调函数-callback function 和 Main函数是同一层,也就是回调函数和main 函数在一个 .c文件里,标注)
可以看到,回调函数通常和应用处于同一抽象层(因为传入什么样的回调函数是在应用级别决定的)。而回调就成了一个高层调用底层,底层再回过头来调用高层的过程。(我认为)这应该是回调最早的应用之处,也是其得名如此的原因。
回调机制的优势
从上面的例子可以看出,回调机制提供了非常大的灵活性。请注意,从现在开始,我们把图中的库函数改称为中间函数了,这是因为回调并不仅仅用在应用和库之间。任何时候,只要想获得类似于上面情况的灵活性,都可以利用回调。
这种灵活性是怎么实现的呢?乍看起来,回调似乎只是函数间的调用,但仔细一琢磨,可以发现两者之间的一个关键的不同:在回调中,我们利用某种方式,把回调函数像参数一样传入中间函数。可以这么理解,在传入一个回调函数之前,中间函数是不完整的。换句话说,程序可以在运行时,通过登记不同的回调函数,来决定、改变中间函数的行为。这就比简单的函数调用要灵活太多了。
好了现在开始编写:
记录-Linux-C动态库的编译与调用。将实现某部分功能的代码封装成库文件,以方便调用,或者是对代码进行保护加密。应用场景:有时想将某代码提供给别人用,但是又不想公开源代码,这时可以将代码封装成库文件。在开发中调用其他人员编写的库。
动态库特点:
1,库的代码不会编译进程序里面,所以动态库编译的程序比较小。
2,由动态库编译的程序依赖于系统的环境变量有没有这个库文件,没有则运行不了。
静态库特点:
1,库的代码会编译进程序里面,所以静态库编译的程序比较大
2,由静态库编译的程序不用依赖于系统的环境变量,所以环境变量有没有这个库文件,也可以运行。
1. 目录结构
2. 生成动态库 libnotice_wake.so(src目录)
2.1 libnotice_wake.h 文件
#ifndef __LIBNOTICE_WAKE_H
#define __LIBNOTICE_WAKE_H
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
/*叫醒服务方式的枚举*/
enum ask_wakeup_way
{
call_me=1, /*打电话*/
knocking_door_me, /*敲门*/
watering_head_me, /*泼水*/
};
typedef void (*ask_for_wakeup)(int num); /*库(中间)函数*/
typedef struct call_function_t /*库(中间)函数结构体*/
{
int way;
ask_for_wakeup function;
}call_function;
int registe_callback(call_function *reg_fun); /*注册库(中间)函数*/
void notice_event(int num); /*事件函数*/
#endif
2.2 libnotice_wake.c 文件
/*库模块
*实现注册函数
*实现事件触发回调
*/
#include "libnotice_wake.h"
/*定义一个函数指针全局变量*/
ask_for_wakeup g_wakeup_ptr;
int registe_callback(call_function *reg_fun)
{
/*注册:g_wakeup_ptr是个函数指针*/
g_wakeup_ptr = reg_fun->function;
return 0;
}
/*事件函数*/
void notice_event(int event)
{
g_wakeup_ptr(event);
}
2.3 生成动态库的 Makefile 文件
CC = gcc
CFLAGS = -Wall -g -O -fPIC
CXXFLAGS =
INCLUDE = ./inc
TARGET = libnotice_wake.so
LIBPATH = ./libs/
vpath %.h ./inc
OBJS = libnotice_wake.o
SRCS = libnotice_wake.c
all:$(TARGET) $(OBJS)
$(OBJS):$(SRCS)
$(CC) $(CFLAGS) -I $(INCLUDE) -c $^
$(TARGET):$(OBJS)
$(CC) $(OBJS) -shared -fPIC -o $(TARGET)
mv $(TARGET) $(LIBPATH)
clean:
rm -f *.o
rm -f $(LIBPATH)*
3. 调用动态库生成 notice_wake_app.1.0.0.1可执行文件(bin目录)
应用程序以两种方式加载动态库:
- 在应用程序执行时加载动态库
- 在编译应用程序时加载动态库(本文使用的编译方法)
对于这种方式编译出来的动态库文件,还需要在 /etc/ld.so.conf.d/ 目录中添加 libmytest.so 库文件的路径说明,即在 /etc/ld.so.conf.d/ 目录中新建配置文件 mytest.conf,且执行 ldconfig 命令, /etc/ld.so.conf.d/mytest.conf 的文件内容为 libmytest.so 库文件的绝对路径,例如:/root/workspace/callback_example_2/lib(这是mytest.conf配置文件的内容)
3.1 main 程序
#include "libnotice_wake.h"
/*这个才是回调函数*/
void callback_function(int event)
{
switch(event)
{
case call_me:
{
printf("Hello,this is hotel servicer.Get up!\n");
}
break;
case knocking_door_me:
{
printf("peng~ peng~ peng~.Get up!\n");
}
break;
case watering_head_me:
{
printf("hua hua hua. fuck!\n");
}
break;
default:
{
printf("Unsupport wakeup way. Please input 1 to 3 !\n");
}
}
}
int main()
{
int ret = 0;
int event = 0;
call_function ptr_get_up;
ptr_get_up.way = 1;
ptr_get_up.function = callback_function;
/*注册回调函数*/
ret = registe_callback(&ptr_get_up);
if (-1 == ret)
{
printf("Register failed.\n");
}
while(1)
{
printf("Please input wakeup way:");
scanf("%d", &event);
/*退出循环*/
if (0 == event)
{
return ;
}
/*触发事件*/
notice_event(event);
}
return 0;
}
3.2 生成可执行文件 notice_wake_app.1.0.0.1 的 Makefile 文件
CROSS =
CC = $(CROSS)gcc
CXX = $(CROSS)g++
DEBUG = -g -O2
CFLAGS = $(DEBUG) -Wall -c
RM = rm -rf
SRCS = $(wildcard ./*.c)
OBJS = $(patsubst %.c, %.o, $(SRCS))
HEADER_PATH = -I../include/
LIB_PATH = -L../lib/
LIBS = -lnotice_wake
# LIBS = libdiv.a
VERSION = 1.0.0.1
TARGET = notice_wake_app.$(VERSION)
$(TARGET) : $(OBJS)
$(CC) $^ -o $@ $(LIB_PATH) $(LIBS)
$(OBJS):%.o : %.c
$(CC) $(CFLAGS) $< -o $@ $(HEADER_PATH)
clean:
$(RM) $(TARGET) *.o
下面的这种情况待日后再修改:
回调函数 void callback_fun(int event),如果能拆分成许多 xxx(); xxx(); xxx();,演示效果会好很多。里面用 switch去处理不同事件,最终回调的函数始终是 callback_fun,体现不出注册、回调的特性。其实说白了就是有一个处理过程,可能会有很多种处理情况,我们把每一种情况的处理都单独写成一个函数(当然这些函数的参数、返回值都是相同的),再将这个完整的处理过程写作一个函数指针,根据事件的不同,来将对应的处理函数地址放到那个指针中,选择函数地址放入指针的这个过程叫做注册。完成用可变的函数指针,去实现相同接口下不同功能的切换。