Passing Vec<String> from Rust to char** in C
Asked Answered
V

2

7

I've been trying to write a shell in Rust that links directly to the libc library. I've used a Vec<String> to hold the arguments to be passed to execvp(), but it seems that my conversion to char ** has not been successful. Upon execution, all the parameters became null strings.

Here's the piece of code involved.

fn safe_execvp(path: String, argv: Vec<String>) -> Result<(), i32> {
    unsafe {
        let c_path = CString::new(path.as_str()).unwrap();
        let mut c_argv_vec = Vec::new();
        for arg in &argv {
            let c_arg = CString::new(arg.as_str()).unwrap().as_ptr();
            c_argv_vec.push(c_arg);
        }
        c_argv_vec.push(std::ptr::null());
        match execvp(c_file.as_ptr(), c_argv_vec.as_ptr()) {
            num => Err(num),
        }
    }
}

execvp is the C library function defined as fn execvp(file: *const i8, argv: *const*const i8) -> i32;.

I'm not sure what I've done wrong. Is it because the memory for the arguments were released before calling execvp()?

V2 answered 10/3, 2017 at 11:37 Comment(0)
M
12

You are creating CString instances and immediately fetching a pointer. Consequently, as you well guessed, ownership of this string was dropped prematurely. This is similar to returning a reference to a local instance, except that since pointers do not retain lifetime information, this case does not trigger a compilation error.

The solution to your problem is to retain the owned C-style strings during the function's scope, and produce a pointer to pointers of the same content separately.

let cstr_argv: Vec<_> = argv.iter()
        .map(|arg| CString::new(arg.as_str()).unwrap())
        .collect();

let mut p_argv: Vec<_> = cstr_argv.iter() // do NOT into_iter()
        .map(|arg| arg.as_ptr())
        .collect();

p_argv.push(std::ptr::null());

let p: *const *const c_char = p_argv.as_ptr();

Playground.

See also: CString::new().unwrap().as_ptr() gives empty *const c_char

Mutate answered 10/3, 2017 at 11:57 Comment(1)
Thanks! I guess I should've kept the CStrings in a vector.V2
T
6

I suggest you read the documentation for CString::as_ptr again:

WARNING

It is your responsibility to make sure that the underlying memory is not freed too early. For example, the following code will cause undefined behavior when ptr is used inside the unsafe block:

# #![allow(unused_must_use)]
use std::ffi::{CString};

let ptr = CString::new("Hello").expect("CString::new failed").as_ptr();
unsafe {
    // `ptr` is dangling
    *ptr;
}

This happens because the pointer returned by as_ptr does not carry any lifetime information and the CString is deallocated immediately after the CString::new("Hello").expect("CString::new failed").as_ptr() expression is evaluated. To fix the problem, bind the CString to a local variable:

# #![allow(unused_must_use)]
use std::ffi::{CString};

let hello = CString::new("Hello").expect("CString::new failed");
let ptr = hello.as_ptr();
unsafe {
    // `ptr` is valid because `hello` is in scope
    *ptr;
}

This way, the lifetime of the CString in hello encompasses the lifetime of ptr and the unsafe block.

You are doing exactly what the documentation says not to do.

Tarmac answered 10/3, 2017 at 11:53 Comment(1)
Thanks! I read that part but didn't realize I made the mistake it warned not to.V2

© 2022 - 2024 — McMap. All rights reserved.