17、实战案例1:图像处理插件系统(滤镜、编解码器)

好,终于到了实战环节。前面我们聊了那么多动态加载的理论、符号解析、生命周期管理,说实话,都是「纸上谈兵」。今天咱们来点真格的——做一个图像处理插件系统。

为什么选图像处理?因为它的接口清晰,容易理解。滤镜就是输入一张图,输出一张图。编解码器也是,输入数据流,输出图像。这种「输入-处理-输出」的模型,天生适合插件化。

我在公司做嵌入式图像处理时,就吃过一次亏。当时把所有滤镜算法都编译进主程序,结果每次加一个新滤镜,就得重新编译整个固件,烧录、测试,折腾一整天。后来改成插件架构,新滤镜只需要编译一个 .so 文件,丢到指定目录,重启就生效。嗯,那感觉,真香。

17.1 系统整体架构

先看看我们要搭的架子。说白了,就是主程序 + 插件管理器 + 一堆动态库。

图像处理插件系统架构 主程序 (main) 图像加载、UI、流程控制 插件管理器 扫描目录、加载/卸载插件 滤镜插件 blur.so, sharpen.so, ... 编解码器插件 png.so, jpeg.so, ... 扩展插件 水印.so, 裁剪.so, ... ./plugins/ 目录

核心思路就三点:

  • 主程序:负责加载图像、调用插件、显示结果。它不关心具体算法。
  • 插件管理器:扫描插件目录,用 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;
}
注意: 上面的代码为了演示做了简化。实际项目中,你需要保存每个插件的 dlopen 返回的 handle,以便在卸载时调用 dlclose。否则会有内存泄漏。我曾经在某个产品中忘了保存 handle,结果插件卸载后,动态库还留在内存里,导致后续版本升级失败。

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 目录,重启即可。

核心收获: 插件化架构的核心不是技术,而是「约定」。接口定好了,各干各的,互不干扰。动态加载只是实现手段,真正的价值在于可扩展性模块化

嗯,这个实战案例就到这里。代码虽然简化了,但骨架是完整的。你可以在它的基础上,加入配置解析、热加载、插件依赖管理等功能。我在实际项目中,还加了一个「插件沙箱」,限制插件的系统调用权限,防止恶意插件搞破坏。这些进阶内容,咱们后面再聊。

公众号:蓝海资料掘金营,微信deep3321