Devlog #5 - I spent 8 hours overengineering my desktop application to avoid spaghetti code
A few days ago I wrote about how my small desktop app was already suffering from spaghetti code due to interaction between its modules. I had originally set out to create an application with just 3 modes: Gravity, Electrostatic movement, and chemical bonds. But this has a big problem: The scope is small, making development uninteresting and the actual application pointless to users.
The Change
In order to solve my spaghetti code problem, I came up with a solution: Instead of implementing 3 different modes, implement an interface that allows you to add as many modes as you want.
The Process
My first step was to decide what the public API should be for creating a new module. I settled for this:
What the module author needs
The module author may want to intercept a few different things:
- Change Force, Velocity and Position values every frame when they are being evaluated
- Add an attribute for a particle (like how a particle needs a charge in electrostatics)
- Add a universal physical constant (like Coulomb’s constant or the gravitational constant)
- Render something on top of the application
What I need to do
I built a module system to register all of the above, plus a psim_module_api.hpp header for the module author. It defines a C-ABI entry point that calls a C++ init function the author provides. I was almost done with the entire process when I discovered that Windows apparently doesn’t properly support the ability of DLLs to access functions in the main program by linking against them at compile time, and had to rewrite a large chunk from scratch.
All the user has to do is include the header and define a void initializePSimModule() which is forward-declared by the header and called by the entry point after populating the function pointer table.
Then we just use user-friendly functions to register anything the module might need. An example module is shown in the screenshots, along with its source code. This module creates constants for boundaries that particles cannot cross, effectively boxing them in a rectangle.
I rewrote the Gravity and Electric modules in the module system. They are part of the core application, so I’m not loading them dynamically every time, but they use the same API as dynamic extensions. This makes it essentially impossible to create spaghetti code around the different modes, and now all I have to do to load a new module into the program is call loadModule(const std::string& path).
The final flow is something like this: The module entry point is a function that receives a function pointer table to all the C-ABI functions that the module uses under the hood. It populates the module’s function pointer table (the one I added because of Windows) and then calls the main initialization function implemented by the module author.
To put things in perspective, here is what happens when you register a constant:
You call registerConstant(...) with a std::string and a std::function => they’re destructured into C-friendly structs => the module finds _regConstant(const char* module, size_t moduleLen, ..., PSIM_Constant_Change_Callback) in the pointer table and calls it => the main program restructures the C parameters back into C++ types => the main program calls the real registration function.
And this is precisely the hard part: The module author can believe that he’s dealing with the same exact std::span<Particle> that is passed to him by the main program, but in reality that span was destructured into a Particle* and a size_t and was then restructured, all before his callback was even called.
This took me 8 hours, and out of everything I’ve done with PSimUltimate for now, this has definitely been the most valuable learning experience. I learned how to expose C++ APIs while maintaining a stable C ABI backend. Very cool!
Comments 2
Yo, this is actually so sick!
@kallenseelo thank you so much!! it was extremely satisfying to get working
Sign in to join the conversation.