I've been wanting to write this post for quite some time now. I feel it's time to share some of my thoughts about how the monolithic version of BennuGD is designed.
If you've followed the bennugd-wii blog since it began, you may already know that the current design is the third iteration of the monolithic version of BennuGD. This article is obviously about the current design.
Why do we need a monolithic version of BennuGD?
Some of you might know that one of the main design goals of BennuGD after forking Fenix was to become modular so, why do we need a monolithic approach?
Well, even if the modular approach seems to have many advantages over a monolithic one, not all the systems support it. BennuGD relies heavily on dynamic libraries (dlls if you're on Windows, so files if you're in most Unix-like systems or dylibs if you're on OSX) and the operating system provides a way of dealing with them.
In most systems, that way is called libdl and it works really well. If you look inside your system you'll realize that there are literally thousands of dynamic libraries lying there, so the system works.
In other -usually smaller- systems we don't have that system nor any other equivalent one and it turns out that some of those systems are particularly interesting for us in the game-making world* so we must find a way to avoid that limitation while conserving as much of the modular design as possible.
But that makes loading libraries that weren't compiled in impossible, doesn't it?
Yep, that's why I call the design monolithic. It would be perfectly possible/pretty easy to create a design based on the current one that combined both ideas by including some modules inside the main binary and loaded others from external module files, but I didn't bother to create it as I don't feel it makes much sense.
How does BennuGD handle modules?
In trying to understand how to bypass the limitation, we must first understand how BennuGD works with the modules.
This is -roughly- what happens when you ask BennuGD to import a module (let's use mod_video as an example):
- BennuGD will look for a file that follows the system naming convention for dynamic libraries in a set of pre-defined directories in your system. For example, in Windows BennuGD would first look for mod_video.dll in the current working directory. Let's imagine the file is found there.
- BennuGD asks the system to load the library and quits if it finds any errors.
- It asks the system for a list of symbols that define compose it. For example, mod_video_constants_def** would be an array containing the constants defined by the library and mod_video_functions_exports would be an array of the functions that the module provides. There are many more that the libraries can define and they're all completely optional.
- Based on the info gathered from mod_video.dll and the rest of the modules BennuGD creates a table with all the information they provide and goes on to compile your code taking that information into account.
So, how to bypass that process?
Well, the best way I could think of when creating the monolithic approach is to construct that table of symbols by hand when creating the BennuGD binaries and compile all their functionality inside the interpreter. Obviously I also had to change the code that performed steps 1-3 so that instead of looking for the symbols in the actual module files it looks for them in the array. As a result when the library loader module is asked to load a module it looks for the library in the array and returns its position in the array (it literally returns a number: look for this library's symbols in the nth row in the array).
You can have a look at that table here; lines 137 and on.
It turns out that the BennuGD compiler and the interpreter need slightly different symbols: the compiler doesn't really care about what your program does as long as it is written correctly and therefore it only cares about the symbols that define how a correctly-written program should look like. So it needs to know how a function should be called (which is the function name, what type of/how many arguments it accepts) but it doesn't care at all about what it does.
The interpreter, in the other hand, needs to know everything about what that function does when called.
There are, also, symbols that both parts need to know about (which are a particular libraries' dependencies, for example) and others that are only useful to the interpreter and therefore I chose to split the list of symbols in two: one with the symbols only needed by the interpreter and another one with the symbols useful to both the compiler and the interpreter.
Defining the actual symbols
If you have a look at the code linked above, you'll find that the symbols themselves are not defined in that file but are included from another file called mod_video_symbols.h.
That file doesn't exist in upstream BennuGD and it explicitly declares the the symbols required by the BennuGD compiler and tells the C compiler what the names of the rest of the symbols are and that they're complete declaration will come from somewhere else (hence the extern word in the declaration after the #else clause).
You can see that the last column in the functions list is set to 0 when compiling BGDC. That column should tell BennuGD what C function to call when the user calls that BennuGD function. As I said, the compiler doesn't care about what that function does and therefore we don't need to declare it. If we did declare it we'd have to compile all the code for all the modules into BGDC, too. There are other more complex reasons to not try to compile all the module code into BGDC, too.
When compiling the module code, the symbols come from mod_video.c which is the unmodified module file from upstream.
So you're writing the symbols many times!
Yes. The issue would be very easily solvable if I were the main author of BennuGD and that was how the second iteration of the architecture worked.
Unfortunately that meant that any patch from upstream takes a lot of work to be integrated as ALL the symbol-defining files had to be modified by hand: when a change came from upstream I had to locate the exact line where that change was done and apply it by hand into my code. Taking into account that each patch might contain hundreds of changes split across multiple files, that made maintaining the project a huge task even if we forgot that the main goal of the project was not to create a monolithic build of BennuGD but to create a port of BennuGD to a set of new platforms, which meant quite a bit of work by itself.
Right now I can just drop the files from the upstream version of BennuGD into my source tree and upload the code to the SVN server. I still have to be careful with the patches to supoprt platforms not available upstream (Wii, PSP, iOS & Android) but this new way of working makes things much simpler than before.
Advantages of this approach
To start, DCBs compiled with upstream BennuGD work just fine -endianess issues aside- in the monolithic builds and vice-versa.
The modular approach isn't gone. Even if the modules are all inside the binaries, they won't get loaded until you actually import them. This saves memory in some systems and from the user's point of view it's transparent: both architectures behave consistently to him.
My code is prepared to be compiled as either a monolithic build or the normal BennuGD modular structure so you can compile both from the same source code.
So that's pretty much it, hope to hear your thoughts in the comments and hope I made it clear in case it's useful to anybody.
* Consoles like the Wii or the PSP don't support it and other systems like the iPhone do support it but Apple seems to be very restrictive about the use of dynamic library loading in third party applications.
** That's why you cannot just rename your modules, BennuGD would then look for the wrong symbols.
[Update 2015-07-14] Updated the link to monolithic_includes.h