Can stdout/stderr of a C/C++ library be caught by Rust?

Viewed 502

I am wrapping a C/C++ library in a Rust crate and calling into it using FFI (I am not using a subprocess).

This library logs to stdout/stderr (using, say, printf() or std::cout) but I would like to "catch" this output and use Rust's log crate to control the output.

Is it possible to redirect stdout/stderr of FFI calls to log?

1 Answers

Please find below an example illustrating the different steps to redirect/restore stderr (file descriptor 2).

The (C-like) style used here was intended in order to keep this example minimal ; of course, you could probably use the libc crate and encapsulate properly all of this in a struct.

Note that, in trivial cases, you may repeat the redirect/invoke/obtain/restore sequence as many times as you want, provided you keep pipe_fd, saved_fd and log_file open.

However, in non-trivial cases, some kind of complication is implied:

  • if the C code produces a quite long message, how can we detect that we have read it all?
    • we could inject an end-marker into STDERR_FILENO after the message is produced at the invoke step and then read log_file until this marker is detected in the obtain step. (this adds some kind of text processing)
    • we could recreate the pipe and log_file before each redirect step, close the PIPE_WRITE end before the invoke step, read log_file until EOF is reached and close it in the obtain step. (this adds the overhead of more system-calls)
  • if the C code produces a very long message, wouldn't it exceed the pipe's internal buffer capacity (and then block writing)?
    • we could execute the invoke step in a separate thread and join() it after the obtain step has completed (end-marker or EOF is reached), so that the invocation still looks serial from the application's point of view. (this adds the overhead of spawning/joining a thread)
    • an alternative is to put all the logging part of the application in a separate thread (spawned once for all) and keep all the invocation steps serial. (if the logging part of the application does not have to be perceived as serial this is OK, but else this just reports the same problem one thread further)
    • we could fork() to perform the redirect and invoke steps in a child process (if the application data does not have to be altered, just read), get rid of the restore step and wait() the process after the obtain step has completed (end-marker or EOF is reached), so that the invocation still looks serial from the application's point of view. (this adds the overhead of spawning/waiting a process, and precludes the ability to alter the application data from the invoked code)
// necessary for the redirection
extern "C" {
    fn pipe(fd: *mut i32) -> i32;
    fn close(fd: i32) -> i32;
    fn dup(fd: i32) -> i32;
    fn dup2(
        old_fd: i32,
        new_fd: i32,
    ) -> i32;
}
const PIPE_READ: usize = 0;
const PIPE_WRITE: usize = 1;
const STDERR_FILENO: i32 = 2;

fn main() {
    //
    // duplicate original stderr in order to restore it
    //
    let saved_stderr = unsafe { dup(STDERR_FILENO) };
    if saved_stderr == -1 {
        eprintln!("cannot duplicate stderr");
        return;
    }
    //
    // create resources (pipe + file reading from it)
    //
    let mut pipe_fd = [-1; 2];
    if unsafe { pipe(&mut pipe_fd[0]) } == -1 {
        eprintln!("cannot create pipe");
        return;
    }
    use std::os::unix::io::FromRawFd;
    let mut log_file =
        unsafe { std::fs::File::from_raw_fd(pipe_fd[PIPE_READ]) };
    //
    // redirect stderr to pipe/log_file
    //
    if unsafe { dup2(pipe_fd[PIPE_WRITE], STDERR_FILENO) } == -1 {
        eprintln!("cannot redirect stderr to pipe");
        return;
    }
    //
    // invoke some C code that should write to stderr
    //
    extern "C" {
        fn perror(txt: *const u8);
    }
    unsafe {
        dup(-1); // invalid syscall in order to set errno (used by perror)
        perror(&"something bad happened\0".as_bytes()[0]);
    };
    //
    // obtain the previous message
    //
    use std::io::Read;
    let mut buffer = [0_u8; 100];
    if let Ok(sz) = log_file.read(&mut buffer) {
        println!(
            "message ({} bytes): {:?}",
            sz,
            std::str::from_utf8(&buffer[0..sz]).unwrap(),
        );
    }
    //
    // restore initial stderr
    //
    unsafe { dup2(saved_stderr, STDERR_FILENO) };
    //
    // close resources
    //
    unsafe {
        close(saved_stderr);
        // pipe_fd[PIPE_READ] will be closed by log_file
        close(pipe_fd[PIPE_WRITE]);
    };
}
Related