I recall discussions in the early software craft days on the mailing lists. One thread, in particular, comes to mind today, that was about whether a good crafter should learn a new programming language every year to learn new coding styles and paradigms, and maybe bring those back to their usual work environment.
Last year, Craig Larman recommended learning the programming language Rust to help, especially with embedded programming. Linus Torvalds also allowed Rust as a third language in the Linux kernel development around the same time. It took me a while to actually try it when I found the perfect for me pet project to dive into.
Over the next couple of blog entries, I want to dive into some of my learnings and approaches. Today, let me give you an introduction to the project I picked for my personal Rust learning curve, where I am, and maybe give you an update now and then about the things I still have open to learn.
Project Introduction
For a while now, I have been playing a bit of Quake Live, a first-person shooter. I was amazed when I found out that there is a server extension that allows you to customize the server experience by writing plugins in Python. minqlx which stands for Mino’s Quake Live eXtension (I think, and found out way too late) is a C-to-Python bridge that preloads (with some Linux LD_ magic) on the server side, and offers such extension possibilities. Since the server behavior has at times been a bit buggy, and I heard lots of things about Rust’s speed and stability, that project came into consideration for me. Maybe I can manage to port all this C stuff over to Rust, and maybe I can extend what’s already in there with some additional functionality. Yeah, I oftentimes consider myself better at taking another one’s initial idea and thinking it further.
Let’s take a look into the different parts and tricks used in minqlx before trying to port some or maybe all of it over to Rust.
Library constructor
minqlx runs as a library pre-loaded before the actual game server. So, the library gets constructed before the game server loads, then finds interesting game functions to hook into and replace the default behavior with additional functionality, like forwarding stuff to the Python world.
In dllmain.c right at the bottom of the file, there is the main entry point to the minqlx library:
__attribute__((constructor))
void EntryPoint(void) {
if (strcmp(__progname, qzeroded))
return;
SearchFunctions();
// Initialize some key structure pointers before hooking, since we
// might use some of the functions that could be hooked later to
// get the pointer, such as SV_SetConfigstring.
#if defined(__x86_64__) || defined(_M_X64)
// 32-bit pointer. intptr_t added to suppress warning about the casting.
svs = (serverStatic_t*)(intptr_t)(*(uint32_t*)OFFSET_PP_SVS);
#elif defined(__i386) || defined(_M_IX86)
svs = *(serverStatic_t**)OFFSET_PP_SVS;
#endif
// TODO: Write script to automatically set version based on git output
// when building and then make it print it here.
DebugPrint("Shared library loaded!\n");
HookStatic();
}
There is not much in here. serverStatic_t is a Quake struct where the static contents of the server will be placed in. HookStatic() is the first entry point for the real magic later on. I will spare you the majority of code snippets from here on, as the blog entry would become too long.
Hooks
Quake Live has some static portions, but the majority of the server runs on a virtual machine in memory. minqlx first of all hooks into some static functions, in order to be able to hook into the VM functions later on. While trying to understand the logic in the C-world in order to replicate it in the Rust-world, I learned that you can replace function calls in libraries with your own functions by just replacing the pointer to the function called in the lookup table. (Yeah, it’s more complicated than that, but for brevity, we’ll leave it at this explanation.)
SearchFunctions in the main constructor looks up the addresses of the functions we want to hook into, while HookStatic hooks into the static functions of the server library. Later on, there is a similar approach taken for finding the various VM functions we would be interested in, and hooking into these as well, replacing the original call with our own delegate, i.e. ideally calling the original function alongside with triggering some logic on the python side to have plugins notice things, or react to game events. Let’s see how this is done.
Python embed and Python dispatchers
python_embed.c and python_dispatchers.c are responsible for the majority of the work here. The different replacement functions will translate the C types into suitable Python types and then make them available to customize game logic in the Python plugins.
The various dispatchers consist of a bridge between our hooking functions that we intersected in the game server, and the Python world, while the embed functions provide additional functionality on the Python side via direct Python functions that can be used in your plugins. For example, in python minqlx.player_state(player_id) will return you a Python object reflecting the current state of the player with that id that is derived from the various Quake Live structures and translated in the according Python embed function.
Quake structures
In the source files, one header file holds type definitions of all the different Quake types and enums. That is quake_common.h. The majority of the types in there have probably been reverse-engineered from open source counter-part games, and probably lots of hours of tinkering.
There are more source files in there, but these will be the main focus for my efforts on transforming the C-world of minqlx towards Rust. But hold on, there are a few different goals a Rust implementation needs to serve while I am underway.
Project goals
There are many useful plugins for minqlx out there. I don’t want to have to port all of them over. So, a backward compatibility to minqlx would be ideal, so that plugin authors don’t need to re-write all their useful plugins.
The resulting library should also be comparable in size to the C one. As it stands, the 64-bit release version of minqlx has roughly 120kB in size. We certainly should not waste several megabytes with our version.
Speaking of 64-bits: minqlx also provides a 32-bit product in the compilation process. Yeah, this is an old game. We will try to focus on 64-bits, the standard nowadays, but if we can also provide the ability to run this on a 32-bit machine, that would be ideal.
Oh, and finally, the result should have at least the same stability as the C version, or players and server admins will not want to use it.
Let’s see how far we can take this.
One thought on “So…, I got a bit rusty lately”