【问题标题】:Can stdout/stderr of a C/C++ library be caught by Rust?Rust 可以捕获 C/C++ 库的 stdout/stderr 吗?
【发布时间】:2020-08-26 14:37:48
【问题描述】:

我将一个 C/C++ 库包装在一个 Rust 板条箱中并使用 FFI 调用它(我 使用子进程)。

这个库记录到 stdout/stderr(例如使用 printf()std::cout),但我想“捕获”这个输出并使用 Rust 的 log crate 来控制输出。

是否可以将 FFI 调用的 stdout/stderr 重定向到 log

【问题讨论】:

  • 您可以完全阻止 any 输出到 stdout / stderr 吗?
  • 是的,但您可能需要使用不安全且特定于操作系统的代码。例如,在类 Unix 的 OS-es 上,您可以使用 dup2() 将标准输出重定向到管道,从后台 Rust 线​​程读取该管道,并酌情调用 log
  • 我可以接受 all 输出被重定向;我希望生锈部分“管理”日志记录...重定向标准输出(使用dup2())是否也会更改生锈代码?我会说是的......
  • 是的,但是您可以在 FFI 代码完成后撤消该效果。 (撤消它相当便宜,归结为几个系统调用。)您还必须小心 Rust 日志记录延迟输出,直到 hack 被撤消,否则它可能会进入无限循环。

标签: rust


【解决方案1】:

请在下面找到说明不同的示例 重定向/恢复标准错误的步骤(文件描述符 2)。

这里使用的(C-like)风格是为了保持这个 示例最小;当然,您可以使用libcstruct 中正确封装和封装所有这些。

请注意,在琐碎的情况下,您可以重复 redirect/invoke/obtain/restore 序列任意多次, 前提是您保持pipe_fdsaved_fdlog_file 处于打开状态。

但是,在不平凡的情况下,隐含了某种复杂性:

  • 如果 C 代码产生 相当 长消息,我们如何检测 我们都读完了吗?
    • 我们可以在STDERR_FILENO之后注入一个结束标记 消息在 invoke 步骤生成,然后读取 log_file 直到在获取步骤中检测到这个标记。 (这增加了 某种文本处理)
    • 我们可以在每次重定向之前重新创建管道和log_file 步骤,在 invoke 步骤之前关闭 PIPE_WRITE 结束,阅读 log_file 直到到达 EOF 并在 获取 步骤中将其关闭。 (这会增加更多系统调用的开销)
  • 如果 C 代码产生 非常 长消息,它不会超过 管道的内部缓冲区容量(然后阻止写入)?
    • 我们可以在单独的线程中执行 invoke 步骤,然后 join() 获取步骤完成后@它(结束标记或 已达到 EOF),因此调用仍然看起来是串行的 从应用程序的角度来看。 (这会增加生成/加入线程的开销)
    • 另一种方法是将应用程序的所有日志记录部分 在一个单独的线程中(全部生成一次)并保留所有 调用 步骤连续。 (如果应用程序的日志记录部分不必 被认为是连续的,这没关系,但否则这只是报告 同样的问题还有一个线程)
    • 我们可以fork() 执行重定向调用 子进程中的步骤(如果应用程序数据没有 要更改,只需阅读),摆脱 restore 步骤和 wait() 获取步骤完成后的过程 (到达结束标记或 EOF),因此调用仍然 看起来 从应用程序的角度来看是连续的。 (这增加了产生/等待进程的开销,并且 排除了更改应用程序数据的能力 调用代码)
// 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]);
    };
}

【讨论】:

  • 如果有很多输出,当管道缓冲区填满时,这种方法将挂起。为避免这种情况,您需要在写入的同时在单独的线程中进行读取。
  • @MichaelAnderson 我同意。我的目的是使示例保持简单,以便专注于重要步骤。但是您的评论指出需要对一些不那么琐碎的情况进行一些解释。
猜你喜欢
  • 1970-01-01
  • 2010-12-05
  • 2012-02-29
  • 2013-01-12
  • 1970-01-01
  • 2023-03-07
  • 1970-01-01
  • 2014-08-05
  • 2013-09-02
相关资源
最近更新 更多