-->

How do I Modular Design in C?

2020-07-24 06:10发布

问题:

I want to make my project more modular so that there are no inter-modular dependencies if one of the module is removed.

For e.g. If I divide the code in my process into multiple directories, say, X, Y and Z so that data structures in X should not be accessed directly by data structures in Y and Z and vice versa then I need some internal communication mechanism between X, Y and Z.

Since I am coding in C, can anyone suggest a sample project or design considerations for the same?

回答1:

This often boils down to API design. A couple things that I find helpful:

  • Remember that your API lives in your header files. The implementation is in the C files.
  • Avoid global variables - use accessor methods if necessary
  • Avoid structure sharing if possible
  • Use callback functions to reduce coupling

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 <stdio.h>
#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);
}

Here I show a libfoo who tracks some foobars. And we have a consumer who in this example, simply wants to show a list of all foobars. Benefits to this design:

  • No globally-visible variables: No one other than libfoo can directly modify the list of foobars. They can only use libfoo in the manner allowed by the public API.

  • By using a callback-iterator approach, I've kept the consumer from having to know anything about how a foobar is even tracked. Today it's a list of struct foobar, maybe tomorrow it's an SQLite database. By hiding the structure definition, the consumer only needs to know that a foobar has a name and an id.


To be truly modular, you're going to need two big things:

  1. A set of APIs that define how modules produce and consume data
  2. A way to actually load modules at runtime

The specifics of this will greatly vary depending on your target platform, modular needs, budget, etc.

For #1 you would generally have a module registration system, where some component tracks a list of loaded modules, as well as meta information about what the produce and consume.

If modules can call into code provided by other modules, you'll need a way to make this visible. This will also play into the implementation of 2. Take the Linux kernel for example - it supports loadable kernel modules for the purpose of adding new features, drivers etc. into the kernel, without having to compile it all into one large binary. Modules can use EXPORT_SYMBOL to indicate that particular symbol (i.e. function) is available for other modules to call. The kernel tracks which modules are loaded, what functions they export, and at which addresses.

For #2 you might leverage your OS's shared library support. On Linux and other Unices, these dynamic libraries are ELF (.so files), which are loaded by the dynamic loader into a process's address space. On Windows, these are DLLs. Normally, this loading is handled automatically for you when your process starts. However, an application can leverage the dynamic loader to explicitly load additional modules of its choosing. On POSIX you would call dlopen(), and on Windows you would use LoadLibrary(). Either function will return some sort of handle to you, which will allow you to make further inquiries or requests about the module.

Your module then might be required (by your design) to export a codingfreak_init function, which is called by your application when a module is first loaded. This function would then make additional calls into your framework, or return data to indicate what facilities it requires and provides.

This is all very general information which should get your wheels turning.



回答2:

I would set up a "public" API before you even start coding. Then, code using only that API from outside each module. Don't cheat; use only the public API (though the API can evolve as needed). It can help to treat the data structures like objects in an object-oriented language, and the public API like the methods of the object, as much as you can. As much as possible, avoid using internal data structure fields directly outside the module; though returning well-defined data structures is OK if they are part of the API. Just don't modify them directly outside the module they come from. If you spend a good amount of time designing the interfaces up front, you can make a very maintainable project.