17、实战案例1:图像处理插件系统(滤镜、编解码器)
好,终于到了实战环节。前面我们聊了那么多动态加载的理论、符号解析、生命周期管理,说实话,都是「纸上谈兵」。今天咱们来点真格的——做一个图像处理插件系统。
为什么选图像处理?因为它的接口清晰,容易理解。滤镜就是输入一张图,输出一张图。编解码器也是,输入数据流,输出图像。这种「输入-处理-输出」的模型,天生适合插件化。
我在公司做嵌入式图像处理时,就吃过一次亏。当时把所有滤镜算法都编译进主程序,结果每次加一个新滤镜,就得重新编译整个固件,烧录、测试,折腾一整天。后来改成插件架构,新滤镜只需要编译一个 .so 文件,丢到指定目录,重启就生效。嗯,那感觉,真香。
17.1 系统整体架构
先看看我们要搭的架子。说白了,就是主程序 + 插件管理器 + 一堆动态库。
核心思路就三点:
- 主程序:负责加载图像、调用插件、显示结果。它不关心具体算法。
- 插件管理器:扫描插件目录,用 dlopen 加载 .so 文件,注册插件。
- 插件本身:每个插件是一个独立的动态库,导出统一的接口函数。
你想想看,这种分层最大的好处是什么?解耦。主程序和插件开发者可以并行工作,互不干扰。我当年带团队时,就让两个人分别写模糊滤镜和锐化滤镜,各自编译各自的 .so,最后合到一起测试,基本没出问题。
17.2 插件接口定义
接口是插件的「契约」。定好了,大家按规矩办事。
我习惯用结构体来定义插件描述,而不是散乱的导出函数。这样更清晰,也方便扩展。
/* plugin.h */
#ifndef PLUGIN_H
#define PLUGIN_H
#include <stdint.h>
/* 图像数据结构 */
typedef struct {
uint8_t *pixels; /* RGBA 像素数据 */
int width;
int height;
int channels; /* 通常是 3 或 4 */
} Image;
/* 插件类型枚举 */
typedef enum {
PLUGIN_TYPE_FILTER = 0,
PLUGIN_TYPE_CODEC,
PLUGIN_TYPE_EFFECT
} PluginType;
/* 插件描述结构体 */
typedef struct {
const char *name; /* 插件名称,如 "gaussian_blur" */
const char *version; /* 版本号 */
PluginType type; /* 插件类型 */
const char *description; /* 简短描述 */
/* 核心处理函数指针 */
int (*process)(Image *in, Image **out, const char *params);
/* 释放输出图像 */
void (*free_image)(Image *img);
} PluginDescriptor;
/* 每个插件必须导出的注册函数 */
typedef PluginDescriptor* (*register_plugin_t)(void);
#endif /* PLUGIN_H */
这里有几个设计细节,我说一下:
- process 函数:输入一张图,输出一张新图。params 参数可以传 JSON 字符串,比如
{"radius":5},这样插件可以灵活配置。 - free_image 函数:谁分配谁释放。插件分配的内存,由插件自己释放。主程序不碰内部细节。
- register_plugin_t:每个 .so 必须导出一个同名函数,返回 PluginDescriptor 指针。这是插件的「入口」。
17.3 插件管理器实现
插件管理器是系统的「心脏」。它负责扫描目录、加载 .so、管理插件生命周期。
/* plugin_manager.c */
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "plugin.h"
#define PLUGIN_DIR "./plugins"
#define MAX_PLUGINS 64
static PluginDescriptor *plugins[MAX_PLUGINS];
static int plugin_count = 0;
/* 加载单个插件 */
static int load_plugin(const char *path) {
void *handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
if (!handle) {
fprintf(stderr, "dlopen failed: %s\n", dlerror());
return -1;
}
/* 查找注册函数 */
register_plugin_t reg = (register_plugin_t)dlsym(handle, "register_plugin");
if (!reg) {
fprintf(stderr, "dlsym failed: %s\n", dlerror());
dlclose(handle);
return -1;
}
PluginDescriptor *desc = reg();
if (!desc || !desc->process) {
fprintf(stderr, "invalid plugin descriptor\n");
dlclose(handle);
return -1;
}
/* 保存到插件列表 */
if (plugin_count < MAX_PLUGINS) {
plugins[plugin_count++] = desc;
printf("loaded plugin: %s v%s\n", desc->name, desc->version);
return 0;
}
dlclose(handle);
return -1;
}
/* 扫描并加载所有插件 */
int load_all_plugins(void) {
DIR *dir = opendir(PLUGIN_DIR);
if (!dir) {
perror("opendir");
return -1;
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
/* 只加载 .so 文件 */
const char *ext = strrchr(entry->d_name, '.');
if (ext && strcmp(ext, ".so") == 0) {
char fullpath[512];
snprintf(fullpath, sizeof(fullpath), "%s/%s", PLUGIN_DIR, entry->d_name);
load_plugin(fullpath);
}
}
closedir(dir);
return plugin_count;
}
/* 按类型查找插件 */
PluginDescriptor* find_plugin(PluginType type, const char *name) {
for (int i = 0; i < plugin_count; i++) {
if (plugins[i]->type == type && strcmp(plugins[i]->name, name) == 0) {
return plugins[i];
}
}
return NULL;
}
/* 卸载所有插件(实际是关闭动态库句柄) */
void unload_all_plugins(void) {
/* 注意:这里简化了,实际需要保存 handle */
plugin_count = 0;
}
17.4 编写一个模糊滤镜插件
有了框架,写插件就简单了。每个插件就是一个 .c 文件,编译成 .so。
/* blur_filter.c */
#include <stdlib.h>
#include <string.h>
#include "plugin.h"
static Image* blur_process(Image *in, const char *params) {
/* 简单实现:3x3 均值模糊 */
Image *out = malloc(sizeof(Image));
out->width = in->width;
out->height = in->height;
out->channels = in->channels;
out->pixels = malloc(in->width * in->height * in->channels);
int stride = in->width * in->channels;
for (int y = 1; y < in->height - 1; y++) {
for (int x = 1; x < in->width - 1; x++) {
for (int c = 0; c < in->channels; c++) {
int idx = (y * stride) + (x * in->channels) + c;
int sum = 0;
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
int nidx = ((y+dy) * stride) + ((x+dx) * in->channels) + c;
sum += in->pixels[nidx];
}
}
out->pixels[idx] = sum / 9;
}
}
}
return out;
}
static void blur_free(Image *img) {
if (img) {
free(img->pixels);
free(img);
}
}
static PluginDescriptor desc = {
.name = "blur",
.version = "1.0.0",
.type = PLUGIN_TYPE_FILTER,
.description = "3x3 box blur filter",
.process = blur_process,
.free_image = blur_free
};
/* 导出注册函数 */
PluginDescriptor* register_plugin(void) {
return &desc;
}
编译命令:
gcc -shared -fPIC -o ./plugins/blur.so blur_filter.c
就这么简单。你写一个锐化滤镜、边缘检测滤镜,流程完全一样。每个插件独立编译,互不依赖。
17.5 编解码器插件示例
编解码器稍微复杂一点,因为涉及文件 I/O。但接口思路是一样的。
/* png_codec.c */
#include "plugin.h"
/* 简化的 PNG 解码器(仅演示接口) */
static int png_decode(const char *filename, Image **out) {
/* 实际项目中这里调用 libpng */
/* 这里只是模拟 */
*out = malloc(sizeof(Image));
(*out)->width = 100;
(*out)->height = 100;
(*out)->channels = 4;
(*out)->pixels = malloc(100 * 100 * 4);
memset((*out)->pixels, 0xFF, 100 * 100 * 4);
return 0;
}
static int png_encode(const char *filename, Image *in) {
/* 模拟编码 */
return 0;
}
static PluginDescriptor desc = {
.name = "png_codec",
.version = "1.0.0",
.type = PLUGIN_TYPE_CODEC,
.description = "PNG image codec",
.process = NULL, /* 编解码器不使用 process */
.free_image = NULL
};
PluginDescriptor* register_plugin(void) {
return &desc;
}
你可能会问:编解码器的接口和滤镜不一样,怎么统一?好问题。我的做法是在 PluginDescriptor 里加一个 void *context 字段,用来存放编解码器特有的函数表。主程序通过 type 字段判断,然后强制转换 context 来调用。虽然有点「脏」,但胜在灵活。
17.6 主程序调用示例
最后,看看主程序怎么用:
/* main.c */
#include <stdio.h>
#include "plugin.h"
int main(void) {
/* 加载所有插件 */
int count = load_all_plugins();
printf("loaded %d plugins\n", count);
/* 查找模糊滤镜 */
PluginDescriptor *blur = find_plugin(PLUGIN_TYPE_FILTER, "blur");
if (blur) {
Image in = { /* 加载图像 */ };
Image *out = NULL;
blur->process(&in, &out, "{\"radius\":3}");
/* 显示或保存 out */
blur->free_image(out);
}
/* 查找 PNG 编解码器 */
PluginDescriptor *png = find_plugin(PLUGIN_TYPE_CODEC, "png_codec");
if (png) {
/* 调用编解码器特有接口 */
}
return 0;
}
整个流程清晰明了。主程序不需要知道 blur 是怎么实现的,也不需要链接任何滤镜库。新加一个滤镜,只需要把 .so 丢到 plugins 目录,重启即可。
嗯,这个实战案例就到这里。代码虽然简化了,但骨架是完整的。你可以在它的基础上,加入配置解析、热加载、插件依赖管理等功能。我在实际项目中,还加了一个「插件沙箱」,限制插件的系统调用权限,防止恶意插件搞破坏。这些进阶内容,咱们后面再聊。