当一个主程序在不同的情况下,需要链接功能不同的动态库时,我们一般的做法时,每换一个情景,我们就要让程序重新链接一个新的动态库,这样就显得很麻烦又笨拙。如果我们可以把动态库作为参数来传递,那样我们只需要在主程序中判断是哪种情景,然后由程序帮我们自动选择所需要链接的动态库即可。
下面我们用一个实例来详细讲解:
这个实例的功能是用一个主程序来使lcd液晶屏显示bmp格式或者jpg格式的图片,当接收到的参数是bmp格式的图片时,就链接bmp动态库;当接收到的参数是jpg格式的图片时,就链接jpg动态库。
制作显示bmp、JPG图片的动态库
common.h是常用的一些头文件
//common.h
#ifndef __COMMOD_H
#define __COMMOD_H
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <strings.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <math.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <time.h>
#include <errno.h>
#include <linux/input.h>
#include <linux/fb.h>
#include <dlfcn.h>
#endif
bmp.h是显示bmp图片所需要的头文件
//bmp.h
#ifndef __BMP_H_
#define __BMP_H_
#include <stdio.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <pthread.h>
#define LCD "/dev/fb0"
#define BACKGROUND "./res/bmp/background.bmp"
#define BAR "./res/bmp/bar.bmp"
#define LOGO "./res/bmp/logo.bmp"
#define KEYON "./res/bmp/key_on.bmp"
#define KEYOFF "./res/bmp/key_off.bmp"
struct bitmap_header
{
int16_t type;
int32_t size; // 图像文件大小
int16_t reserved1;
int16_t reserved2;
int32_t offbits; // bmp图像数据偏移量
}__attribute__((packed));
struct bitmap_info
{
int32_t size; // 本结构大小
int32_t width; // 单位:像素
int32_t height;// 单位:像素
int16_t planes; // 总为零
int16_t bit_count; // 色深:24(1像素=24位=3字节)
int32_t compression;
int32_t size_img; // bmp数据大小,必须是4的整数倍
int32_t X_pel;
int32_t Y_pel;
int32_t clrused;
int32_t clrImportant;
}__attribute__((packed));
struct rgb_quad
{
int8_t blue;
int8_t green;
int8_t red;
int8_t reserved;
}__attribute__((packed));
struct image_info
{
int width;
int height;
int pixel_size;
};
void draw_piano(char *FB, struct fb_var_screeninfo *vinfo, char *backgound);
char *load_bmp (const char *bmpfile, struct image_info *imginfo);
void freelcd(void);
void display(char *image, int offset_x, int offset_y);
#endif
showbmp.c是封装好的显示bmp图片的功能模块
//showbmp.c
#include "common.h"
#include "bmp.h"
static bool first = true;
static int lcd_w;
static int lcd_h;
static int lcd_bpp;
static int lcd;
static char *lcdmem;
void display(char *bmp, int x, int y) // ./main xxx.bmp
{
// 0,分析BMP图片的头部信息,提取长宽深等信息
int fd = open(bmp, O_RDWR);
struct bitmap_header head;
struct bitmap_info info;
bzero(&head, sizeof(head));
bzero(&info, sizeof(info));
read(fd, &head, sizeof(head));
read(fd, &info, sizeof(info));
// 1,读取BMP图片的具体图像数据(RGB)
int rgb_size = head.size-sizeof(head)-sizeof(info);
char *rgb = calloc(1, rgb_size);
int total = 0;
while(rgb_size > 0)
{
int n = read(fd, rgb+total, rgb_size);
rgb_size -= n;
total += n;
}
if(first)
{
// 2,打开LCD设备
lcd = open("/dev/fb0", O_RDWR);
// 3,获取LCD设备的硬件参数(分辨率、色深……)
struct fb_var_screeninfo vsinfo;
bzero(&vsinfo, sizeof(vsinfo));
ioctl(lcd, FBIOGET_VSCREENINFO, &vsinfo); // 将LCD设备参数信息放入svinfo中
printf("屏幕分辨率: %d × %d\n", vsinfo.xres, vsinfo.yres);
// 4,映射内存
lcd_w = vsinfo.xres;
lcd_h = vsinfo.yres;
lcd_bpp = vsinfo.bits_per_pixel;
lcdmem = mmap(NULL, lcd_w*lcd_h*lcd_bpp/8,
PROT_READ | PROT_WRITE, MAP_SHARED, lcd, 0);
if(lcdmem == MAP_FAILED)
{
perror("映射内存失败");
exit(0);
}
first = false;
}
bzero(lcdmem, lcd_w*lcd_h*lcd_bpp/8);
int w = (lcd_w-x)<info.width ? (lcd_w-x) : info.width;
int h = (lcd_h-y)<info.height ? (lcd_h-y) : info.height;
int pad = (4-(info.width*3)%4) % 4;
int img_line_size = info.width*3 + pad;
int lcd_line_size = lcd_w * lcd_bpp/8;
// 5,妥善地将BMP中的RGB数据搬到映射内存上
char *rgb_r = rgb + img_line_size*(info.height-1);
char *tmp = lcdmem+(y*800+x)*4;
for(int j=0; j<h; j++)
{
for(int i=0; i<w; i++)
{
memcpy(tmp+j*lcd_line_size+4*i, (rgb_r-j*img_line_size)+3*i, 3);
}
}
// 6, 释放相应资源
close(fd);
free(rgb);
}
void freelcd(void)
{
if(!first)
{
munmap(lcdmem, lcd_w*lcd_h*lcd_bpp/8);
close(lcd);
}
first = true;
}
上面这部分代码就可以制作bmp的动态库libbmp.so了,具体的命令请移步到我的另一篇文章:linux下的动态库静态库解析
接下来就是制作显示jpg图片的动态库了,下面这个showjpg.c是封装好的显示jpg图片的功能模块。当然除了这个.c文件外,我们肯定还需要链接JPEG库,至于如何编译移植JPEG库就请移步到我的另一篇文章了:编译移植使用JPEG库
//showjpg.c
#include "common.h"
#include "jpeglib.h"
static int lcd;
static int lcd_w;
static int lcd_h;
static int lcd_bpp;
static int screen_size;
static struct fb_var_screeninfo vsinfo;
static char *lcdmem;
static bool first = true;
void display(char *jpg, int x, int y)
{
// 第一次显示图片的时候,就准备好LCD设备
if(first)
{
lcd = open("/dev/fb0", O_RDWR);
bzero(&vsinfo, sizeof(vsinfo));
ioctl(lcd, FBIOGET_VSCREENINFO, &vsinfo); // 获取了LCD的硬件参数
lcd_w = vsinfo.xres;
lcd_h = vsinfo.yres;
lcd_bpp = vsinfo.bits_per_pixel;
screen_size = lcd_w * lcd_h * lcd_bpp/8;
lcdmem = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, lcd, 0);
first = false;
}
bzero(lcdmem, screen_size);
int jpgfd = open(jpg, O_RDONLY);
int jpg_size = lseek(jpgfd, 0L, SEEK_END);
lseek(jpgfd, 0L, SEEK_SET);
char *jpg_buffer = calloc(1, jpg_size);
// 完整地读完了 a.jpg 文件的内容,妥妥地放到了 jpg_buffer 中
int total = jpg_size;
while(total > 0)
{
int n = read(jpgfd, jpg_buffer+(jpg_size-total), total);
total -= n;
}
close(jpgfd);
// JPG ==> RGB
// 声明解压缩结构体,以及错误管理结构体
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
// 使用缺省的出错处理来初始化解压缩结构体
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
// 配置该cinfo,使其从jpg_buffer中读取jpg_size个字节
// 这些数据必须是完整的JPEG数据
jpeg_mem_src(&cinfo, jpg_buffer, jpg_size);
// 读取JPEG文件的头,并判断其格式是否合法
int ret = jpeg_read_header(&cinfo, true);
if(ret != 1)
{
fprintf(stderr, "[%d]: jpeg_read_header failed: "
"%s\n", __LINE__, strerror(errno));
exit(1);
}
// 开始解码
jpeg_start_decompress(&cinfo);
// cinfo中保存了图片文件的尺寸信息
cinfo.output_width; // 宽
cinfo.output_height; // 高
cinfo.output_components; // 深:每个像素点包含的字节数
// 图片的每一行所包含的字节数
int row_stride = cinfo.output_width * cinfo.output_components;
// 根据图片的尺寸大小,分配一块相应的内存rgb_buffer
// 用来存放从jpg_buffer解压出来的图像数据
unsigned char *rgb_buffer = calloc(1, row_stride*cinfo.output_height);
// 循环地将图片的每一行读出并解压到rgb_buffer中
int line = 0;
while(cinfo.output_scanline < cinfo.output_height)
{
unsigned char *buffer_array[1];
buffer_array[0] = rgb_buffer +
(cinfo.output_scanline) * row_stride;
jpeg_read_scanlines(&cinfo, buffer_array, 1);
}
// 解压完了,将jpeg相关的资源释放掉
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
free(jpg_buffer);
// 恭喜!现在rgb_buffer中就已经有图片对应的RGB数据了
int red_offset = vsinfo.red.offset;
int green_offset= vsinfo.green.offset;
int blue_offset = vsinfo.blue.offset;
char *lcdtmp = lcdmem;
char *rgbtmp = rgb_buffer;
lcdtmp += (y*lcd_w+ x)*(lcd_bpp/8);
int w = (lcd_w-x)<cinfo.output_width ? (lcd_w-x) : cinfo.output_width;
int h = (lcd_h-y)<cinfo.output_height? (lcd_h-y) : cinfo.output_height;
// 将rgb数据妥善地放入lcdmem
for(int j=0; j<h; j++)
{
for(int i=0; i<w; i++)
{
memcpy(lcdtmp + 4*i + red_offset/8, rgbtmp + 3*i + 0, 1);
memcpy(lcdtmp + 4*i + green_offset/8, rgbtmp + 3*i + 1, 1);
memcpy(lcdtmp + 4*i + blue_offset/8, rgbtmp + 3*i + 2, 1);
}
lcdtmp += (lcd_w*lcd_bpp/8); // lcd显存指针向下偏移一行
rgbtmp += (row_stride); // rgb指针向下偏移一行
}
// 释放相应的资源
free(rgb_buffer);
}
void freelcd(void)
{
if(!first)
{
munmap(lcdmem, screen_size);
close(lcd);
first = true;
}
}
接下来我们制作jpg库,这个是我们自己写的用来专门显示jpg图片的动态库,和JPEG库是不一样的,JPEG库是用来专门解码jpg格式的图片的,由于我们要显示jpg图片,所以我们需要用JPEG库来解码jpg图片,以获取jpg图片上面的数据。
经过上面的努力,我们会得到两个动态库文件:libbmp.so、libjpg.so。
接下来,我们就要把这两个动态库作为参数传递到主函数中。
下面是主程序show_image的代码:
#include "common.h"
int main(int argc, char **argv) // ./show_iamge xxx.jpg/xxx.bmp
{
if(argc != 2)
{
printf("用法: %s <图片文件(*.bmp/*.jpg)>\n", argv[0]);
exit(0);
}
void *handle;
// 0,判断后缀,并打开相应的动态库
if(strstr(argv[1], ".bmp")) // 图片是BMP格式的,加载BMP库
{
handle = dlopen("libbmp.so", RTLD_NOW);
}
if(strstr(argv[1], ".jpg")) // 图片是JPG格式的,加载JPG库
{
handle = dlopen("libjpg.so", RTLD_NOW);
}
// 1,查找指定的函数接口
void (*display)(char *file, int x, int y);
void (*freelcd)(void);
display = dlsym(handle, "display");
freelcd = dlsym(handle, "freelcd");
if(display == NULL || freelcd == NULL)
{
perror("找不到display或者找不到freelcd");
exit(0);
}
// 2,使用相应的库接口来显示指定的图片
display(argv[1], 50, 20);
freelcd();
dlclose(handle);
return 0;
}
然后我们编译show_image.c
arm-none-linux-gnueabi-gcc show_image.c -o show_image -ldl -L dir/ -ljpeg
其中:
-ldl 是使用了动态链接库接口
-L dir/ 指定第三方库所在的路径(即JPEG库所安装的路径下的lib/目录)
-ljpeg 链接JPEG库
然后我们将可执行文件show_image、libbmp.so、libjpg.so传输到开发板上,修改main的权限为777,将两个动态库所在路径添加到LD_LIBRARY_PATH环境变量中,最后运行即可。
./show_image xxx.bmp //这样就可以显示一张bmp格式的图片
./show_image xxx.jpg //这样就可以显示一张jpg格式的图片,两种格式可以随时切换
总结
-
将 showbmp.c 编译成 showbmp.o,没有借助任何第三方库
-
将 showjpg.c 编译成 showjpg.o ,由于用到了 jpeg 第三方库的相应的函数,因此需要指定和包含相应的头文件。由于编译成 jpg.o 不进行库代码的链接,因此不需要指定 jpeg 库。
-
将 showbmp.o 制作成动态库 libbmp.so
-
将 showjpg.o 制作成动态库 libjpg.so,由于将来在执行 libjpg.so 中的代码时需要链接 libjpeg.so ,因此在此编译指令中需要指定 libjpeg.so。
-
编译show_image.c ,该程序编译时没有指定任何图像编解码库,而是用了动态链接库接口,因此编译指令包含了 -ldl 选项,使用该接口表明将本程序链接动态库的行为从编译时推迟到运行时,以此来获得更大的灵活性。
如果以后还有其他格式的图片需要显示,直接将制作好的动态库传到之前那些格式的动态库的路径下,再将新的动态库作为参数给主程序调用即可,这样大大提高了效率,具有很强的可移植性。
这只是一个用来说明问题的实例,动态库参数化这个技术可以运用在很多复杂情景,剩下的就大家一起探索吧。