With newer versions of Redis, the modules API lets users extend Redis' already powerful functionality. To quote the Redis website:
Redis modules make possible to extend Redis functionality using external modules, implementing new Redis commands at a speed and with features similar to what can be done inside the core itself.
The module API is defined in a single header file with the main goal of writing them in C but also
allowing for any language with C bindings. With that in mind, I decided to try and write a simple
module using Rust since it offers a stable FFI story. Once I learned that you can introduce new data
types I ended up creating a simple MultiMap
type considering Redis' hash type only supports one
value. I was able to leverage many builtin types in Rust with the hardest part being getting values
from the API into a Rust type. It made more sense as I continued to develop but it wasn’t obvious at
first. The rest of this post will go from start to finish while writing a module using Rust.
This was an intimidating part at first but it got immensely simpler once I found the correct tools.
I was quite impressed with how easy it was to integrate binding generation into a cargo pipeline.
Cargo offers a build script feature
which let me create the API bindings to use in my FFI module. The first thing I tried was the
bindgen crate since it would handle a lot of boilerplate for me.
I was able to copy the example verbatim in to a build.rs
script and then copy the the module
example from Redis' documentation only to run into an issue that I wasn’t familiar with. I hadn’t used
C in a while and forgot that static
functions will not be exported in a header file. Since the
RedisModule_OnLoad
function is static, there were no bindings generated for it. In order to work
around this I had to write a few lines of C to export my own function which was then called from
the API. In order to use this I ended up downloading the cc crate. After
I had my simple example working I didn’t need to worry about the bindings again since they were all
generated behind the scenes with these helpful tools.
Rust offers a great set of common data structures which I was able to use to implement a MultiMap
.
By embedding the type inside my own structure, I could write functions that the FFI calls could use
to store new data or read values from a given key. Here is the definition of the MultiMap
:
/// `MultiMap` represents a map of `String` keys to a `Vec` of `String` values.
#[derive(Clone, Debug, Default)]
pub struct MultiMap {
inner: HashMap<String, Vec<String>>,
}
Since this is really just a HashMap
my implementation just used its available functions for everything I needed. Given the power of
Rust’s trait-based generics I was able to write a simple insert function for example.
/// Insert will add all values passed as an argument to the specified key.
pub fn insert<K, I>(&mut self, key: K, values: I)
where
K: Into<String>,
I: IntoIterator<Item = String>,
{
let entry = self.inner.entry(key.into()).or_insert_with(|| vec![]);
values.into_iter().for_each(|item| entry.push(item));
}
The MultiMap
module turned out to be less than eighty lines allowing me to focus on using the
Redis API to implement it natively.
In order to implement a new data type, the API asks for a custom structure along with a function
call to RedisModule_CreateDataType
. This structure allows the user to define things like how it
is loaded into memory or serialized to disk along with a unique name. After that is initialized, you
are able to create commands for this type using the RedisModule_CreateCommand
function.
In order to create a new data type, you must define how it can be serialized to disk, read from the
snapshot file, and how to handle freeing its memory. In the MultiMap
example we have to use Option
types to represent
function pointers that the API expects. These functions help with the persistence that was mentioned
earlier. These functions were relatively straightforward to write once I found a good way to represent
the structure on disk.
In order to create a command that Redis can understand you can use the RedisModule_CreateCommand
function and pass it values such as a command name, command function, and various flags. As an example,
I will walk through my length function which returns the length of the values stored at a given key.
The first few lines enable automatic memory management as defined by the API as well as a check for the number of arguments for the function:
ffi::RedisModule_AutoMemory.unwrap()(ctx);
if argc != 3 {
return ffi::RedisModule_WrongArity.unwrap()(ctx);
}
Then I unpack the arguments from the C array into a Rust slice and then grab the key name denoted by the second argument in the slice:
let args = slice::from_raw_parts(argv, argc as usize);
let key = ffi::RedisModule_OpenKey.unwrap()(
ctx,
args[1],
(ffi::REDISMODULE_READ as i32) | (ffi::REDISMODULE_WRITE as i32),
) as *mut ffi::RedisModuleKey;
After this, we do a validity check on the key to make sure it is actually a MultiMap
type:
if invalid_key_type(key) {
return reply_with_error(ctx, ffi::ERRORMSG_WRONGTYPE);
}
Then we can work with the key and access the Rust API that I created:
let map = ffi::RedisModule_ModuleTypeGetValue.unwrap()(key) as *mut MultiMap;
if map.is_null() {
ffi::RedisModule_ReplyWithLongLong.unwrap()(ctx, 0);
} else {
let m = &mut *map;
let map_key = string_from_module_string(args[2]);
ffi::RedisModule_ReplyWithLongLong.unwrap()(ctx, m.key_len(map_key) as i64);
}
This is the function used above to convert a RedisModuleString
into something that Rust could use
while interacting with the MultiMap
.
/// Perform a lossy conversion of a module string into a `Cow<str>`.
pub unsafe extern "C" fn string_from_module_string(
s: *const ffi::RedisModuleString,
) -> Cow<'static, str> {
let mut len = 0;
let c_str = ffi::RedisModule_StringPtrLen.unwrap()(s, &mut len);
CStr::from_ptr(c_str).to_string_lossy()
}
We end our function by returning an integer to denote the success or failure following the Unix tradition of zero and one respectively.
I enjoyed working with Rust’s FFI capabilities and didn’t run into too many issues. Once I grasped how Rust interacts with C enough it was easy to make progress using the Redis API. My current implementation could use some polish and more tests. I will list the repository below for anyone who is interested. Feel free to open issues, make pull requests, or just ask questions as well.
Thanks for reading. The repository can be found here. More information on Redis modules can be found here.