Building a Modern Cross-Platform C++ Plugin System with Runtime Loading and dlfcn-win32
Created:
Sun 05 July 2026written by Xavier Figuera
Last modification:
Sun 05 July 2026Building a Modern Cross-Platform C++ Plugin System with Runtime Loading and dlfcn-win32
Modern software is expected to be modular, maintainable and easy to extend. Whether you're developing a game engine, a rendering engine, a CAD application, an editor, or any other extensible C++ application, there is one question that almost every large project eventually needs to answer:
How can new functionality be added without recompiling the entire application?
One of the most elegant solutions is to adopt a plugin architecture.
Instead of linking every module directly into the executable, optional functionality is distributed as independent shared libraries that are loaded dynamically at runtime.
This approach has become a standard design pattern across the software industry because it provides excellent scalability, clean separation of responsibilities and a high degree of flexibility.
In this article we'll explore how to build a modern cross-platform C++ plugin system using:
- Modern C++
- Runtime loading
- Shared libraries (DLL / SO)
- Abstract interfaces
- Factory functions
- CMake
- dlfcn-win32
Although the example presented here is intentionally simple, the architecture closely resembles those used by professional software such as game engines, rendering engines and large desktop applications.
New to C++ plugin architectures?
This article is a comprehensive reference guide intended for readers who want to explore every aspect of the implementation.
If you're looking for a quicker introduction, you may prefer the companion tutorial:
Table of Contents
- Building a Modern Cross-Platform C++ Plugin System with Runtime Loading and dlfcn-win32
- Table of Contents
- Source code repository link
- Static Linking vs Plugin Architecture
- Why Use a Plugin Architecture?
- Typical Applications That Use Plugins
- Project Goals
- High-Level Architecture
- Layered Architecture
- Compile-Time Dependencies
- Runtime Dependencies
- Runtime Loading Overview
- Why This Design Works
- What's Next?
- Designing the SDK
- The Role of the SDK
- The SDK Architecture
- Why Is the SDK Header-Only?
- The Dependency Graph
- Interface-Based Design
- Why Use Abstract Interfaces?
- The Dependency Inversion Principle
- Shared Data Structures
- Versioning the SDK
- What Should Never Be Inside the SDK?
- SDK Summary
- Implementing the Plugin
- The Plugin Architecture
- Why Doesn't the Application Create Plugin Objects?
- The Factory Pattern
- Exporting Factory Functions
- Why Use extern "C"?
- Why Export Functions Instead of Classes?
- Creating Objects Inside the Plugin
- Destroying Objects
- Object Lifetime
- Encapsulation
- Plugin Summary
- Building a Plugin Manager
- Why Do We Need a Plugin Manager?
- Responsibilities of the Plugin Manager
- High-Level Architecture
- Separating Responsibilities
- The Plugin Lifecycle
- Discovering Plugins
- Runtime Discovery
- Loading the Library
- Managing Library Handles
- Resolving Exported Symbols
- Validating the Plugin
- Why Hide dlopen()?
- Error Handling
- Benefits of a Plugin Manager
- Plugin Manager Summary
- Runtime Loading with dlopen(), dlsym() and dlfcn-win32
- What Is Runtime Loading?
- What Happens When dlopen() Is Called?
- The Library Handle
- Why Use RTLD_NOW?
- Resolving Symbols with dlsym()
- Symbol Resolution
- Calling the Factory Function
- Releasing the Library
- Object Lifetime Matters
- Cross-Platform Loading
- Why Use dlfcn-win32?
- The Complete Runtime Loading Process
- Runtime Summary
- Organizing the Project with CMake
- Project Organization
- Why Separate the Projects?
- Installing the SDK
- Building the SDK
- Using find_package()
- Building the Plugin
- Building the Host Application
- Build Order
- Deploying the Plugin
- Running the Application
- Example Output
- CMake Summary
- Frequently Asked Questions
- What is a C++ plugin?
- What is a plugin architecture?
- Why use plugins instead of static libraries?
- Why use abstract interfaces?
- Why shouldn't the application instantiate plugin classes directly?
- Why use factory functions?
- Why use extern "C"?
- What is name mangling?
- Why not export entire C++ classes?
- What is dlopen()?
- What is dlsym()?
- What is dlclose()?
- Why use RTLD_NOW?
- What is dlfcn-win32?
- Why use dlfcn-win32 instead of LoadLibrary()?
- Can multiple plugins implement the same interface?
- Can plugins depend on other plugins?
- Can plugins be loaded after the application starts?
- Is a Plugin Manager really necessary?
- Can this architecture scale to a game engine?
- Does this architecture work on Linux?
- Can this architecture support dozens of plugins?
- Is this architecture suitable for commercial software?
- Common Mistakes
- What's Next?
- Final Thoughts
Source code repository link
Flow - C++ Plugin System Example - Flow Labs Research
Static Linking vs Plugin Architecture
Before implementing a plugin system, it's worth understanding how it differs from a traditional statically linked application.
Although both approaches are perfectly valid, they solve different problems.
A statically linked application is often simpler to build and deploy, making it an excellent choice for smaller projects.
As applications grow, however, adding new functionality frequently requires rebuilding and redistributing the entire application.
A plugin architecture takes a different approach.
Instead of embedding every feature directly into the executable, optional functionality is moved into independently compiled shared libraries that are loaded only when required.
The following table summarizes the main differences.
| Static Linking | Plugin Architecture |
|---|---|
| Functionality is linked at compile time | Functionality is loaded at runtime |
| Rebuilding is required after adding new features | New plugins can often be added without recompiling |
| Strong compile-time dependencies | Loose coupling through interfaces |
| Larger executable | Smaller core application |
| Difficult to distribute optional features | Features can be distributed independently |
| Tightly integrated modules | Modular and extensible design |
| Simple deployment | Flexible deployment |
Neither approach is universally better.
For small utilities, static linking is often the simplest solution.
For large extensible applications—such as game engines, rendering engines, IDEs or CAD software—a plugin architecture usually provides greater flexibility and long-term maintainability.
Why Use a Plugin Architecture?
Imagine you're developing a modern rendering engine.
The engine has one primary responsibility:
Render frames efficiently.
Everything else should remain as modular as possible.
Although a rendering engine depends on many subsystems, not every one of them belongs inside the core engine.
For example, the engine may require services responsible for:
- Loading the engine's native mesh format
- Loading the engine's native material format
- Resource serialization
- Shader compilation
- Material compilation
- Texture processing
- Scene loading
- Editor integration
A common mistake is to compile every one of these systems directly into the rendering engine.
At first this approach appears simple.
As the project grows, however, several problems begin to emerge.
- Every new subsystem requires rebuilding the rendering engine.
- Compile times continue to increase.
- The engine becomes increasingly difficult to maintain.
- Internal dependencies multiply over time.
- Individual systems cannot evolve independently.
- Replacing or upgrading a subsystem becomes progressively harder.
Eventually, the rendering engine becomes responsible for much more than rendering.
A better approach is to separate the engine from the services it consumes.
Instead of embedding every subsystem into the engine, the core renderer defines a set of abstract interfaces while independent plugins provide the concrete implementations.
For example, the rendering engine may simply request a mesh loader through an interface such as IFlowMeshLoader.
The engine does not know—or need to know—whether that loader reads a binary file, a memory stream, a network resource or any other data source.
Its only concern is that the requested service fulfills the contract defined by the SDK.
flowchart LR
Engine["Rendering Engine"]
SDK["FlowRenderEngineSDK"]
Plugin["FlowToolsFormats"]
Mesh["IFlowMeshLoader"]
Material["IFlowMaterialLoader"]
Engine --> SDK
Plugin --> SDK
Plugin --> Mesh
Plugin --> Material
Engine -. Uses Interfaces .-> Mesh
Engine -. Uses Interfaces .-> Material
This architecture cleanly separates responsibilities.
The rendering engine focuses exclusively on rendering.
Plugins implement specialized services.
The SDK defines the communication contract between both sides.
As a result, new functionality can be introduced without modifying or recompiling the core engine.
This separation not only reduces coupling but also makes the entire architecture easier to maintain, extend and test over time.
Typical Applications That Use Plugins
Plugin systems are everywhere.
Many of the applications developers use every day rely heavily on dynamically loaded modules.
Some examples include:
- Game engines
- Rendering engines
- CAD software
- IDEs
- Image editors
- Video editors
- Audio workstations
- Scientific visualization software
- Simulation software
- Database tools
In all these cases, the core application provides the framework while plugins provide specialized functionality.
This separation allows independent development teams to work on different components without modifying the core application.
Project Goals
This project is intentionally designed to demonstrate the architectural concepts behind a modern plugin system rather than implementing a large feature set.
The main goals are:
- Build a reusable plugin architecture.
- Separate interfaces from implementations.
- Load plugins dynamically at runtime.
- Keep compile-time dependencies minimal.
- Support both Windows and Linux.
- Organize projects using modern CMake.
- Demonstrate clean software engineering practices.
Although the project is relatively small, the same architecture scales naturally to considerably larger applications.
High-Level Architecture
The entire project is divided into four independent modules.
flowchart TD
App["SomeGraphicApp"]
Graphics["FlowRenderEngineGraphics"]
Plugin["FlowToolsFormats"]
SDK["FlowRenderEngineSDK"]
App --> Graphics
Graphics -->|"Runtime Loading"| Plugin
SDK -. Shared Interfaces .-> Graphics
SDK -. Shared Interfaces .-> Plugin
Each module has a very specific responsibility.
| Component | Responsibility |
|---|---|
| FlowRenderEngineSDK | Defines the public interfaces shared by every project |
| FlowRenderEngineGraphics | Loads plugins and manages their lifetime |
| FlowToolsFormats | Implements the SDK interfaces |
| SomeGraphicApp | Uses the public interfaces exposed by the graphics library |
Notice something important.
The application never communicates directly with the plugin.
Instead, every interaction passes through abstract interfaces defined inside the SDK.
This keeps both sides completely independent.
Layered Architecture
Another way to visualize the system is by looking at it as a layered architecture.
flowchart TB
Application["Application"]
Graphics["Graphics Library"]
SDK["Shared SDK"]
Plugin["Plugin"]
Implementation["Concrete Loaders"]
Application --> Graphics
Graphics --> SDK
Plugin --> SDK
Plugin --> Implementation
Each layer has a single responsibility.
This is one of the fundamental principles behind maintainable software architecture.
Compile-Time Dependencies
One of the primary objectives of this architecture is minimizing compile-time dependencies.
Only the SDK is shared between projects.
graph LR
SDK["FlowRenderEngineSDK"]
Graphics["FlowRenderEngineGraphics"]
Plugin["FlowToolsFormats"]
Application["SomeGraphicApp"]
SDK --> Graphics
SDK --> Plugin
SDK --> Application
Graphics --> Application
Notice what is missing.
There is no compile-time dependency between the application and the plugin.
The application does not even know that the plugin exists.
This dramatically reduces coupling and allows plugins to evolve independently.
Runtime Dependencies
Once the application starts, the relationships between components change completely.
Instead of relying on compile-time linking, the graphics library loads plugins dynamically.
graph LR
App["SomeGraphicApp"]
Manager["Plugin Manager"]
DLL["FlowToolsFormats.dll"]
Loader["FlowMeshLoaderPlugin"]
App --> Manager
Manager -. dlopen() .-> DLL
DLL --> Loader
This is one of the key differences between traditional software architectures and plugin-based systems.
Compile-time dependencies disappear, replaced by runtime discovery.
The application becomes significantly more flexible because plugins can be replaced, updated or extended without rebuilding the executable.
Runtime Loading Overview
From the application's point of view, requesting a mesh loader is surprisingly simple.
sequenceDiagram
participant App
participant Graphics
participant Plugin
App->>Graphics: Request Mesh Loader
Graphics->>Plugin: dlopen()
Graphics->>Plugin: dlsym(CreateMeshLoader)
Plugin-->>Graphics: IFlowMeshLoader*
Graphics-->>App: IFlowMeshLoader*
The application never instantiates any concrete implementation.
Instead, it receives a pointer to an abstract interface.
This is a classic application of dependency inversion, one of the SOLID principles.
Because the application only knows about interfaces, the implementation can change at any time without affecting the application.
Why This Design Works
Although the example is intentionally compact, it incorporates several software engineering principles that are commonly found in production codebases.
Some of its main advantages include:
- Low coupling
- High cohesion
- Runtime extensibility
- Independent module development
- Faster incremental builds
- Easier maintenance
- Cleaner project organization
- Better long-term scalability
Perhaps the greatest advantage is that new plugins can be developed and distributed independently of the host application.
As long as a plugin implements the interfaces defined by the SDK, the application can discover and load it dynamically at runtime.
This level of flexibility is one of the reasons plugin architectures remain a popular design pattern across modern C++ applications.
What's Next?
Now that we've explored the overall architecture, the next step is understanding the most important component of the entire system:
The SDK.
The SDK defines the contract that both the application and every plugin must follow.
Without it, independently developed plugins would have no reliable way to communicate with the host application.
In the next section we'll design a header-only SDK that provides a stable and reusable API shared by every project in the solution.
Designing the SDK
Before writing a single line of plugin code, we first need to define how plugins will communicate with the host application.
This is arguably the most important design decision in the entire architecture.
A common mistake is allowing the application to depend directly on classes implemented by the plugin.
Although this may work for small projects, it quickly becomes difficult to maintain as the application grows.
Instead, both sides should depend only on a shared contract.
This contract is provided by the SDK.
The SDK contains no application logic.
It contains no rendering code.
It contains no file format implementation.
Its only purpose is to define the API that every plugin must implement.
The SDK therefore becomes the common language spoken by both the application and every plugin.
The Role of the SDK
The SDK acts as a contract between completely independent projects.
It defines:
- Abstract interfaces
- Shared data structures
- Enumerations
- Common type definitions
- Export macros
- Version information
Nothing else.
Because the SDK only exposes declarations, both the host application and every plugin can evolve independently while remaining binary compatible.
The SDK Architecture
The SDK sits at the center of the entire project.
Every component depends on it.
Nothing depends on the implementation.
flowchart LR
SDK["FlowRenderEngineSDK"]
Host["FlowRenderEngineGraphics"]
Plugin["FlowToolsFormats"]
Application["SomeGraphicApp"]
SDK --> Host
SDK --> Plugin
SDK --> Application
Notice that there are no direct dependencies between the application and the plugin.
Everything passes through the SDK.
This dramatically reduces coupling throughout the project.
Why Is the SDK Header-Only?
One question often arises:
Why not compile the SDK as a shared library?
The answer is surprisingly simple.
The SDK contains only declarations.
There is no implementation to compile.
It consists entirely of:
- Interface classes
- Plain data structures
- Enumerations
- Constants
- Templates
- Utility headers
Since there is no executable code, building a library would provide little benefit while introducing an unnecessary binary dependency.
Making the SDK header-only keeps the architecture lightweight and simplifies integration.
The Dependency Graph
The dependency graph is intentionally designed to point in a single direction.
graph TD
SDK["FlowRenderEngineSDK"]
Host["FlowRenderEngineGraphics"]
Plugin["FlowToolsFormats"]
Application["SomeGraphicApp"]
SDK --> Host
SDK --> Plugin
SDK --> Application
Host --> Application
This ensures that every project can be built independently.
The plugin never includes application headers.
The application never includes plugin headers.
Only the SDK is shared.
Interface-Based Design
The SDK exposes only abstract interfaces.
For example, a mesh loader may look conceptually like this.
class IFlowMeshLoader
{
public:
virtual ~IFlowMeshLoader() = default;
virtual bool LoadMesh(
const std::string& filename,
MeshAsset& mesh) = 0;
};
Notice that there is no implementation.
The SDK simply defines what a mesh loader must be capable of.
Every plugin is free to implement this interface however it chooses.
This is one of the key principles behind interface-based architectures.
Why Use Abstract Interfaces?
Abstract interfaces provide several important advantages.
They completely separate the public API from the implementation.
flowchart LR
Application
Interface["IFlowMeshLoader"]
Plugin["FlowMeshLoaderPlugin"]
Binary["FlowMeshBinaryLoader"]
Application --> Interface
Plugin --> Interface
Plugin --> Binary
The application knows only about IFlowMeshLoader.
The plugin internally decides which implementation to instantiate.
This allows implementations to evolve without affecting client code.
The Dependency Inversion Principle
This architecture is also an excellent example of the Dependency Inversion Principle (DIP).
Instead of high-level modules depending on low-level modules, both depend on abstractions.
flowchart TB
Application
SDK["Interfaces"]
Plugin
Application --> SDK
Plugin --> SDK
Because both projects depend on abstractions rather than implementations, they remain completely independent.
This is one of the reasons plugin architectures scale so well.
Shared Data Structures
Interfaces alone are not enough.
The application and the plugin also need to exchange data.
The SDK therefore contains shared data structures such as:
- MeshAsset
- MaterialLibrary
- Vertex
- IndexBuffer
- TextureInfo
These structures define the format of the information exchanged between both sides.
Since both projects include the same headers, they always agree on the memory layout of the exchanged data.
Versioning the SDK
One challenge with plugin systems is maintaining compatibility over time.
As new functionality is added, interfaces inevitably evolve.
A common solution is to associate a version number with the SDK.
SDK Version
Major : 1
Minor : 0
Patch : 0
Plugins can expose the SDK version they were compiled against.
When the host application loads a plugin, it can verify that both versions are compatible before creating any objects.
This simple validation prevents many difficult-to-debug runtime errors.
What Should Never Be Inside the SDK?
The SDK should remain as small and stable as possible.
Avoid placing implementation details inside the shared contract.
For example, the SDK should never contain:
- Rendering code
- OpenGL code
- Vulkan code
- File parsers
- Internal helper classes
- Application logic
- Resource managers
Doing so would unnecessarily increase coupling between projects.
A good SDK is intentionally boring.
Its only purpose is to define a stable communication contract.
SDK Summary
The SDK is the foundation of the entire plugin architecture.
Rather than sharing implementations, every project shares only a common contract composed of interfaces and data structures.
This simple design has several important consequences.
- The application never depends on plugin implementations.
- Plugins can be developed independently.
- Compile-time dependencies remain minimal.
- Binary compatibility becomes easier to maintain.
- The architecture scales naturally as new plugins are added.
With the SDK in place, we are finally ready to implement our first plugin.
In the next section we'll build the plugin itself, export factory functions and see how objects are created dynamically at runtime.
Implementing the Plugin
With the SDK in place, we can finally implement our first plugin.
The plugin is responsible for providing the concrete implementation of the interfaces defined by the SDK.
Unlike the SDK, this project contains real application code.
For example, it may include:
- Mesh loaders
- Material loaders
- Texture decoders
- Scene importers
- Binary parsers
- Internal helper classes
These implementation details remain completely hidden from the application.
The only visible part of the plugin is the public interface exposed through the SDK.
The Plugin Architecture
Internally, the plugin is free to organize its code however it wishes.
One possible architecture is shown below.
flowchart TD
Plugin["FlowToolsFormats"]
MeshPlugin["FlowMeshLoaderPlugin"]
MaterialPlugin["FlowMaterialLoaderPlugin"]
MeshBinary["FlowMeshBinaryLoader"]
MaterialBinary["FlowMaterialBinaryLoader"]
Plugin --> MeshPlugin
Plugin --> MaterialPlugin
MeshPlugin --> MeshBinary
MaterialPlugin --> MaterialBinary
Notice that none of these implementation classes are visible outside the plugin.
The application never knows they exist.
Why Doesn't the Application Create Plugin Objects?
One common question is:
Why doesn't the application simply include the plugin headers and create the objects directly?
For example:
FlowMeshLoaderPlugin loader;
Although this appears convenient, it completely defeats the purpose of a plugin architecture.
Doing so would create a compile-time dependency between the application and the plugin.
Every time the plugin changes, the application would need to be rebuilt.
Instead, the application should only know about the abstract interface.
IFlowMeshLoader* loader;
This keeps both projects completely independent.
The Factory Pattern
If the application cannot instantiate plugin classes directly, who creates them?
The answer is simple.
The plugin creates its own objects.
The application merely asks the plugin to do so.
This design pattern is known as the Factory Pattern.
flowchart LR
Application
Factory["CreateMeshLoader()"]
Plugin["FlowMeshLoaderPlugin"]
Application --> Factory
Factory --> Plugin
Instead of exposing constructors, the plugin exposes factory functions.
The host application calls these functions to obtain interface pointers.
Exporting Factory Functions
A typical plugin exports functions similar to these.
extern "C"
{
FLOW_PLUGIN_EXPORT
IFlowMeshLoader* CreateMeshLoader();
FLOW_PLUGIN_EXPORT
void DestroyMeshLoader(IFlowMeshLoader* loader);
}
Only these functions are visible outside the shared library.
Everything else remains private.
This keeps the plugin implementation completely encapsulated.
Why Use extern "C"?
This is one of the most important aspects of any C++ plugin system.
Without extern "C" every C++ compiler performs name mangling.
For example, the function
CreateMeshLoader()
may become something like
?CreateMeshLoader@@YAPEAVIFlowMeshLoader@@XZ
or
_Z16CreateMeshLoaderv
depending on the compiler.
The application would have no reliable way to locate these symbols.
Using extern "C" disables name mangling and guarantees a predictable exported symbol.
CreateMeshLoader
This allows dlsym() and GetProcAddress() to locate the function reliably on every platform.
Why Export Functions Instead of Classes?
Another common mistake is attempting to export entire C++ classes from a shared library.
Although technically possible, this often creates several problems.
Different compilers may generate different ABIs.
Even different versions of the same compiler can introduce binary incompatibilities.
Exporting small C-style factory functions avoids these issues almost entirely.
Instead of exposing implementation classes, the plugin exposes a stable binary interface.
flowchart LR
Application
Factory["CreateMeshLoader()"]
Interface["IFlowMeshLoader"]
Plugin["FlowMeshLoaderPlugin"]
Application --> Factory
Factory --> Plugin
Plugin --> Interface
The application never interacts with the concrete implementation.
Everything passes through the interface.
Creating Objects Inside the Plugin
Internally, the factory function simply creates the concrete implementation.
Conceptually it behaves like this.
IFlowMeshLoader* CreateMeshLoader()
{
return new FlowMeshLoaderPlugin();
}
The application never knows which class has actually been instantiated.
The plugin is free to change its implementation at any time.
Tomorrow the factory could return
FlowMeshLoaderPluginV2
or
CompressedMeshLoader
without requiring any modification to the application.
This flexibility is one of the greatest strengths of interface-based architectures.
Destroying Objects
If the plugin creates the object, it should also destroy it.
Instead of writing
delete loader;
the application calls another exported function.
DestroyMeshLoader(loader);
This guarantees that object creation and destruction occur inside the same module.
It also avoids problems related to different runtime libraries.
Object Lifetime
The complete lifecycle of a plugin object looks like this.
sequenceDiagram
participant App
participant Plugin
App->>Plugin: CreateMeshLoader()
Plugin-->>App: IFlowMeshLoader*
App->>App: Use interface
App->>Plugin: DestroyMeshLoader()
Plugin-->>Plugin: delete object
Ownership is very clear.
The plugin owns the implementation.
The application owns only the interface pointer.
Encapsulation
One of the major advantages of this architecture is encapsulation.
The plugin hides everything except its public API.
flowchart LR
subgraph Plugin
Factory
Interface
Implementation
Parser
Utilities
end
Application --> Factory
Factory --> Interface
Interface --> Implementation
Implementation --> Parser
Implementation --> Utilities
The application cannot accidentally depend on internal classes because they are never exposed.
This keeps the architecture clean even as the project grows.
Plugin Summary
The plugin is responsible for implementing the interfaces defined by the SDK while hiding all implementation details.
Rather than exporting C++ classes directly, it exposes a small set of factory functions that create and destroy interface objects.
This design provides several important advantages.
- Stable binary interfaces.
- Better encapsulation.
- Lower compile-time coupling.
- Cleaner project organization.
- Greater flexibility.
- Easier long-term maintenance.
With the plugin implemented, the only remaining challenge is loading it dynamically at runtime.
In the next section we'll build the Plugin Manager, discover exported symbols using dlopen() and dlsym(), and create plugin instances without introducing any compile-time dependency on the plugin itself.
Building a Plugin Manager
At this point we have already designed the SDK and implemented the plugin.
However, there is still one important piece missing.
The application needs a mechanism capable of discovering plugins, loading shared libraries, locating exported symbols, creating plugin instances and releasing every resource correctly.
This responsibility belongs to the Plugin Manager.
Although it is often overlooked in small examples, the Plugin Manager is one of the most important components of any production-ready plugin architecture.
Rather than exposing platform-specific APIs throughout the application, it centralizes every aspect of plugin management behind a clean and reusable interface.
This greatly simplifies the rest of the codebase.
Why Do We Need a Plugin Manager?
A common beginner implementation looks something like this.
void* plugin = dlopen("FlowToolsFormats.dll", RTLD_NOW);
auto create =
reinterpret_cast<CreateMeshLoaderFn>(
dlsym(plugin, "CreateMeshLoader"));
IFlowMeshLoader* loader = create();
Although this works, it creates several problems.
- Platform-specific code appears throughout the application.
- Error handling becomes repetitive.
- Plugin lifetime becomes difficult to manage.
- Resource cleanup is easy to forget.
- Every subsystem must understand how dynamic loading works.
As applications grow, this quickly becomes difficult to maintain.
Instead, all runtime loading should be delegated to a dedicated Plugin Manager.
Responsibilities of the Plugin Manager
The Plugin Manager has a single responsibility:
Manage the complete lifecycle of every plugin loaded by the application.
This includes:
- Discovering plugins
- Loading shared libraries
- Validating plugins
- Resolving exported symbols
- Creating plugin instances
- Destroying plugin instances
- Managing library handles
- Reporting errors
- Unloading plugins
Notice that none of these responsibilities belong to the application itself.
The application should only request services.
It should never care where those services come from.
High-Level Architecture
The Plugin Manager acts as an intermediary between the application and every plugin.
flowchart LR
Application
PluginManager
Plugin
Factory
Interface
Application --> PluginManager
PluginManager --> Plugin
Plugin --> Factory
Factory --> Interface
Interface --> Application
From the application's perspective there is only one object to interact with.
Everything else remains hidden.
Separating Responsibilities
One of the primary goals of the Plugin Manager is keeping each component focused on a single responsibility.
flowchart TD
Application["Application"]
Manager["Plugin Manager"]
Library["Shared Library"]
Plugin["Plugin"]
Loader["Mesh Loader"]
Application --> Manager
Manager --> Library
Library --> Plugin
Plugin --> Loader
Each layer performs one specific task.
This makes the architecture considerably easier to understand and maintain.
The Plugin Lifecycle
Every plugin follows the same lifecycle.
stateDiagram-v2
[*] --> Discovered
Discovered --> Loaded
Loaded --> SymbolsResolved
SymbolsResolved --> Active
Active --> Released
Released --> Unloaded
Unloaded --> [*]
Keeping these states explicit greatly simplifies debugging.
It also makes it easier to report meaningful error messages whenever a plugin fails during initialization.
Discovering Plugins
The first responsibility of the Plugin Manager is locating available plugins.
For small examples, loading a single DLL directly is perfectly acceptable.
FlowToolsFormats.dll
However, larger applications usually search one or more plugin directories.
A typical layout might look like this.
Application/
SomeGraphicApp.exe
plugins/
FlowToolsFormats.dll
AssimpPlugin.dll
ObjImporter.dll
GltfImporter.dll
ExperimentalLoader.dll
This approach allows new functionality to be added simply by copying a new plugin into the plugins directory.
No recompilation is required.
Runtime Discovery
The discovery process usually follows a very simple workflow.
flowchart TD
Start
Scan["Scan plugins directory"]
Found{"Plugin found?"}
Load["Load library"]
Next["Continue scanning"]
End
Start --> Scan
Scan --> Found
Found -->|Yes| Load
Found -->|No| Next
Load --> Next
Next --> End
Each plugin is treated independently.
A failure while loading one plugin should not necessarily prevent the application from loading the remaining plugins.
This makes the system significantly more robust.
Loading the Library
Once a plugin has been discovered, the Plugin Manager attempts to load it.
Conceptually, the process is very straightforward.
sequenceDiagram
participant Manager
participant Library
Manager->>Library: dlopen()
Library-->>Manager: Library Handle
If the library loads successfully, the Plugin Manager stores the returned handle for future use.
This handle becomes the application's connection to every exported function contained inside the plugin.
Managing Library Handles
Each successfully loaded plugin produces a library handle.
The Plugin Manager is responsible for keeping these handles alive for as long as any object created by the plugin still exists.
flowchart LR
PluginManager
Handle1["Library Handle"]
Handle2["Library Handle"]
PluginA
PluginB
PluginManager --> Handle1
PluginManager --> Handle2
Handle1 --> PluginA
Handle2 --> PluginB
Destroying a library handle too early would invalidate every object created by that plugin.
For this reason, handles usually remain alive until application shutdown.
Resolving Exported Symbols
Loading the shared library is only the first step.
The Plugin Manager must also locate the exported factory functions.
flowchart LR
Library
dlsym
Create["CreateMeshLoader"]
Destroy["DestroyMeshLoader"]
Library --> dlsym
dlsym --> Create
dlsym --> Destroy
Only after every required symbol has been successfully resolved can the plugin be considered usable.
If even one required symbol is missing, the plugin should be rejected.
Validating the Plugin
A production-ready Plugin Manager typically performs several validation steps before exposing the plugin to the rest of the application.
Typical checks include:
- Required symbols exist.
- SDK versions are compatible.
- Plugin version is supported.
- Initialization succeeds.
- Mandatory interfaces are available.
Only after every validation step succeeds should the plugin become active.
Why Hide dlopen()?
One of the main objectives of the Plugin Manager is preventing the rest of the application from depending on platform-specific APIs.
Without a Plugin Manager the application quickly becomes littered with code such as:
dlopen()
dlsym()
dlclose()
Instead, higher-level systems interact with a much cleaner interface.
auto loader = pluginManager.GetMeshLoader();
The application no longer cares whether the loader came from:
- A DLL
- A shared object
- A built-in module
- A remote plugin
- A test implementation
This level of abstraction makes the architecture significantly easier to extend.
Error Handling
Dynamic loading can fail for many reasons.
For example:
- Plugin not found.
- Missing dependency.
- Invalid SDK version.
- Missing exported symbol.
- Corrupted library.
- Unsupported architecture.
- Initialization failure.
A dedicated Plugin Manager provides a single location for handling all of these situations consistently.
This also makes logging much cleaner.
Benefits of a Plugin Manager
Introducing a dedicated Plugin Manager provides numerous advantages.
- Centralized runtime loading.
- Cleaner architecture.
- Better separation of responsibilities.
- Simplified error handling.
- Easier debugging.
- Better scalability.
- Improved testability.
- Platform independence.
Perhaps most importantly, the rest of the application never needs to know how plugins are actually loaded.
That implementation detail remains completely encapsulated.
Plugin Manager Summary
The Plugin Manager is the central coordinator of the entire plugin architecture.
Rather than exposing platform-specific loading APIs throughout the codebase, it encapsulates every aspect of plugin discovery, loading, validation and lifetime management behind a single reusable component.
With the Plugin Manager in place, the application simply requests the interfaces it needs while the manager transparently loads plugins, resolves exported symbols and creates the required objects.
Now that we understand the responsibilities of the Plugin Manager, we're finally ready to examine the low-level APIs that make runtime loading possible.
In the next section we'll explore dlopen(), dlsym() and dlclose(), understand how they work internally, and see how dlfcn-win32 provides a unified API across both Windows and Linux.
Runtime Loading with dlopen(), dlsym() and dlfcn-win32
With the architecture complete, it's finally time to look at the low-level API responsible for loading plugins at runtime.
Although the Plugin Manager hides these implementation details from the rest of the application, understanding how dynamic loading works provides valuable insight into the overall architecture.
Fortunately, the API required by the Plugin Manager is surprisingly small.
In most applications only three functions are needed.
dlopen()dlsym()dlclose()
Together they provide everything required to load shared libraries, locate exported symbols and release resources.
What Is Runtime Loading?
Traditional applications link every library before the executable starts.
When the operating system launches the program, all required modules are already known.
A plugin architecture works differently.
Instead of linking every library during compilation, libraries are loaded only when they are actually needed.
flowchart LR
Executable
PluginManager
SharedLibrary
Executable --> PluginManager
PluginManager -->|"Runtime"| SharedLibrary
This ability to discover new functionality while the application is already running is one of the defining characteristics of a plugin system.
What Happens When dlopen() Is Called?
Calling dlopen() involves much more than simply opening a file.
Internally, the operating system performs several operations.
flowchart TD
Request["dlopen()"]
Locate["Locate library"]
Load["Load into memory"]
Resolve["Resolve dependencies"]
Relocate["Perform relocations"]
Initialize["Run initialization code"]
Handle["Return library handle"]
Request --> Locate
Locate --> Load
Load --> Resolve
Resolve --> Relocate
Relocate --> Initialize
Initialize --> Handle
Only after all these steps succeed does the operating system return a valid library handle.
If any step fails, the library is not loaded.
The Library Handle
A successful call to dlopen() returns an opaque handle.
void* handle = dlopen(
"FlowToolsFormats.dll",
RTLD_NOW);
The application should never attempt to interpret this value.
It simply acts as a reference to the loaded shared library.
Every subsequent operation uses this handle.
For example:
- Resolving symbols
- Querying exported functions
- Closing the library
The Plugin Manager is responsible for storing and managing these handles.
Why Use RTLD_NOW?
One of the most commonly used loading flags is:
RTLD_NOW
This instructs the loader to resolve every required symbol immediately.
flowchart LR
Library
Resolve["Resolve all symbols"]
Ready["Plugin ready"]
Library --> Resolve
Resolve --> Ready
The main advantage is that loading failures occur immediately.
If a dependency is missing or an exported symbol cannot be resolved, the plugin never becomes active.
This makes debugging significantly easier.
Resolving Symbols with dlsym()
Loading a library only makes its code available.
The application still needs a way to locate exported functions.
This is the role of dlsym().
auto createMeshLoader =
reinterpret_cast<CreateMeshLoaderFn>(
dlsym(handle, "CreateMeshLoader"));
The Plugin Manager provides the library handle together with the name of the exported symbol.
If the symbol exists, a function pointer is returned.
Otherwise the lookup fails.
Symbol Resolution
The process is conceptually simple.
flowchart LR
Library
SymbolTable["Exported Symbols"]
Factory["CreateMeshLoader()"]
Library --> SymbolTable
SymbolTable --> Factory
Every shared library exposes a symbol table.
dlsym() searches this table looking for the requested function.
If the symbol is found, the application receives a callable function pointer.
Calling the Factory Function
Once the symbol has been resolved, creating objects becomes straightforward.
sequenceDiagram
participant Manager
participant Plugin
Manager->>Plugin: dlsym("CreateMeshLoader")
Plugin-->>Manager: Function Pointer
Manager->>Plugin: CreateMeshLoader()
Plugin-->>Manager: IFlowMeshLoader*
Notice that the application still never creates the object directly.
The plugin remains responsible for constructing every implementation.
Releasing the Library
Eventually the plugin is no longer required.
At that point the Plugin Manager releases the library.
dlclose(handle);
Internally this decreases the library's reference count.
When the reference count reaches zero, the operating system unloads the shared library and releases its resources.
Object Lifetime Matters
One important rule must always be respected.
Never unload a plugin while objects created by that plugin still exist.
Doing so immediately invalidates every function and every object allocated inside the shared library.
flowchart TD
PluginLoaded
ObjectCreated
ObjectDestroyed
LibraryReleased
PluginLoaded --> ObjectCreated
ObjectCreated --> ObjectDestroyed
ObjectDestroyed --> LibraryReleased
This is why the Plugin Manager usually owns every library handle until application shutdown.
Cross-Platform Loading
Windows and Linux expose different APIs for runtime loading.
| Windows | Linux |
|---|---|
| LoadLibrary() | dlopen() |
| GetProcAddress() | dlsym() |
| FreeLibrary() | dlclose() |
Maintaining separate implementations quickly becomes repetitive.
Fortunately, dlfcn-win32 provides a compatibility layer that exposes the familiar POSIX API on Windows.
From the application's point of view, runtime loading becomes identical on both platforms.
void* handle = dlopen(
"FlowToolsFormats.dll",
RTLD_NOW);
auto create =
reinterpret_cast<CreateMeshLoaderFn>(
dlsym(handle, "CreateMeshLoader"));
dlclose(handle);
The Plugin Manager no longer needs to care whether it is running on Windows or Linux.
Why Use dlfcn-win32?
Using dlfcn-win32 provides several advantages.
- One implementation for every platform.
- Cleaner source code.
- Easier maintenance.
- Simpler testing.
- Reduced use of platform-specific macros.
- Familiar POSIX interface.
Instead of maintaining two independent implementations, the Plugin Manager works with a single API.
This greatly simplifies the codebase.
The Complete Runtime Loading Process
Putting everything together, the complete loading sequence looks like this.
sequenceDiagram
participant Application
participant PluginManager
participant Library
Application->>PluginManager: Request Mesh Loader
PluginManager->>Library: dlopen()
Library-->>PluginManager: Library Handle
PluginManager->>Library: dlsym(CreateMeshLoader)
Library-->>PluginManager: Function Pointer
PluginManager->>Library: CreateMeshLoader()
Library-->>PluginManager: IFlowMeshLoader*
PluginManager-->>Application: Interface Pointer
Although several steps occur internally, the application experiences an extremely simple API.
It simply requests a mesh loader.
Everything else remains hidden behind the Plugin Manager.
Runtime Summary
Dynamic loading is one of the key technologies that makes plugin architectures possible.
Using only three functions—dlopen(), dlsym() and dlclose()—the Plugin Manager can discover shared libraries, locate exported factory functions and create interface objects without introducing any compile-time dependency on plugin implementations.
Combined with dlfcn-win32, the same source code works on both Windows and Linux, providing a clean and maintainable foundation for cross-platform C++ applications.
Now that the runtime loading mechanism is complete, the only remaining step is to organize the entire project using CMake, build every component independently and deploy the plugin alongside the application.
Organizing the Project with CMake
One of the strengths of this example is that it is intentionally split into several independent projects.
Although it would be possible to place everything inside a single CMake project, doing so would not accurately represent how plugin-based applications are typically developed.
In a real-world project, the SDK, plugins and host application are often maintained independently.
This separation improves modularity, simplifies maintenance and allows teams to work on different components without interfering with each other.
Project Organization
The solution is divided into three independent projects.
flowchart TD
SDK["FlowRenderEngineSDK"]
Plugin["FlowToolsFormats"]
Engine["FlowRenderEngine"]
SDK --> Plugin
SDK --> Engine
Each project has a clearly defined responsibility.
| Project | Purpose |
|---|---|
| FlowRenderEngineSDK | Public interfaces shared by every component |
| FlowToolsFormats | Plugin implementation |
| FlowRenderEngine | Host application and Plugin Manager |
Notice that the plugin and the engine never depend directly on each other.
Both depend only on the SDK.
Why Separate the Projects?
Keeping each project independent provides several advantages.
- Smaller repositories.
- Faster incremental builds.
- Better modularity.
- Cleaner dependency graph.
- Independent versioning.
- Easier distribution.
- Simpler maintenance.
This organization also reflects how many commercial SDKs are distributed.
Developers receive the SDK separately and build plugins against its public API.
Installing the SDK
Because the SDK is shared by every project, it needs to be discoverable by CMake.
Although the SDK is header-only, it is still installed as a CMake package.
This allows other projects to locate it using find_package().
flowchart LR
SDK["FlowRenderEngineSDK"]
Install["cmake --install"]
Package["CMake Package"]
Plugin
Engine
SDK --> Install
Install --> Package
Package --> Plugin
Package --> Engine
The SDK therefore becomes a reusable dependency that can be consumed by any project.
Building the SDK
Since the SDK contains only headers, there is no binary library to generate.
The build process simply creates and installs the package configuration files.
cmake -S . -B build
cmake --build build
cmake --install build --prefix build/install
After installation, the SDK behaves like any other CMake package.
Using find_package()
Both the plugin and the host application locate the SDK through CMake.
Conceptually the process is straightforward.
find_package(FlowRenderEngineSDK REQUIRED)
target_link_libraries(
MyTarget
PRIVATE
FlowRenderEngineSDK::FlowRenderEngineSDK)
Although the SDK is header-only, exposing it as an imported CMake target provides a consistent developer experience.
Consumers simply link against the exported interface target.
Building the Plugin
The plugin is developed independently from the engine.
The only requirement is access to the installed SDK.
cmake -S . -B build \
-DCMAKE_PREFIX_PATH=../FlowRenderEngineSDK/build/install
cmake --build build
The resulting shared library can then be copied into the application's plugin directory.
Building the Host Application
The host application follows exactly the same workflow.
cmake -S . -B build \
-DCMAKE_PREFIX_PATH=../FlowRenderEngineSDK/build/install
cmake --build build
Because both projects consume the same SDK package, they always compile against the same public interfaces.
Build Order
Since the projects are independent, they should be built in the following order.
flowchart LR
SDK["1. SDK"]
Plugin["2. Plugin"]
Engine["3. Engine"]
SDK --> Plugin
SDK --> Engine
The SDK must be installed before either the plugin or the engine can be configured.
Deploying the Plugin
After building the plugin, the generated shared library must be placed somewhere the application can locate it.
For this example, the simplest approach is placing the plugin next to the executable.
build/
bin/
SomeGraphicApp.exe
FlowRenderEngineGraphics.dll
FlowToolsFormats.dll
On Linux the plugin would typically be:
libFlowToolsFormats.so
Many production applications instead use a dedicated plugins directory.
For example:
Application/
SomeGraphicApp.exe
plugins/
FlowToolsFormats.dll
AssimpPlugin.dll
GltfPlugin.dll
ObjImporter.dll
This layout makes it possible to add or remove functionality simply by copying or deleting plugin files.
No recompilation is required.
Running the Application
When the application starts, the Plugin Manager performs the complete loading process automatically.
Conceptually the startup sequence is:
sequenceDiagram
participant App
participant Manager
participant Plugin
App->>Manager: Initialize()
Manager->>Plugin: Load shared library
Manager->>Plugin: Resolve factories
Manager->>Plugin: Create loaders
Manager-->>App: Ready
From the application's perspective, plugin loading is completely transparent.
It simply requests the interfaces it needs.
Example Output
After successfully loading the plugin, the example application produces output similar to the following.
SomeGraphicApp.exe
Mesh loaded
Vertex count: 3
Material count: 1
Although the example is intentionally small, exactly the same startup sequence can scale to applications capable of loading dozens or even hundreds of plugins.
CMake Summary
By separating the SDK, the plugin and the host application into independent CMake projects, the architecture remains clean, modular and easy to maintain.
Installing the SDK as a CMake package allows every component to share a stable public API while remaining completely independent.
This organization closely resembles the structure used by many professional software projects and provides an excellent foundation for building large extensible applications.
With the build system complete, the final step is reviewing the overall architecture, discussing best practices, answering common questions and summarizing the key ideas presented throughout this article.
Frequently Asked Questions
Building a plugin architecture is often more about software design than about dynamic loading itself.
The following questions summarize many of the most common topics developers encounter when designing extensible C++ applications.
What is a C++ plugin?
A C++ plugin is an independently compiled shared library that extends the functionality of an application without requiring the application itself to be recompiled.
Rather than being linked directly into the executable, plugins are loaded dynamically at runtime.
This allows new features to be added simply by distributing additional shared libraries.
What is a plugin architecture?
A plugin architecture is a software design pattern where the application provides a framework while optional functionality is implemented by external modules.
The host application defines a public API.
Plugins implement that API.
This separation allows both projects to evolve independently.
Why use plugins instead of static libraries?
Static libraries are linked during compilation.
Any modification requires rebuilding the application.
Plugins are loaded dynamically.
This makes it possible to distribute updates independently, reduce compile-time dependencies and extend applications without recompilation.
Why use abstract interfaces?
Abstract interfaces separate the public API from the implementation.
The application depends only on the interface.
The plugin provides the implementation.
This dramatically reduces coupling and improves maintainability.
Why shouldn't the application instantiate plugin classes directly?
Creating plugin objects directly introduces a compile-time dependency on the plugin implementation.
Using factory functions allows the plugin to remain completely independent from the host application.
Why use factory functions?
Factory functions provide a stable binary interface between the host application and the plugin.
Instead of exposing constructors, the plugin exports small C-style functions that create and destroy interface objects.
Why use extern "C"?
Without extern "C" the compiler performs C++ name mangling.
Name mangling makes exported symbols compiler-dependent.
Using extern "C" guarantees predictable symbol names that can be located using dlsym() or GetProcAddress().
What is name mangling?
Name mangling is the process by which C++ compilers encode additional information into symbol names.
This allows features such as function overloading.
Unfortunately, mangled names are compiler-specific.
Plugin systems therefore usually export C-compatible functions instead.
Why not export entire C++ classes?
Exporting complete classes can introduce ABI compatibility issues between compilers or compiler versions.
Exporting small factory functions together with abstract interfaces provides a considerably more stable binary interface.
What is dlopen()?
dlopen() loads a shared library into the application's address space and returns a handle that can later be used to locate exported symbols.
What is dlsym()?
dlsym() searches a loaded shared library for a specific exported symbol and returns its address.
Plugin managers typically use it to locate factory functions.
What is dlclose()?
dlclose() releases a previously loaded shared library.
When no remaining references exist, the operating system unloads the library from memory.
Why use RTLD_NOW?
RTLD_NOW instructs the operating system to resolve all required symbols immediately.
This causes loading errors to appear during initialization rather than later during execution.
What is dlfcn-win32?
dlfcn-win32 is a compatibility library that implements the POSIX dynamic loading API on Windows.
It allows the same source code using dlopen(), dlsym() and dlclose() to compile on both Windows and Linux.
Why use dlfcn-win32 instead of LoadLibrary()?
Using dlfcn-win32 allows the entire Plugin Manager to be implemented using a single cross-platform API.
This significantly reduces platform-specific code.
Can multiple plugins implement the same interface?
Yes.
A plugin architecture often supports multiple implementations of the same interface.
For example, several mesh importers may all implement the same IFlowMeshLoader interface while supporting different file formats.
Can plugins depend on other plugins?
Yes.
However, dependencies between plugins should generally be minimized.
Keeping plugins independent makes deployment and maintenance considerably easier.
Can plugins be loaded after the application starts?
Yes.
One of the primary advantages of runtime loading is that plugins can be discovered and loaded while the application is already running.
Is a Plugin Manager really necessary?
For small examples it may not be strictly necessary.
However, larger applications quickly benefit from centralizing plugin discovery, loading, validation, lifetime management and error handling inside a dedicated Plugin Manager.
Can this architecture scale to a game engine?
Absolutely.
The architecture presented in this article is intentionally simple, but the underlying principles scale naturally to considerably larger projects.
Game engines frequently use plugins for:
- Asset importers
- Rendering backends
- Physics engines
- Audio systems
- Scripting languages
- Editor extensions
Does this architecture work on Linux?
Yes.
Because runtime loading is implemented through the POSIX API and dlfcn-win32 provides the same interface on Windows, the Plugin Manager remains almost entirely platform independent.
Can this architecture support dozens of plugins?
Yes.
The Plugin Manager can maintain multiple library handles simultaneously and dynamically discover every compatible plugin located inside the plugin directory.
Many professional applications load hundreds of plugins using this same architectural approach.
Is this architecture suitable for commercial software?
Yes.
Separating the SDK, the host application and plugins into independent projects is a common design used by many commercial applications because it provides excellent modularity, scalability and long-term maintainability.
Common Mistakes
Although building a plugin system is conceptually straightforward, several design mistakes appear repeatedly in real-world projects.
Fortunately, most of them are easy to avoid once you understand how a plugin architecture is intended to work.
Some of the most common mistakes include:
- Exporting concrete C++ classes instead of abstract interfaces.
- Forgetting to use
extern "C"for exported factory functions. - Calling
deletefrom the host application instead of the plugin. - Unloading a shared library while objects created by that library are still alive.
- Ignoring SDK version compatibility.
- Exposing internal implementation details through the public API.
- Mixing platform-specific loading code throughout the application instead of centralizing it inside a Plugin Manager.
Avoiding these pitfalls will make your plugin architecture considerably more robust and easier to maintain.
Several of these topics deserve a much deeper discussion and will be explored in future articles.
What's Next?
The architecture presented in this article provides a solid foundation for building extensible C++ applications.
However, many advanced topics remain beyond the scope of this introduction.
Future articles in this series will explore subjects such as:
- Designing a production-ready Plugin Manager.
- Supporting multiple plugins implementing the same interface.
- SDK version negotiation and binary compatibility.
- Plugin metadata and discovery.
- Hot reloading during development.
- Building a plugin registry.
- Common ABI pitfalls in C++ plugin systems.
- Best practices for long-term maintainability.
Together, these topics will gradually evolve this simple example into a production-ready plugin architecture suitable for larger applications.
Final Thoughts
Designing a plugin architecture is about much more than simply calling dlopen().
The real challenge lies in defining stable interfaces, minimizing compile-time dependencies and creating a clean separation between the host application and plugin implementations.
By introducing a shared SDK, abstract interfaces, factory functions and a dedicated Plugin Manager, we've built an architecture that is modular, extensible and portable across multiple operating systems.
Although the example presented in this article is intentionally compact, the same principles can scale to significantly larger projects such as rendering engines, game engines, CAD software and professional desktop applications.
Hopefully this article has demonstrated that a modern C++ plugin system is not only powerful, but also remarkably elegant when built upon solid software engineering principles.
Happy coding!