Ryan1729
Ryan1729

Reputation: 1010

Is there a more idiomatic way to keep an optional argument string from being freed?

I want to take command-line arguments in a Rust program and pass them to a C function. However, these arguments are optional and the program should behave differently if no arguments are supplied. I have read the docs for CString::as_ptr but I had hoped that keeping a local variable containing an Option containing the argument, (if it exists,) would keep that String from being freed as in the following example.

This Rust code:

extern crate libc;

use std::ffi::CString;

extern "C" {
    fn print_in_c(opt_class: *const libc::c_char) -> libc::c_int;
}

fn main() {
    let mut args = std::env::args();
    //skip execuatble name
    args.next();

    let possible_arg = args.next();

    println!("{:?}", possible_arg);

    let arg_ptr = match possible_arg {
        Some(arg) => CString::new(arg).unwrap().as_ptr(),

        None => std::ptr::null(),
    };

    unsafe {
        print_in_c(arg_ptr);
    };
}

Along with this C code:

#include <stdio.h>
int
print_in_c(const char *bar)
{
  puts("C:");
  puts(bar);

  return 0;
}

But this didn't work. The code prints out the following when passed an argument of "foo":

Some("foo")
C:

Followed by a blank line.

I got the program to print the correct text if I change the Rust code to the following:

extern crate libc;

use std::ffi::CString;

extern "C" {
    fn print_in_c(opt_class: *const libc::c_char) -> libc::c_int;
}

fn main() {
    let mut args = std::env::args();
    //skip execuatble name
    args.next();

    let possible_arg = args.next();

    println!("{:?}", possible_arg);

    let mut might_be_necessary = CString::new("").unwrap();

    let arg_ptr = match possible_arg {
        Some(arg) => {
            might_be_necessary = CString::new(arg).unwrap();
            might_be_necessary.as_ptr()
        }

        None => std::ptr::null(),
    };

    unsafe {
        print_in_c(arg_ptr);
    };
}

When run, this prints

Some("foo")
C:
foo

as expected.

This method technically works, but it is awkward to extend to multiple arguments and results in a compiler warning:

warning: value assigned to `might_be_necessary` is never read
  --> src/main.rs:19:9
   |
19 |     let mut might_be_necessary = CString::new("").unwrap();
   |         ^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: #[warn(unused_assignments)] on by default

Is there a better way to do this?

Upvotes: 3

Views: 905

Answers (1)

user4815162342
user4815162342

Reputation: 155246

The problem is that your code is creating a temporary CString but holding on to just a pointer. The actual CString is dropped, while the dangling pointer is passed to the C function. To understand what's going on, it is useful to expand the pattern match to a more verbose form:

let arg_ptr = match possible_arg {
    Some(arg) => {
        let tmp = CString::new(arg).unwrap();
        tmp.as_ptr()
    } // <-- tmp gets destructed here, arg_ptr is dangling
    None => std::ptr::null(),
};

Safe Rust prevents dangling pointers by only supporting pointer indirection through references, whose lifetimes are carefully tracked by the compiler. Any use of a reference that outlives the object is automatically rejected at compile time. But you are using raw pointers and an unsafe block which prevents those checks from taking place, so you need to manually ensure proper lifetimes. And indeed, the second snippet fixes the problem by creating a local variable that stores the CString for long enough for its value to outlive the pointer.

The prolonged lifetime comes at the cost of an additional local variable. But fortunately it can be avoided - since you already have a local variable that holds the pointer, you can modify that to store the actual CString, and extract the pointer only when actually needed:

let arg_cstring = possible_arg.map(|arg| CString::new(arg).unwrap());
unsafe {
    print_in_c(arg_cstring.as_ref()
               .map(|cs| cs.as_ptr())
               .unwrap_or(std::ptr::null()));
}

There are several things to notice here:

  • arg_cstring holds an Option<CString>, which ensures that CString has storage that can outlive the pointer passed to the C function;
  • Option::as_ref() is used to prevent arg_cstring from being moved into map, which would again free it before the pointer was actually used;
  • Option::map() is used as an alternative to pattern matching when you want to express "do something with Option if Some, otherwise just leave it as None".
  • The pattern x.as_ref().map(|x| x.as_ptr().unwrap_or(null()) can and probably should be moved into a utility function if it is used more than once in the program. Be careful that the function takes reference to Option to avoid a move.

Upvotes: 8

Related Questions