创建一个指向结构体的*mut *mut。

7
我试图使用指向我的结构体的指针调用pthread_join函数,以便C线程可以将结构体填充到我指向的内存中。(是的,我知道这非常不安全...) pthread_join的函数签名如下:
pub unsafe extern fn pthread_join(native: pthread_t,
                                  value: *mut *mut c_void)
                                  -> c_int

"我正在做这个练习,将一本书中的C代码移植到Rust。C代码如下:"
pthread_t   tid1;
struct foo  *fp;
err = pthread_create(&tid1, NULL, thr_fn1, NULL);
err = pthread_join(tid1, (void *)&fp);

我想出了这段代码:
extern crate libc;
use libc::{pthread_t, pthread_join};

struct Foo {}

fn main() {
    let tid1:pthread_t = std::mem::uninitialized();
    let mut fp:Box<Foo> = std::mem::uninitialized();
    let value = &mut fp;
    pthread_join(tid1, &mut value);
}

但我看到的错误是:
error[E0308]: mismatched types
  --> src/bin/11-threads/f04-bogus-pthread-exit.rs:51:24
   |
51 |     pthread_join(tid1, &mut value);
   |                        ^^^^^^^^^^ expected *-ptr, found mutable reference
   |
   = note: expected type `*mut *mut libc::c_void`
              found type `&mut &mut std::boxed::Box<Foo>`

这是否只使用强制类型转换就能实现,还是需要使用 transmute 函数?

1
我不确定这个代码是否能正常工作,因为它调用了一个 C 库..所以我会将其留作注释。你可以尝试让编译器推断类型..但你需要将值强制转换为指针(而不是引用) - Simon Whitehead
@SimonWhitehead 这个方法有效!愿意把这写成一个答案吗?Matthieu 的回答可能是应该采取的方法(将所有权交给 C,然后再拿回来),但我认为你的回答是一个不错的替代方案。 - hansaplast
@SimonWhitehead,你的回答好像消失了 :( - xxks-kkk
@zack 最终我没有将它写成答案!这里的回答已经很合适了 :) - Simon Whitehead
2个回答

7
这里有几个问题:
  • Box是指向堆分配资源的指针,您可以使用Box::into_raw(some_box)提取指针本身,
  • 引用不会被自动转换为指针(即使它们具有相同的表示形式),您需要进行显式转换,
  • 您需要将具体类型强制转换为c_void,类型推断可能能够完成此操作
  • 您有一个指向指针的引用,您需要一个指向指针的指针;您有过多的间接级别。
让我们让它工作:
// pthread interface, reduced
struct Void;

fn sample(_: *mut *mut Void) {}

// actual code
struct Foo {}

fn main() {
    let mut p = Box::into_raw(Box::new(Foo{})) as *mut Void;
    sample(&mut p as *mut _);
}

请注意,这会泄漏内存(由于 into_raw 导致),通常应该使用 from_raw 将内存压回一个 Box 中,以便调用 Foo 的析构函数并释放内存。

“泄漏内存”是什么意思?而且:Box::new在完成into_raw部分后立即被删除,会不会导致悬空指针? - hansaplast
1
@hansaplast:如果是这样的话,into_raw有什么意义呢?你读过我提供的链接文档吗?它详细解释了函数的行为。 - Matthieu M.
我没有看到链接。非常感谢提供的文档,回答了我之前的问题。我会把它放回盒子里,但是如果我不这样做会发生什么?当主函数结束并且 p 被删除时,内存不是被释放了吗? - hansaplast
2
@hansaplast:p是一个原始指针,原始指针不表达所有权(或缺乏)。Rust不知道sample现在是否负责删除内存,因此它不会做任何事情。如果你不将它塞进一个Box中,它就会泄漏。请注意,泄漏是安全的,在小量上是无害的(但是在大量上,你会有问题,然而D或Clang编译器例如不释放它们的内存以使其更快)。 - Matthieu M.
应用于@hansaplast的问题,此解决方案会泄漏内存,因为into_raw返回的指针被pthread_join无条件地覆盖,所以原始的Box完全丢失。如果您将p的(新)值转换回(新的)Box,除非它最初来自Rust(这似乎不太可能,因为您正在调用C来获取它),否则在最终删除时可能会崩溃或破坏分配器。 - trent
@trentcl:确实,对于pthread_join而言,在传递指针之前不需要分配任何内容。 - Matthieu M.

5
代码无法正常运行,这是因为C线程在你指向的内存中并没有真正“填写结构体”。它负责分配自己的内存(或从另一个线程事先接收内存)并进行填充。C线程“返回”的唯一东西是一个地址,并且此地址由pthread_join捕获。
这就是为什么pthread_join接收void **(即指向void *的指针)。这种输出参数使pthread_join能够存储(返回)新完成线程提供的void *指针。线程可以通过将其传递给pthread_exit或从传递给pthread_create的start_routine中返回它来提供指针。在Rust中,原始指针可以使用以下代码接收:
let mut c_result: *mut libc::c_void = ptr::null_mut();
libc::pthread_join(tid1, &mut c_result as *mut _);
// C_RESULT now contains the raw pointer returned by the worker's
// start routine, or passed to pthread_exit()

返回指针所指向的内存内容和大小是等待连接线程和连接线程之间的约定。如果工作线程是用C实现并且设计为由其他C代码调用,则显而易见的选择是为其分配结果结构的内存空间,填充其内容,并提供已分配内存的指针。例如:

struct ThreadResult { ... };

...
ThreadResult *result = malloc(sizeof(struct ThreadResult));
result->field1 = value1;
...
pthread_exit(result);

在这种情况下,你的 Rust 代码可以通过复制 C 结构并获取其所有权来解释线程的结果:
// obtain a raw-pointer c_result through pthread_join as 
// shown above:
let mut c_result = ...;
libc::pthread_join(tid1, &mut c_result as *mut _);

#[repr(C)]
struct ThreadResult { ... } // fields copy-pasted from C

unsafe {
    // convert the raw pointer to a Rust reference, so that we may
    // inspect its contents
    let result = &mut *(c_result as *mut ThreadResult);

    // ... inspect result.field1, etc ...

    // free the memory allocated in the thread
    libc::free(c_result);
    // RESULT is no longer usable
}

1
在我的上面的例子中,我的理解是指针所指向的内存需要已经分配(我认为需要使用std::mem::zeroed())。 从pthread_join的手册中可以看到:“从成功的pthread_join()调用返回,并且value_ptr参数为非NULL时,终止线程由pthread_exit()传递的值将存储在value_ptr引用的位置中。” - hansaplast
1
@hansaplast 你只需要为指向指针的指针分配存储空间。以这种方式获得的指针将指向哪个内存是任何人的猜测。它甚至不需要是一个有效的地址 - 例如,如果线程被取消,则 result_ptr 将包含 libc::PTHREAD_CANCELED(在 Linux 上)。 - user4815162342
2
@hansaplast 确实,“值”被复制了 - 但是这个“值”只是指针本身的值,即地址。(更准确地说,它是一个指针大小的整数,可能包含一个实际可解引用的地址,也可能是NULL。)标准没有规定这个指针指向堆分配的数据还是静态分配的数据,或者它是否指向任何东西(如果它是NULL或任意整数转换为void *)。这完全是调用pthread_exit和调用pthread_join的代码之间的契约问题。 - user4815162342
我现在注意到,最初的答案中代码中result_ptr的类型不正确,应该是*mut c_void而不仅仅是c_void。我已经进行了更正(并测试编译),希望消除一些混淆。 - user4815162342
1
@hansaplast 一个堆栈分配的结构体无法工作,因为堆栈是每个线程独有的,所以退出线程会销毁其堆栈。只要你理解被复制的“值”仅仅是提供给pthread_exit的指针,那么手册就是正确的。如果考虑到C语言没有复制构造函数的概念,并且像pthread_exit这样的库函数不知道传递给它的结构体的大小,那么更复杂的数据复制显然是不可能的。 - user4815162342
显示剩余3条评论

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