我如何在C模块化设计?

我想让我的项目更加模块化,以便在删除其中一个模块时没有模块间依赖关系。

例如,如果我将我的过程中的代码分成多个目录,比如X,Y和Z,那么X中的数据结构不应该由Y和Z中的数据结构直接访问,反之亦然,那么我需要一些内部通信机制X,Y和Z.

由于我使用C编码,是否有人可以建议样本项目或设计考虑因素?

这通常归结为API设计。 我认为有用的几件事:

  • 请记住,您的API存在于头文件中。 实现在C文件中。
  • 避免全局变量 – 必要时使用访问器方法
  • 尽可能避免结构共享
  • 使用回调函数来减少耦合

libfoo.h

int (*libfoo_callback)(void *arg, const char *name, int id); /** * Iterate over all known foobars in the system. */ int libfoo_iterate_foobars(libfoo_callback cb, void *arg); 

libfoo.c

 #include "libfoo.h" /* Private to libfoo.c */ struct foobar { struct foobar *next; const char *name; int id; }; /* Don't make this globally visible */ static struct foobar *m_foobars; int libfoo_iterate_foobars(libfoo_callback cb, void *arg) { struct foobar *f; for (f = m_foobars; f != NULL; f = f->next) { int rc = cb(f->name, f->id); if (rc <= 0) return rc; /* Stop iterating */ } return 0; } 

some_consumer.c

 #include  #include "libfoo.h" struct cbinfo { int count; }; static int test_callback(void *arg, const char* name, int id) { struct cbinfo *info = arg; printf(" foobar %d: id=%d name=%s\n", info->count++, id, name); return 1; /* keep iterating */ } void test(void) { struct cbinfo info = { 0 }; printf("All foobars in the system:\n"); libfoo_iterate_foobars(test_callback, &info); printf("Total: %d\n", info.count); } 

在这里,我展示了一个跟踪一些foobars的libfoo。 我们有一个消费者,在这个例子中,只是想显示所有foobars的列表。 这个设计的好处:

  • 没有全局可见的变量:除了libfoo之外,没有人可以直接修改foobar列表。 它们只能以公共API允许的方式使用libfoo。

  • 通过使用回调迭代器方法,我让消费者​​不必了解有关如何跟踪foobar的任何信息。 今天它是struct foobar的列表,也许明天它是一个SQLite数据库。 通过隐藏结构定义,消费者只需要知道foobar具有nameid


真正实现模块化,您将需要两件大事:

  1. 一组API,用于定义模块如何生成和使用数据
  2. 一种在运行时实际加载模块的方法

具体情况将根据您的目标平台,模块化需求,预算等因素而有很大差异。

对于#1,您通常会有一个模块注册系统,其中一些组件跟踪已加载模块的列表,以及有关生成和使用内容的元信息。

如果模块可以调用其他模块提供的代码,则需要一种方法使其可见。 这也将用于实现2.以Linux内核为例 - 它支持可加载内核模块 ,以便将新function,驱动程序等添加到内核中,而无需将其全部编译为一个大型二进制文件。 模块可以使用EXPORT_SYMBOL来指示特定符号(即函数)可供其他模块调用。 内核跟踪加载哪些模块,导出哪些函数以及哪些地址。

对于#2,您可以利用操作系统的共享库支持。 在Linux和其他Unices上,这些动态库是ELF( .so文件),它们由动态加载程序加载到进程的地址空间中。 在Windows上,这些是DLL。 通常,在您的流程开始时,会自动处理此加载。 但是,应用程序可以利用动态加载程序显式加载其选择的其他模块。 在POSIX上你可以调用dlopen() ,在Windows上你可以使用LoadLibrary() 。 这两种函数都会为您返回某种句柄 ,这样您就可以对模块进行进一步的查询或请求。

然后可能需要您的模块(按照您的设计)导出codingfreak_init函数,该函数在首次加载模块时由您的应用程序调用。 然后,此函数将对您的框架进行额外调用,或返回数据以指示它需要和提供的工具。

这些都是非常一般的信息,应该让你的车轮转动。

在你开始编码之前我会设置一个“公共”API。 然后,代码仅使用每个模块外部的API。 不要作弊; 仅使用公共API(尽管API可以根据需要发展)。 它可以帮助尽可能多地处理面向对象语言中的对象之类的数据结构,以及像对象方法那样的公共API。 尽可能避免在模块外部直接使用内部数据结构字段; 虽然如果它们是API的一部分,返回定义良好的数据结构是可以的。 只是不要直接在它们来自的模块之外修改它们。 如果您花费大量时间预先设计接口,则可以创建一个非常易于维护的项目。