Rookie going from std::process to libc exec

One of the first things I thought I'd try with rust is an executable that runs a separate executable and pass through all command line arguments. Seemed like the perfect time to try my first Rust program, to biuld a static executable with no runtime dependencies.

I started out with libc, but could not fit all the pieces together. Then I noticed std::process and got things running quickly. Given this short program, how would I convert this to using straight libc exec? I could not figure out the right way to get all the &osstr variables in a state the compiler would accept.

use std::env;
use std::process;

fn main() {
    let mut exec = env::current_exe().unwrap();
    exec.set_extension("sh");
    let args: Vec<String> = env::args().collect();

    let mut cmd = process::Command::new(exec);
    for arg in &args[1..] {
        cmd.arg(arg);
    }
    let status = cmd.status().unwrap();
    assert!(status.success());
}

Quick clarification.

I think std::process is great. I'm just curious what the libc version looks like, since I couldn't do it myself.

The signature of execv (the most convenient function in the exec family in this case):

pub unsafe extern fn execv(prog: *const c_char,
                           argv: *const *const c_char)
                           -> c_int

It takes a C string and an array of C strings; the latter doesn't have its size passed explicitly but is terminated with a null entry.

Rust has a wrapper around C strings, i.e. nul-terminated strings, called CString (owned) or CStr (borrowed). On Unix, to go from an OsStr to CString, we can call as_bytes on the OsStr to get a &[u8] and pass to CString::new, which copies the bytes into a new Vec<u8> and adds a nul terminator. (We can't go directly to CStr without copying because the existing buffer in an OsStr might not be nul-terminated. The original argv from libc might be, but not the copy made by args_os.)

    use std::os::unix::ffi::OsStrExt;
    let args: Vec<CString> = env::args_os().map(|arg| CString::new(arg.as_bytes()).unwrap()).collect();

But that's a vector of CString wrapper objects; we need a vector of raw *const c_chars. The original Vec is still needed to keep the allocations alive, though.

    let mut args_raw: Vec<*const c_char> = args.iter().map(|arg| arg.as_ptr()).collect();

Add the null value:

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

And we can get the raw argv to pass to C:

    let argv: *const *const c_char = args_raw.as_ptr();

A similar process to get prog (using the as_os_str method on Path):

    let exec_cstr: CString = CString::new(exec.as_os_str().as_bytes()).unwrap();
    let prog: *const c_char = exec_cstr.as_ptr();

And do the exec:

    unsafe { libc::execv(prog, argv); }

If execution continues, there was an error. C code would check errno, but errno is typically defined as a macro which expands to something like (*__errno_location()), depending on the OS. The libc crate directly exposes the underlying function rather than abstracting, so it's best not to use it directly. But such an abstraction can be found in std:

    use std::io::Error;
    let errno: i32 = Error::last_os_error().raw_os_error().unwrap();
    println!("errno = {}", errno);

(On Windows, things are a bit different: OsStr logically represents a series of UTF-16 u16 values, which on Windows is equivalent to wchar_t. So you'd probably want to use the wide-char based function _wexecv and encode_wide in std::os::windows::ffi::OsStrExt.)

3 Likes

Ideally you would never ever want to use libc functions such as _wexecv on Windows. They're just wrappers that impose limitations and add overhead to whatever you're doing. If you want to create a process without going through std::process on Windows you'd want to use CreateProcessW.