Hello! Welcome to the final lesson in our module on function pointers and callbacks.
In our last session, we implemented the Strategy pattern, which was a powerful way to make algorithms interchangeable. We created a context that could switch between different behaviors, but all those behaviors were known at compile time and linked into the final executable. Today, we're going to take that concept of dynamic behavior one giant leap further.
Your learning outcome for this lesson is to design a simple plugin architecture using function pointers. We'll explore how a host application can discover and load new functionality at runtime from separate library files, without being recompiled. This is a cornerstone of modular, extensible software, from web servers like Apache to complex IDEs and, in many ways, the modular service-oriented architectures you work with in automotive systems.
The Big Idea: Decoupling at Runtime
Imagine your ADAS gateway application. The core logic is stable, but you frequently need to add support for new types of data processing, logging formats, or communication protocols developed by different teams. Recompiling and re-validating the entire monolithic application for each new feature is slow, risky, and inefficient.
A plugin architecture solves this. The main application (the host) provides a stable core and defines a set of rules (an API). External modules (plugins) are developed independently. At runtime, the host can discover these plugins, load them into its own memory space, and execute their functionality through the predefined API.
The "glue" that makes this possible consists of two key C-level concepts:
- Dynamic Loading: The ability of an application to load a shared library (
.dllon Windows,.soon Linux) into memory at runtime. - Function Pointers: The mechanism used to define the API contract, allowing the host to call functions inside the loaded library without knowing their implementation at compile time.
Part 1: The Core Mechanism of Dynamic Loading
Before we design the high-level architecture, let's understand the low-level mechanics. Operating systems provide a specific API for dynamic loading. On POSIX systems like Linux, this is provided by the dlfcn.h header. The key functions are:
void* dlopen(const char* filename, int flags): Loads the shared library specified byfilename. It returns a "handle" (a generic pointer) to the loaded library, orNULLon failure.void* dlsym(void* handle, const char* symbol): Searches for a symbol (like a function name) within a library previously opened withdlopen. It returns avoid*pointer to that symbol's address.int dlclose(void* handle): Unloads the library, decrementing its reference count.
The process is a straightforward sequence: open the library, find the function you need, cast it to the correct function pointer type, and then call it.
The following article provides a concise, practical demonstration of this process.
It's All In The Libs – Building A Plugin System Using ...
This article, 'It's All In The Libs', by [s-g] on Hackaday, gives a clear, command-line based introduction to dynamic loading. It cuts straight to the essential mechanism.
Please read the section 'Dynamically Loading A Shared Library'. Focus on the dynload.c example. Notice the sequence: dlopen gets the handle, dlsym finds the address of the double_me function, and this address is then assigned to a function pointer.
While the example uses Linux's dlfcn.h, the concept is identical on Windows, where you would use LoadLibrary() to get a handle and GetProcAddress() to find the function's address. Your experience with diverse toolchains in automotive development means you're likely familiar with this cross-platform reality. The principle remains the same.
Part 2: Designing the Host-Plugin Contract (The API)
Simply loading one function is a start, but a real plugin needs a richer contract. It might need initialization, a main execution function, and a cleanup function. It may also need to provide metadata about itself, like its name or version.
This is where we bring back the pattern from our last two lessons: defining an interface using a struct of function pointers.
- The Host Application defines a
structcontaining function pointers for all the operations a plugin is expected to support. Thisstructis the API. - The Plugin implements the functions for each of these pointers and also implements one special, agreed-upon "entry point" function (e.g.,
get_plugin_api()). - The Interaction: The host calls
dlopen()on the plugin file, then callsdlsym()to get a pointer to theget_plugin_api()entry point. It calls this function, which returns thestructfilled with pointers to the plugin's actual implementation functions. The host can now call any of the plugin's functions through this struct.
The following resource illustrates a complete, albeit more complex, architecture based on this principle. We will focus on the core API design and loading mechanism.
Simple Plug-in Architecture in Plain C
The article 'Simple Plug-in Architecture in Plain C' by user 'krishjk' on CodeProject provides a deep dive into building a plugin system. We will focus on the architectural principles and the C implementation of the API.
Please read the following sections, focusing on the concepts and ignoring the GTK-specific GUI code: 'The Architecture' (section 1) and 'The API' (section 3): These sections explain why a formal API is needed and how it separates the host from the plugin. Plugin Interface Definition (within section 5): Look for the _PluginStruct. This is the core API contract, defined as a structure of function pointers (nameProc, runProc, etc.). Also, note the CREATEPROC typedef—this is the entry point function that the host will look for. Plugin Loading Logic (within section 8): Study the load_all_plugins function. This is the host's side. Pay close attention to the sequence: plugin_helper_load_plugin (which calls LoadLibrary), then GetProcAddress to find plugin_app_create_plugin, and finally, the most important part: calling pls = create() to get the plugin's structure of function pointers. The host then uses these pointers, for example, connecting runproc to a menu action with g_signal_connect.
As you can see, the CodeProject example is a perfect illustration of our pattern. The host knows nothing about the plugin's internal workings. It only knows that it can load the library, call a creation function to get a _PluginStruct, and then use the function pointers within that struct. This is a robust and highly decoupled design.
Part 3: Putting It All Together - A Practical Design
Let's synthesize what we've learned from both resources into a minimal, complete design. We'll use the simple "Telephone" game idea from the Hackaday article but structure it with the more robust API pattern from the CodeProject article.
Imagine our host program wants to apply a series of transformations to a string. Each transformation is a plugin.
1. The API Header (plugin_api.h)
This is shared by the host and all plugins.
#ifndef PLUGIN_API_H
#define PLUGIN_API_H
// The struct defining our plugin's capabilities
typedef struct {
// Returns the name of the plugin
const char* (*get_plugin_name)();
// The main processing function
void (*process_data)(char* data);
// A cleanup function
void (*cleanup)();
} PluginAPI;
// The one function every plugin MUST export.
// The name "create_plugin" is the symbol the host will look for.
PluginAPI* create_plugin();
#endif // PLUGIN_API_H
2. A Plugin Implementation (uppercase.c)
This would be compiled into a shared library (uppercase.so or uppercase.dll).
#include "plugin_api.h"
#include <ctype.h>
#include <stdio.h>
const char* name_func() {
return "Uppercase Converter";
}
void process_func(char* data) {
for (int i = 0; data[i] != '\0'; i++) {
data[i] = toupper(data[i]);
}
}
void cleanup_func() {
// Nothing to clean up in this simple plugin
printf("Uppercase plugin cleaned up.\n");
}
// Global instance of our API struct
static PluginAPI plugin_interface = {
.get_plugin_name = name_func,
.process_data = process_func,
.cleanup = cleanup_func
};
// The exported entry point
PluginAPI* create_plugin() {
printf("Creating uppercase plugin.\n");
return &plugin_interface;
}
3. The Host Application (main.c)
This loads and uses the plugin.
#include <dlfcn.h>
#include <stdio.h>
#include <string.h>
#include "plugin_api.h"
int main(int argc, char* argv[]) {
if (argc < 3) {
printf("Usage: %s <plugin.so> <string>\n", argv[0]);
return 1;
}
const char* plugin_path = argv[1];
char data_string[256];
strncpy(data_string, argv[2], 255);
data_string[255] = '\0';
// 1. Load the plugin library
void* handle = dlopen(plugin_path, RTLD_LAZY);
if (!handle) { /* error handling */ return 1; }
// 2. Find the entry point function
PluginAPI* (*create_func)();
create_func = dlsym(handle, "create_plugin");
if (!create_func) { /* error handling */ dlclose(handle); return 1; }
// 3. Call the entry point to get the API struct
PluginAPI* plugin = create_func();
printf("Original data: %s\n", data_string);
printf("Loaded plugin: %s\n", plugin->get_plugin_name());
// 4. Use the plugin's functions via the API
plugin->process_data(data_string);
printf("Processed data: %s\n", data_string);
plugin->cleanup();
// 5. Unload the library
dlclose(handle);
return 0;
}
Test your understanding!
Based on the design above, imagine you need to create a second plugin called reverse.c that reverses the string in place.
- What would the
process_funcfor this new plugin look like (just the core logic)? - How would you compile and run the host application to use your new
reverseplugin instead of theuppercaseone?
Show answer
-
The
process_funcwould contain standard string reversal logic. The rest of the plugin file (reverse.c) would be structured identically touppercase.c, just with this function swapped in.void reverse_process_func(char* data) { int len = strlen(data); for (int i = 0; i < len / 2; i++) { char temp = data[i]; data[i] = data[len - 1 - i]; data[len - 1 - i] = temp; } } -
You would compile
reverse.cinto its own shared library, for example,reverse.so.gcc -shared -fPIC -o reverse.so reverse.cThen, you would run the exact same, unchanged host executable, simply passing the new plugin's path as a command-line argument:
./main reverse.so "hello world"The host program does not need to be recompiled to gain this new functionality, which is the entire point of the plugin architecture.
Conclusion
Today, we've reached a peak in our study of function pointers, seeing how they enable one of the most powerful patterns in software engineering: runtime extensibility. By combining dynamic library loading with an API contract defined by a struct of function pointers, you can create systems that are incredibly modular, flexible, and maintainable.
Key Takeaways:
- A plugin architecture consists of a host application, one or more plugin modules (shared libraries), and a shared API that defines the contract between them.
- The core mechanism is dynamic loading, using OS functions like
dlopen/dlsym(Linux) orLoadLibrary/GetProcAddress(Windows). - Function pointers are the ideal tool in C to create the API contract. A
structof function pointers provides a clean, comprehensive interface for a plugin's capabilities. - The host's responsibility is to load the plugin, retrieve the function pointer struct via a known entry-point function, and then call the plugin's code through those pointers.
Preview of the Next Lesson:
This lesson concludes our module on function pointers. We've gone from the fundamentals to implementing complex design patterns. Our next module, "Type Qualifiers and Const Correctness," will shift our focus to another critical area for writing safe, robust, and optimized C code. We will start by learning to distinguish between a 'pointer to const data' and a 'const pointer', a crucial skill for designing clear and safe APIs like the one we built today.