Getting started with Rust and shinqlx

Since minqlx seems to stand for “Mino’s Quake Live eXtension” and I go by the player name of ShiN0 in QL, I thought an obvious name for my Rust implementation of minqlx would be shinqlx for ShiN0’s Quake Live eXtension.

But before we dive into the first steps I took, maybe a few introductory words and maybe some references in case you want to go a similar route.

Getting started with Rust

Having learned one or another programming language in my life time, I thought just getting started with Rust would be easy. All you need to learn is some syntax and the standard library. After falling into that trap for some days, unable to produce anything working at all, I took by heart the following references:

  • The Rust Programming Language book is a great resource for learning the syntax and the standard library in Rust and getting familiar with some of the concepts in Rust.
  • Buiding on that book, Rustlings is a github repository that you can clone, and working through the exercises there. Indeed, there are many solutions to the problems in the Rustlings challenges, in case you get stuck. One by one you get familiar with the concecpts of the language, and the data types.
  • Right now, I am working through the different items in the Effective Rust book. I just wished I came alongside this earlier, but it’s never too late to do some refactorings to clean up the mess you created in your first learning steps.

And, in case you don’t want to go through all this, yet still want to follow me along, here are some basic introductory words from someone that did most stuff in Java back in the days.

Rust main building block are crates. You can think of those being libraries, and there is a great pool of crates from others that you can use out of the box most of the time. Confusingly crate may also stand for an application crate. More or less, a crate is a bundled set of at least one compiled source file, you can have more than one.

Traits in the Rust world can be thought of interfaces in other languages. There is a large basic set of traits the language ships with. If you use external crates in your programs, there may be more traits to come.

Rust builds on explicit borrowing of values. Once you borrowed a variable to another function, you can no longer call certain functions with it, unless that function returns the borrowing before. This leads to fewer problems with concucrrency and leaked memory.

Rust also can interoperate with other languages. For the sake of our project here, we can annotate functions to be extern “C”, so that the compiler offers the option for C-programs to call your Rust function. Usually the process macro #[no_mangle] helps here to tell the compiler not to mangle your extern “C” functions, in other words, they will be known to the C-world by the same name you use in the Rust world.

You can also call C functions with the right set-up. Rust considers extern “C” functions as “unsafe”, and the compiler will prompt you to put such calls into unsafe{} blocks in your code. That does not lead to down-graded performance or anything like a try..catch block in other languages. It just means that you tell the compiler that you put the right thoughts into place to make sure it’s safe to call that unsafe function at this point in the program.

Let’s get started.

First thing first: forwarding from the C-hooks to Rust

In order to comply with the goals I stated in my previous blog entry, I decided to go for the replacement of the C-hooks in Rust. The idea would be to have the C code call our Rust code, that will then delegate some things back to the original minqlx C-code – up until we know how we want to replace that. Let’s work through one of such replacements.

The simplest thing is probably the ClientSpawn hook. As a reminder: minqlx pre-loads itself in Linux before the Quake Live dedicated server is loaded and started. It searches for interesting functions and hooks up its own replacement functions that call the original Quake Live function, while forwarding the particular game event towards the python world, where server plugins then can customize the play experience for the players on that server. ClientSpawn usually gets called after a player connected successfully, and entered a match, and his player gets spawned into the server. Here is the original C source that we want to transfer to Rust:

void __cdecl My_ClientSpawn(gentity_t* ent) {
    ClientSpawn(ent);
    ClientSpawnDispatcher(ent - g_entities);
}

ClientSpawn is the original Quake Live function that gets called first. ClientSpawnDispatcher is the forwarding dispatcher to the python world. gentity_t is a Quake Live native game entity, which could be a player, a rocket, or other map entities that players can interact with. ent – g_entities calculates the player’s client id. g_entities is a long list of all currently available game entities. This struct holds the players on the server always in its first 64 entries.

Ideally, we could write a ShiNQlx_ClientSpawn function, let the C-world know about that function, replace the whole hooking mechanism there going from My_ClientSpawn towards ShiNQlx_ClientSpawn, and we are done.

Here is the corresponding Rust function:

#[no_mangle]
pub extern "C" fn ShiNQlx_ClientSpawn(ent: *mut gentity_t) {
        extern "C" {
            static g_entities: *mut gentity_t;
            static ClientSpawn: extern "C" fn(*const gentity_t);
            fn ClientSpawnDispatcher(ent: c_int);
        }

    unsafe { ClientSpawn(ent) };
    unsafe { ClientSpawnDispatcher(ent.offset_from(g_entities) };
}

Let me explain. The first extern “C” block declares various external C functions and structs. We will probably need the original g_entities from the Quake Live engine. We certainly still have to call the game’s ClientSpawn function. For the time being, we will leave the ClientSpawnDispatcher with the original minqlx source, and call it directly here, until we made up our mind on how to forward directly from Rust to Python.

When we put this function into the quake_common.h header file, replace the hook from My_ClientSpawn with ShiNQlx_ClientSpawn, we notice, it works. But is it really holding up to the safety claims of Rust?

The various Rust sources always call for “creating a safe interface” for the Rust world whenever you want to interoperate with the outside C-world. Our function still gets a pointer to a gentity_t, which I also explicitly declared as mutable pointer here. Thanks to Adrian Heine who recommended the safe interface in this way. Here is a version of the above function after added some more Rust structs, that is somewhat more “safe” and Rust-like:

#[no_mangle]
pub extern "C" fn ShiNQlx_ClientSpawn(ent: *mut gentity_t) {
    let Some(game_entity): Option<GameEntity> = ent.try_into().ok() else {
        return;
    };

    QuakeLiveEngine::default().client_spawn(&mut game_entity);
    extern "C" {
        fn ClientSpawnDispatcher(ent: c_int);
    }
    unsafe { ClientSpawnDispatcher(game_entity.get_client_id() };
}

pub(crate) trait ClientSpawn {
    fn client_spawn(&self, ent: &mut GameEntity);
}

impl ClientSpawn for QuakeLiveEngine {
    fn client_spawn(&self, ent: &mut GameEntity) {
        extern "C" {
            static ClientSpawn: extern "C" fn(*const gentity_t);
        }

        unsafe { ClientSpawn(ent.gentity_t) };
    }
}
pub(crate) struct GameEntity {
    gentity_t: &'static mut gentity_t,
}

impl TryFrom<*mut gentity_t> for GameEntity {
    type Error = &'static str;

    fn try_from(game_entity: *mut gentity_t) -> Result<Self, Self::Error> {
        unsafe {
            game_entity
                .as_mut()
                .map(|gentity| Self { gentity_t: gentity })
                .ok_or("null pointer passed")
        }
    }
}

impl GameEntity {
    pub(crate) fn get_client_id(&self) -> i32 {
        extern "C" {
            static g_entities: *mut gentity_t;
        }

        unsafe { (self.gentity_t as *const gentity_t).offset_from(g_entities) as i32 }
    }
}

The Rust standard trait TryFrom creates a safe Rust struct out of our raw C pointer, or delivers an Error. We encapsulate the raw gentity_t with a GameClient struct in Rust. The Result of the TryFrom trait in Rust here has the type Result<GameEntity, Error>. You can convert Results into Options by calling the .ok() function on it. Then you will either have Some(game_entity) or None. The declaration let Some(game_entity)… extract the GameEntity from the Option<GameEntity> here to further work with. The else block behind that simply returns if the conversion failed.

Since we are identifying the client_id from the GameEntity, this seems like behavior that really wants to be on the GameEntity struct, so I moved all the calculation there, declaring the g_entities there as well. Then we can call the ClientSpawnDispatcher of minqlx directly with the calculated client_id.

Phew, quite a bit of boiler-plate code, but it seems to work. So, I went ahead and transferred in a similar manner (with more logic) the various hooking function as best as I could to Rust. Once everything ran, I was able to start-up the server, and see where this went. After a few more round of fiddling, I managed to get a running server and a sort of Hello World for our ShiNQlx project. How did I do that? Let me go into the cargo build system and build steps to have C code compiled in there in the next blog entry.

  • Print
  • Twitter
  • LinkedIn
  • Google Bookmarks

Leave a Reply

Your email address will not be published. Required fields are marked *