This often boils down to API design. A few things that I find useful:
- Remember that your API is in your header files. The implementation is in the C files.
- Avoid global variables - use access methods if necessary.
- Avoid sharing structure
- Use callback functions to reduce communication
libfoo.h
int (*libfoo_callback)(void *arg, const char *name, int id); int libfoo_iterate_foobars(libfoo_callback cb, void *arg);
libfoo.c
#include "libfoo.h" struct foobar { struct foobar *next; const char *name; int id; }; 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; } 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 the libfoo that tracks some foobars. And we have a consumer who in this example just wants to show a list of all foobars. Benefits for this design:
There are no variables visible around the world: no one but libfoo can directly modify the foobars list. They can use libfoo only according to the public API.
Using the iterator callback approach, I did not let the consumer know anything about how to track the fobar. Today is a list of struct foobar , maybe tomorrow is a SQLite database. Anyone hiding the definition of a structure, the consumer should only know that foobar has a name and id .
To be truly modular, you will need two big things:
- A set of APIs that defines how modules produce and consume data.
- Method for loading modules at runtime
The specifics of this will vary greatly depending on your target platform, modular needs, budget, etc.
For # 1, you usually have a module registration system where some component keeps track of the list of loaded modules, as well as meta-information about what it produces and consumes.
If modules can invoke code provided by other modules, you will need a way to make this visible. This will also play in implementation 2. Take the Linux kernel, for example, it supports loadable kernel modules in order to add new features, drivers, etc. to the kernel, without having to compile it into one large binary file. Modules can use EXPORT_SYMBOL to indicate that a particular character (i.e., Function) is available to other called modules. The kernel keeps track of which modules are loaded, which functions they export, and to which addresses.
For # 2, you can use shared library support for the OS. On Linux and other Unix, these dynamic libraries are ELF files ( .so ) that are loaded by the dynamic loader into the process address space. On Windows, this is a DLL. Usually this download is automatically processed for you when you start your process. However, the application can use the dynamic loader to explicitly load additional modules of its choice. On POSIX you would call dlopen() , and on Windows you would use LoadLibrary() . Any of these functions will return some kind of handle that will allow you to make further requests or requests for the module.
Then your module may be required (according to your design) to export the codingfreak_init function, which is called by your application the first time the module is loaded. This function will then make additional calls to your infrastructure or return data to indicate what tools it requires and provides.
This is all very general information, which should lead to the rotation of your wheels.