< 返回版块

duan-zhou 发表于 2024-11-04 00:25

use std::{marker::PhantomPinned, pin::Pin, ptr::NonNull};

pub struct User {
    pub name: String,
}

pub struct Token<'a> {
    pub data: &'a str,
}

impl<'a> Token<'a> {
    pub fn data(&self) -> String {
        self.data.to_string()
    }
}

pub fn user_token<'a>(user: &'a User) -> Token<'a> {
    Token { data: &user.name }
}

pub struct ResData<'a> {
    pub user: User,
    pub token: Token<'a>,
    _pin: PhantomPinned,
}

// 假设 User, Token, user_token 定义来自第三方库, 需要返回token信息
pub fn build<'a>() -> Pin<Box<ResData<'a>>> {
    let user = User {
        name: "hello".to_string(),
    };
    let ptr = NonNull::from(&user);
    let token = user_token(unsafe { ptr.as_ref() });
    let data = ResData {
        user,
        token,
        _pin: PhantomPinned,
    };
    Box::pin(data)
}

#[test]
fn test() {
    let data = build();
    assert_eq!(data.token.data(), "hello")
}

评论区

写评论
SleepyBoy 2024-11-04 17:42

data是分配到栈上的,在一般的情况下,这时候的“自引用”已经被赋值给栈上的引用,然后data才被移动pin到堆上,会出现指针指向错误。所以一般需要先pin再赋值。

在本例不出问题的原因是,String本身字符串是放在堆上的,&str切片直接切的是堆上的字符串,并没有引用到ResData结构体自身,如果改成&String引用,那么也会出现一般情况下的指针指向错误。修正就是先pin再取指针赋值

--
👇
zylthinking: 请教一下:

    let data = ResData {
        user,
        token,
        _pin: PhantomPinned,
    };
    Box::pin(data)

这里的 data 不能保证 data, data.user.name 分配在什么地方吧, 虽然大概率是在 heap 上; Box::pin(data) 语义其实应该将 data 放在在 heap 上, 但因为 data 本身不保证放在什么地方, 因此, user 也就不能保证在什么地方, 虽然 data.user.name.as_str() 作为一个潜在可被改写, 扩充的缓冲区一般在 heap 上, 但这也是凑巧...

因此, 将自引用的 ResData 通过 Box::pin 返回去, 会不会在一般情况下(非本例)导致内存非法引用呢

SleepyBoy 2024-11-04 17:32

字符串切片类型是一个比较特殊的类型,所以说你写的严格来说某种程度上不是自引用。因为String的实际字符串是存在堆上的,String创建出来的&str引用切片,都会是直接指向堆上切片的胖指针,而不是直接指向String结构体的,所以你不需要pin也是没问题的。 如果是其他的情况,一般是先把非自引用的部分,先pin到堆上,确保其不再移动后,再赋值自引用的部分。 按照你原始代码举例,如果token的内部类型改为&String,那么就会出现指针指向错误,因为你先把栈上的&String赋值了,然后又把String移到了堆上(pin) 第一位老哥给了一个教程网站,里面讲的就挺好的,可以看看

zylthinking 2024-11-04 10:37
pub fn build<'a>() -> Pin<Box<ResData<'a>>> {
    let user = User {
        name: "hello".to_string(),
    };
    let ptr = NonNull::from(&user);
    let token = user_token(unsafe { ptr.as_ref() });
    let data = ResData {
        user,
        token,
        _pin: PhantomPinned,
    };
    
    println!("{:p} {:p} {:p}", &data.user, &data.user.name, data.user.name.as_str());
    Box::pin(data)
}


fn main() {
    let data = build();
    println!("{:p} {:p} {:p}", &data.user,  &data.user.name, data.user.name.as_str());
    assert_eq!(data.token.data(), "hello")
}

输出: 0x7ffed25d39a8 0x7ffed25d39a8 0x6322c61a9b80 0x6322c61a9ae0 0x6322c61a9ae0 0x6322c61a9b80

只有 data.user.name.as_str() 相同, 显示出确实是利用了 String 内部持有的是一个 pointer 这个事实才没有导致未定义行为, 否则, 自引用必然被破坏

zylthinking 2024-11-04 10:20

请教一下:

    let data = ResData {
        user,
        token,
        _pin: PhantomPinned,
    };
    Box::pin(data)

这里的 data 不能保证 data, data.user.name 分配在什么地方吧, 虽然大概率是在 heap 上; Box::pin(data) 语义其实应该将 data 放在在 heap 上, 但因为 data 本身不保证放在什么地方, 因此, user 也就不能保证在什么地方, 虽然 data.user.name.as_str() 作为一个潜在可被改写, 扩充的缓冲区一般在 heap 上, 但这也是凑巧...

因此, 将自引用的 ResData 通过 Box::pin 返回去, 会不会在一般情况下(非本例)导致内存非法引用呢

坚果修补匠 2024-11-04 09:19

你是不是在找: https://course.rs/advance/circle-self-ref/self-referential.html

1 共 5 条评论, 1 页