Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I pass each element of a slice as a separate argument to a variadic C function?

Tags:

rust

variadic

I'm building a Redis Module in Rust. I've found some good examples, but I'm stuck when dealing with interfacing a C function that is supposed to accept variadic arguments.

The Redis Module C SDK has a function called RedisModule_Call which accepts a few specific arguments then n arguments that represent a Redis command. From the Redis Module SDK documentation (in C):

RedisModuleCallReply *reply;
reply = RedisModule_Call(ctx,"INCR","sc",argv[1],"10");

RedisModule_Call's first three arguments are specific, but the rest represent Redis commands that could easily have hundreds of arguments.

In Rust, I'm following the patterns in Redis-Cell which is a Redis module implemented (successfully) in Rust. The module is fantastic but has a very limited way of dealing with this particular problem. Effectively, it accepts up to three arguments in a somewhat brute force way:

pub fn call(&self, command: &str, args: &[&str]) -> Result<Reply, CellError> {
   // ... code ... 
   let raw_reply = match args.len() {
        1 => raw::call1::call(/* ... */),
        2 => raw::call2::call(/* ... */),
        // ...

These call1 and call2 functions are practically just stubs that handle the different argument lengths:

pub mod call2 {
    use redis::raw;

    pub fn call(
        ctx: *mut raw::RedisModuleCtx,
        cmdname: *const u8,
        fmt: *const u8,
        arg0: *mut raw::RedisModuleString,
        arg1: *mut raw::RedisModuleString,
    ) -> *mut raw::RedisModuleCallReply {
        unsafe { RedisModule_Call(ctx, cmdname, fmt, arg0, arg1) }
    }

    #[allow(improper_ctypes)]
    extern "C" {
        pub static RedisModule_Call: extern "C" fn(
            ctx: *mut raw::RedisModuleCtx,
            cmdname: *const u8,
            fmt: *const u8,
            arg0: *mut raw::RedisModuleString,
            arg1: *mut raw::RedisModuleString,
        ) -> *mut raw::RedisModuleCallReply;
    }
}

I need to be able to pass in n arguments, with n being determined at run time, so this method of hard coding isn't practical. I know Rust has limited support for variadic functions and I've been doing some reading about RFC 2137, but I'm not sure this applies.

I'm looking for a way to apply an argument vector to the end of RedisModule_Call or something like a spread syntax for the arguments. I'm relatively new to Rust but I've searched and searched and I can't seem to find any way to scratch this itch in Rust.

To clarify - I can pass arguments into RedisModule_Call (which is variadic) no problem, but I can't find a syntactical way to pass in a variable number of arguments in Rust into the C function. What I'm trying to accomplish is something like this:

impl Redis {
    pub fn call(&self, command: &str, args: &[&str]) -> Result<Reply, CellError> {
        /* ... */

       unsafe { RedisModule_Call(ctx, cmdname, fmt, ...args) }
       /* ... */ 

Where ...args is some sort of black magic to that would allow args to represent 1 argument or 100 which would be the equivalent of RedisModule_Call(ctx, cmdname, fmt, args[0], args[1] /* ... and so on */).

like image 691
stockholmux Avatar asked Mar 06 '23 05:03

stockholmux


1 Answers

You don't, at least not yet, and I'd wager probably never.

To be able to do this, you'd need two key abilities, both of which are outside of your control:

  1. Redis needs to provide a function that accepts a va_list argument, not just a ....

    It's strange that Redis doesn't already provide such a function, but perhaps this is a sign that other people implementing modules avoid the problem entirely.

  2. Rust needs to provide a way to construct the va_list argument.

    While it looks like RFC 2137 will introduce a VaList type, the proposed API does not provide a way to create one or set values in it.

Note that you can't do what you want, even in C (at least not easily or portably).


What can you do instead? Assuming that you are implementing the code that consumes the variadic arguments, you can remove the variation from your call. A collection of items in C is just a pointer and a length, so pass that instead:

extern "C" {
    fn call(n_args: i32, ...);
}

fn x(args: &[i32]) {
    unsafe { call(2, args.len(), args.as_ptr()) };
}

If you didn't have control of what reads the code on the other side, one possible (read: terrible) idea is to pattern match on some "large enough" subset of the slice and dispatch off to the variadic function:

extern "C" {
    fn call(n_args: i32, ...);
}

fn x(args: &[i32]) {
    unsafe {
        match args {
            [] => call(0),
            [a] => call(1, a),
            [a, b] => call(2, a, b),
            _ => panic!("Didn't implement this yet"),
        }
    }
}

See also:

  • How to wrap a call to a FFI function that uses VarArgs in Rust?
  • Populating a va_list
  • Forward an invocation of a variadic function in C
like image 96
Shepmaster Avatar answered May 10 '23 14:05

Shepmaster