C结构体到Rust的错误映射

3

出于教育目的,我尝试在Rust中访问FILE结构:

unsafe {
    let passwd = libc::fopen("/etc/passwd".to_ptr(), &('r' as libc::c_char));
    let fp = &mut *(passwd as *mut MY_FILE);
    println!("flags={}, file={}", fp._flags, fp._file);
}

我通过在OS X上运行bindgen对stdio.h进行处理,获得了MY_FILE结构体:

bindgen /usr/include/stdio.h

一些文件以写模式打开时,_flags 总是为 8(读模式下为 4),因此这个标志似乎有误(我使用 C 代码进行了测试,确实不是 4 或 8)。但是文件指针似乎是正确的。这可能是什么原因?我是否从错误的头文件中提取了绑定?是否需要在 #[repr(C,)] 属性中添加一些内容? 这里 是包含结构体的完整代码。
这是对 早期问题 的跟进提问。

你发布的代码无法编译(我认为是fp和passwd混淆了)。此外,你传递给C函数的字符串没有以NUL结尾,因此你的代码具有完全未定义的行为。 - Sebastian Redl
哦,是的,我忘记添加一个文件进行NUL终止了,我已经更新了上面的要点。 - hansaplast
不担心每次泄漏字符串吗?无论如何,我认为第二个参数也有同样的问题。 - Sebastian Redl
1个回答

3

首先,您的ToPtr实现可能会导致不安全的代码。如下所示:

// code in italics is wrong
impl ToPtr for str {
    fn to_ptr(&self) -> *const i8 {
        CString::new(self).unwrap().as_ptr()
    }
}

这将分配一个新的 CString 并返回其内容的指针,但是当 to_ptr 返回时,CString 将被删除,因此这是一个悬空指针。 任何对此指针的引用都是未定义的行为。 文档 对此有一个重要的警告,但这仍然是一个非常常见的错误。

从字符串字面量创建 *const c_char 的一种正确方法是 b"string here\0".as_ptr() as *const c_char。该字符串以 null 结尾,并且没有悬空指针,因为字符串字面量在整个程序中都存在。如果您要转换非常量字符串,则在使用它时必须使 CString 保持活动状态,如下所示:

let s = "foo";
let cs = CString::new(s).unwrap(); // don't call .as_ptr(), so the CString stays alive
unsafe { some_c_function(cs.as_ptr()); }
// CString is dropped here, after we're done with it

补充说明:一个编辑人员"建议"(我是 Stack Overflow 的新手,但似乎评论比重写我的答案更礼貌)上面的代码可以这样写:

let s = "foo";
unsafe {
    // due to temporary drop rules, the CString will be dropped at the end of the statement (the `;`)
    some_c_function(CString::new(s).unwrap().as_ptr());
}

尽管这是技术上正确的(最好的一种正确),但所涉及到的"临时降低规则"很微妙——这是因为as_ptr接受一个对CString(实际上是&CStr,因为编译器将方法链更改为CString::new(s).unwrap().deref().as_ptr())的引用而不是消耗它,并且因为我们只有一个要调用的C函数。在写不安全代码时,我不喜欢依赖于任何微妙或不明显的东西。
除此之外,我修复了你代码中的不安全性(你所有的调用都使用字符串字面量,所以我只是使用了我上面提到的第一种策略)。在OSX上,我得到了以下输出:
0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0
0, 0, 8, 1, 0, 0, 0, 0, 0, 0, 0
0, 0, 8, 2, 0, 0, 0, 0, 0, 0, 0
0, 0, 4, 3, 0, 0, 0, 0, 0, 0, 0

那么,这与您的结果相匹配,对吧?我还编写了以下C程序:

#include <stdio.h>
#include <unistd.h>

int main() {
    struct __sFILE *fp1 = fdopen(STDIN_FILENO, "r");
    struct __sFILE *fp2 = fdopen(STDOUT_FILENO, "w");
    struct __sFILE *fp3 = fdopen(STDERR_FILENO, "w");
    struct __sFILE *passwd = fopen("/etc/passwd", "r");

    printf("%i %i %i %i\n", fp1->_flags, fp2->_flags, fp3->_flags, passwd->_flags);
}

并得到以下输出:

4 8 8 4

这似乎证实了Rust的结果。在/usr/include/stdio.h的顶部有一条注释,它说:
/*
 * The following always hold:
 *
 *  if (_flags&(__SLBF|__SWR)) == (__SLBF|__SWR),
 *      _lbfsize is -_bf._size, else _lbfsize is 0
 *  if _flags&__SRD, _w is 0
 *  if _flags&__SWR, _r is 0
 */

并查找这些常量:

#define __SLBF  0x0001      /* line buffered */
#define __SRD   0x0004      /* OK to read */
#define __SWR   0x0008      /* OK to write */

这似乎与我们得到的输出相匹配:使用读模式打开文件为4,写则为8。那么问题出在哪里呢?

谢谢你对悬空指针的解释,这些东西非常重要。它在文档中,但我没有读过。然后:我所拥有的C代码来自《高级Unix编程》一书。在我发现大多数FILE结构变量只有在流读/写时才被填充之后,我在书中找到了这句话:“请注意,在打印其缓冲状态之前,我们对每个流执行I/O操作,因为第一个I/O操作通常会为流分配缓冲区”(再次提醒我阅读手册)。工作要点:https://gist.github.com/philippkeller/c683dda9d89ca0f8cb6830b6dc52910b - hansaplast

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接