深入RUST标准库内核(二 内存)—mem模块/MaybeUninit<T>

本书github链接:inside-rust-std-library

前面章节参见:
深入RUST标准库内核(序言) - 简书 (jianshu.com)
深入RUST标准库内核(一 概述) - 简书 (jianshu.com)
深入RUST标准库内核(二 内存)—Layout/原生指针 - 简书 (jianshu.com)
深入RUST标准库内核(二 内存)—NonNull<T>/申请及释放 - 简书 (jianshu.com)

mem模块结构及函数

MaybeUninit<T>

MaybeUninit<T>结构定义

源代码如下:

    #[repr(transparent)] 
    pub union MaybeUninit<T> {
        uninit: (),
        value: ManuallyDrop<T>,
    }

MaybeUninit的内存布局就是ManuallyDrop<T>的内存布局,从后文可以看到,ManuallyDrop<T>实际就是T的内存布局。所以MaybeUninit在内存中实质也就是T类型。
RUST的引用使用的内存块必须保证是内存对齐及赋以初始值,未初始化的内存块和清零的内存块都不能满足引用的条件。但堆内存申请后都是未初始化的,且在程序中某些情况下也需要先将内存设置为未初始化,尤其在处理泛型时。因此,RUST提供了MaybeUninit<T>容器来实现对未初始化变量的封装,从而可以不引发编译错误完成某些对T类型未初始化变量的操作.
MaybeUninit<T>利用ManuallyDrop<T>的方式对T的未初始化进行了一个标识。这对T也有一个保护,使得未初始化的变量免于被RUST自动调用drop所释放掉.

ManuallyDrop<T> 结构及行为

源代码如下:

#[repr(transparent)]
pub struct ManuallyDrop<T: ?Sized> {
    value: T,
}

一个变量被ManuallyDrop获取所有权后,RUST编译器将不再对其自动调用drop操作。需要代码显式的调用drop来释放置入ManuallyDrop的T类型变量。
ManuallyDrop主要使用场景:

  1. 作为MaybeUninit的内部结构,对未初始化的内存做一个保护和标识。
  2. 希望由代码显式释放变量时。

重点关注的一些行为:
ManuallyDrop<T>::new(val:T) -> ManuallyDrop<T>, 此函数返回ManuallyDrop变量拥有传入的T类型变量所有权,并将此块内存直接用ManuallyDrop封装, 对于val,编译器不再主动做drop操作。这里,T实际上应该是实现了Sized的类型
ManuallyDrop<T>::into_inner(slot: ManuallyDrop<T>)->T, 将封装的T类型变量所有权转移出来,编译器会重新将返回的变量纳入drop管理体系。
ManuallyDrop<T>::take(slot: &mut ManuallyDrop<T>)->T,实质是复制一个变量。 不影响原有的变量。
ManuallyDrop<T>::drop(slot: &mut ManuallyDrop<T>),手动drop掉内部变量。
ManuallyDrop<T>::deref(&self)-> & T, 返回内部包装的变量的引用,返回的引用可正常使用
ManuallyDrop<T>::deref_mut(&mut self)-> & mut T返回内部包装的变量的可变引用,返回的引用可正常使用

ManuallyDrop代码举例:

    use std::mem::ManuallyDrop;
    let mut x = ManuallyDrop::new(String::from("Hello World!"));
    x.truncate(5); // 可以对x进行操作
    assert_eq!(*x, "Hello");
    // 但对x的drop不会再发生
MaybeUninit<T> 行为

MaybeUninit<T>提供了在GlobalAlloc Trait之外的一种获取内存的方法, 实际上可类比为泛型 new()的一种实现方式,不过返回的不是指针,而是变量。MaybeUninit<T>获取的内存位于栈空间。
MaybeUninit<T>::uninit()->MaybeUninit<T>, 是MaybeUninit<T>栈上申请内存的行为,申请的内存大小是T类型的内存大小,该内存没有初始化。利用泛型和Union内存布局,RUST巧妙的实现了在栈上申请一块未初始化内存。此函数非常非常非常值得关注,是非常多场景下的代码解决方案。
MaybeUninit<T>::new(val:T)->MaybeUninit<T>, 内部用ManuallyDrop封装了val, 然后用MaybeUninit封装ManuallyDrop。因为如果T没有初始化过,调用这个函数会编译失败,所以此时内存实际上已经初始化过了。
MaybeUninit<T>::zeroed()->MaybeUninit<T>, 申请了T类型内存并清零。

    pub fn zeroed() -> MaybeUninit<T> {
        let mut u = MaybeUninit::<T>::uninit();
        // SAFETY: `u.as_mut_ptr()` points to allocated memory.
        unsafe {
            //必须使用write_bytes,否则无法给内存清0
            u.as_mut_ptr().write_bytes(0u8, 1);
        }
        u
    }

MaybeUninit<T>::assume_init()->T,代码如下:

    pub const unsafe fn assume_init(self) -> T {
        // SAFETY: the caller must guarantee that `self` is initialized.
        // This also means that `self` must be a `value` variant.
        unsafe {
            intrinsics::assert_inhabited::<T>();
            //把T的所有权返回,编译器会主动对T调用drop
            ManuallyDrop::into_inner(self.value)
        }
    }

MaybeUninit<T>::assume_init_read()->T 此函数最后会调用ptr::read()函数。代码如下:

    pub const unsafe fn assume_init_read(&self) -> T {
        
        unsafe {
            intrinsics::assert_inhabited::<T>();
            //会调用ptr::read
            self.as_ptr().read()
        }
    }

    //此函数会复制一个变量,如果类型T含有引用或智能指针,仅仅调用这个函数会可能导致内存重复释放问题,
    //但在assume_init_read()中使用此函数不会导致问题,因为src被ManuallyDrop封装,不会被释放。
    pub const unsafe fn read<T>(src: *const T) -> T {` 
        //利用MaybeUninit::uninit申请未初始化的T类型内存
        let mut tmp = MaybeUninit::<T>::uninit();
        //SAFETY: the caller must guarantee that `src` is valid for reads.
        // `src` cannot overlap `tmp` because `tmp` was just allocated on
        // the stack as a separate allocated object.
        //
        // Also, since we just wrote a valid value into `tmp`, it is guaranteed
        // to be properly initialized.
        unsafe {
            //完成内存拷贝
            copy_nonoverlapping(src, tmp.as_mut_ptr(), 1);
            //初始化后的内存移出ManuallyDrop 并返回
            tmp.assume_init()
        }
    }

可见,assume_init_read 行为实际上是从一个已有类型生成并复制一个新的变量。此时原有的MaybeUninit变量可保持原状。
MaybeUninit<T>::assume_init_drop() 对内部变量进行drop操作
MaybeUninit<T>::assume_init_ref()->&T 返回内部T类型变量的借用,调用者应保证内部T类型变量已经初始化,&T此时是完全正常的
MaybeUninit<T>::assume_init_mut()->&mut T返回内部T类型变量的可变借用,调用者应保证内部T类型变量已经初始化,&mut T此时是完全正常的
MaybeUninit<T>::write(val)->&mut T, 代码如下:

    pub const fn write(&mut self, val: T) -> &mut T {
        //通常情况下,如果*self是初始化过得,那调用下面的等式时,会立刻调用*self拥有所有权变量的drop。但因为MaybeUninit<T>封装的变量不会被drop。所以下面这个等式实际上隐含了 *self必须是未初始化的,否则的话,这里会丢失掉已初始化的变量所有权信息,可能造成内存泄漏。
        *self = MaybeUninit::new(val);
        // SAFETY: We just initialized this value.
        unsafe { self.assume_init_mut() }
    }

MaybeUninit<T>::uninit_array<const LEN:usize>()->[Self; LEN] 此处对LEN的使用方式需要注意,这是不常见的一个泛型写法,这个函数同样的申请了一块内存。代码:

    pub const fn uninit_array<const LEN: usize>() -> [Self; LEN] {
        // SAFETY: An uninitialized `[MaybeUninit<_>; LEN]` is valid.
        unsafe { MaybeUninit::<[MaybeUninit<T>; LEN]>::uninit().assume_init() }
    }

这里要注意区别数组类型和数组元素的初始化。对于数组[MaybeUninit<T>;LEN]这一类型本身来说,初始化就是确定整体的内存大小,所以数组类型在声明后就已经完成了。所以此时assume_init()是正确的。这是一个理解上的盲点。

MaybeUninit<T>::array_assume_init<const N:usize>(array: [Self; N]) -> [T; N] 这个函数没有把所有权转移出来,代码分析如下:

    pub unsafe fn array_assume_init<const N: usize>(array: [Self; N]) -> [T; N] {
        // SAFETY:
        // * The caller guarantees that all elements of the array are initialized
        // * `MaybeUninit<T>` and T are guaranteed to have the same layout
        // * `MaybeUninit` does not drop, so there are no double-frees
        // And thus the conversion is safe
        unsafe {
            //最后是调用是*const T::read(),此处 as *const _的写法可以简化代码,这里没有把T类型变量所有权转移到返回值
            //返回后,此MaybeUninit变量应该被丢弃
            (&array as *const _ as *const [T; N]).read()
        }
    }

MaybeUninit<T>一些典型使用代码例子:
Box的内存申请例子:

    pub fn try_new_uninit_in(alloc: A) -> Result<Box<mem::MaybeUninit<T>, A>, AllocError> {
        let layout = Layout::new::<mem::MaybeUninit<T>>();
        //allocate(layout)?返回NonNull<[u8]>, NonNull<[u8]>::<MaybeUninit<T>>::cast()返回NonNull<MaybeUninit<T>>
        let ptr = alloc.allocate(layout)?.cast();
        //以下代码在Box结构时分析
        unsafe { Ok(Box::from_raw_in(ptr.as_ptr(), alloc)) }
    }

以上代码可以看到,NonNull<[u8]>可以直接通过cast 转换为NonNull<MaybeUninit<T>>, 这是另一种MaybeUninit<T>的生成方法,直接通过指针类型转换将未初始化的内存转换为MaybeUninit<T>。

    use std::mem::MaybeUninit;

    // Create an explicitly uninitialized reference. The compiler knows that data inside
    // a `MaybeUninit<T>` may be invalid, and hence this is not UB:
    // 获得一个未初始化的i32引用类型内存
    let mut x = MaybeUninit::<&i32>::uninit();
    // Set it to a valid value.
    // 将&0写入变量,完成初始化
    x.write(&0);
    // Extract the initialized data -- this is only allowed *after* properly
    // initializing `x`!
    // 将初始化后的变量解封装供后继的代码使用。
    let x = unsafe { x.assume_init() };

以上代码,编译器不会对x.write进行报警,这是MaybeUninit<T>的最重要的应用,这个例子展示了RUST如何给未初始化内存赋值的处理方式。调用assume_init前,必须保证变量已经被正确初始化。

更复杂的例子:

    use std::mem::{self, MaybeUninit};
    
    let data = {
    // Create an uninitialized array of `MaybeUninit`. The `assume_init` is
    // safe because the type we are claiming to have initialized here is a
    // bunch of `MaybeUninit`s, which do not require initialization.
    // data在声明后实际上就已经初始化完毕。
    let mut data: [MaybeUninit<Vec<u32>>; 1000] = unsafe {
        //这里注意实际调用是MaybeUninit::<[MaybeUninit<Vec<u32>>;1000]>::uninit(), RUST的类型推断机制完成了泛型实例化
        MaybeUninit::uninit().assume_init()
    };
    
    // Dropping a `MaybeUninit` does nothing. Thus using raw pointer
    // assignment instead of `ptr::write` does not cause the old
    // uninitialized value to be dropped. Also if there is a panic during
    // this loop, we have a memory leak, but there is no memory safety
    // issue.
    for elem in &mut data[..] {
    elem.write(vec![42]);
    }
    
    // Everything is initialized. Transmute the array to the
    // initialized type.
    // 直接用transmute完成整个数组类型的转换
    unsafe { mem::transmute::<_, [Vec<u32>; 1000]>(data) }
    };
    
    assert_eq!(&data[0], &[42]);

下面例子说明一块内存被 MaybeUnint<T>封装后,编译器将不再对其做释放,必须在代码中显式释放:

    use std::mem::MaybeUninit;
    use std::ptr;
   
    // Create an uninitialized array of `MaybeUninit`. The `assume_init` is
    // safe because the type we are claiming to have initialized here is a
    // bunch of `MaybeUninit`s, which do not require initialization.
    let mut data: [MaybeUninit<String>; 1000] = unsafe { MaybeUninit::uninit().assume_init() };
    // 初始化了500个String变量
    let mut data_len: usize = 0;
    for elem in &mut data[0..500] {
        //write没有将所有权转移出ManuallyDrop
        elem.write(String::from("hello"));
        data_len += 1;
    }
    // For each item in the array, drop if we allocated it.
    //rust不能自动去释放已经申请的String, 必须手工调用drop_in_place释放
    for elem in &mut data[0..data_len] {
        unsafe { ptr::drop_in_place(elem.as_mut_ptr()); }
    }

上例中,在没有assume_init()调用的情况下,必须手工调用drop_in_place释放内存。
MaybeUninit<T>是一个非常重要的类型结构,未初始化内存是编程中不可避免要遇到的情况,MaybeUninit<T>也就是RUST编程中必须熟练使用的一个类型。

mem模块函数库

mem::zeroed<T>() -> T 此函数用MaybeUninit::zeroed获取全零内存后,调用assume_init(), 返回一个清零的T变量,要确认全零是一种T类型合理的初始化才可用
mem::uninitialized<T>() -> T 用MaybeUnint::uninit获取一块未初始化内存,然后调用assume_init(), 此时内存彻底未初始化。
mem::take<T: Default>(dest: &mut T) -> T 将dest设置为默认内容(不改变所有权),用一个新变量返回dest的内容。
mem::replace<T>(dest: &mut T, src: T) -> T 用src的内容赋值dest(不改变所有权),用一个新变量返回dest的内容。
mem::transmute<T,U>(src: T) -> U 直接将T类型内存转化为U类型内存。 类似C语言的&(U *(&src))操作。
mem::transmute_copy<T, U>(src: &T) -> U 新建类型U的变量,并把src的内容拷贝到U。
mem::forget<T>(t:T) 通知RUST不做变量的drop操作,代码用ManuallyDrop完成
mem::forget_unsized<T:Sized?> 代码用intrinsics::forget完成
mem::size_of<T>()->usize/mem::min_align_of<T>()->usize/mem::size_of_val<T>(val:& T)->usize/mem::min_align_of_val<T>(val: &T)->usize/mem::needs_drop<T>()->bool 基本就是直接调用intrinsic模块的同名函数
mem::drop<T>(_x:T) 释放内存

ptr模块再探

ptr::read<T>(src: *const T) -> T 此函数用已有的类型复制出一个新的类型实体,对于不支持Copy Trait的类型,read函数是RUST实现未知类型变量的复制的一种方法,此函数作为内存函数take(), replace(), transmute_copy()的基础,底层使用intrisic::copy_no_overlapping支持,代码分析已经在前面章节完成
ptr::read_unaligned<T>(src: *const T) -> T当数据结构中有未内存对齐的成员变量时,需要用此函数读取内容并转化为内存对齐的变量。否则会引发UB(undefined behaiver) 如下例:

/// Read a usize value from a byte buffer:

   use std::mem;
  
   fn read_usize(x: &[u8]) -> usize {
       assert!(x.len() >= mem::size_of::<usize>());

       let ptr = x.as_ptr() as *const usize;
       unsafe { ptr.read_unaligned() }
   }

例子中,为了从byte串中读取一个usize,需要用read_unaligned来获取值,不能象C语言那样通过指针类型转换直接获取值。

ptr::write<T>(dst: *mut T, src: T) 代码如下:

pub const unsafe fn write<T>(dst: *mut T, src: T) {
    unsafe {
        //浅拷贝
        copy_nonoverlapping(&src as *const T, dst, 1);
        //必须调用forget,这里所有权已经转移。不允许再对src做drop操作
        intrinsics::forget(src);
    }
}

write函数本质上就是一个所有权转移的操作。完成src到dst的浅拷贝,然后调用了forget(src), 这使得src的Drop不再被调用(也规避src类型如果有引用导致的重复释放问题)。从而将所有权转移到dst。此函数是mem::replace, mem::transmute_copy的基础。底层由intrisic:: copy_no_overlapping支持。
这个函数中,如果dst已经初始化过,那原dst变量的所有权将被丢失掉,有可能引发内存泄漏。

pub const fn 
replace<T>(dest: &mut T, src: T) -> T {
    unsafe {
        let result = ptr::read(dest);
        ptr::write(dest, src);
        result
    }
}

上面的函数不会导致内存泄漏问题。因为read本身做了一个已有变量的复制。

ptr::write_unaligned<T>(dst: *mut T, src: T) 与read_unaligned相对应。举例如下:

    #[repr(packed, C)]
    struct Packed {
        _padding: u8,
        unaligned: u32,
    }
    
    let mut packed: Packed = unsafe { std::mem::zeroed() };
    
    // Take the address of a 32-bit integer which is not aligned.
    // In contrast to `&packed.unaligned as *mut _`, this has no undefined behavior.
    // 对于结构中字节没有按照2幂次对齐的成员,要用addr_of_mut!宏来获得地址,无法用取引用的方式。
    let unaligned = std::ptr::addr_of_mut!(packed.unaligned);
    
    unsafe { std::ptr::write_unaligned(unaligned, 42) };
    
     assert_eq!({packed.unaligned}, 42); // `{...}` forces copying the field instead of creating a reference.

ptr::read_volatile<T>(src: *const T) -> T 是intrinsics::volatile_load的封装
ptr::write_volatile<T>(dst: *mut T, src:T) 是intrinsics::volatiel_store的封装

ptr::macro addr_of($place:expr) 因为用&获得引用必须是字节按照2的幂次对齐的地址,所以用这个宏获取非地址对齐的变量地址
ptr::macro addr_of_mut($place:expr) 作用同上。
指针的通用函数请参考Rust库函数参考

NonNull 与MaybeUninit<T>相关函数

NonNull<T>::as_uninit_ref<`a>(&self) -> &`a MaybeUninit<T> NonNull与MaybeUninit的引用基本就是直接转换的关系,一体双面
NonNull<T>::as_uninit_mut<`a>(&self) -> &`a mut MaybeUninit<T>
NonNull<[T]>::as_uninit_slice<'a>(&self) -> &'a [MaybeUninit<T>]
NonNull<[T]>::as_uninit_slice_mut<'a>(&self) -> &'a mut [MaybeUninit<T>]

Unique

Unique类型结构定义如下

    #[repr(transparent)]
    pub struct Unique<T: ?Sized> {
        pointer: *const T,
        // NOTE: this marker has no consequences for variance, but is necessary
        // for dropck to understand that we logically own a `T`.
        //
        // For details, see:
        // https://github.com/rust-lang/rfcs/blob/master/text/0769-sound-generic-drop.md#phantom-data
        _marker: PhantomData<T>,
    }

和NonNull对比,Unique多了PhantomData<T>类型变量。这个定义使得编译器知晓,Unique<T>拥有了pointer指向的内存的所有权,NonNull<T>没有这个特性。具备所有权后,Unique<T>可以实现Send, Sync等Trait。
指针在被Unique封装前,必须保证是NonNull的
RUST用Allocator申请出来的内存的所有权用Unique<T>做了绑定,使得内存进入了RUST的所有权和借用系统。
Unique<T>创建举例:

    //下面的代码前文已经解释过,请参考
    pub fn try_new_uninit_in(alloc: A) -> Result<Box<mem::MaybeUninit<T>, A>, AllocError> {
        let layout = Layout::new::<mem::MaybeUninit<T>>();
        let ptr = alloc.allocate(layout)?.cast();
        unsafe { Ok(Box::from_raw_in(ptr.as_ptr(), alloc)) }
    }
    
    pub unsafe fn from_raw_in(raw: *mut T, alloc: A) -> Self {
        Box(unsafe { Unique::new_unchecked(raw) }, alloc)
    }

以上代码是Box<T>的创建关联函数,从代码可以分析比较完整的RUST的动态内存申请类型转换过程是,GlobalAlloc::alloc申请返回* mut u8指针,Allocator::allocate() 返回NonNull<[U8]>类型指针,由NonNull::cast()转化为NonNull<MaybeUninit<T>>类型,然后由NonNull<MaybeUninit<T>> 重新拆封装建立Unique<MaybeUninit<T>>类型。至此,申请的动态内存进入了RUST的所有权识别领域

Unique::cast<U>(self)->Unique<U> 类型转换,程序员应该保证T和U的内存布局相同
Unique::<T>::new(* mut T)->Option<Self> 此函数内部判断* mut T是否为0值
Unique::<T>::new_unchecked(* mut T)->Self 封装* mut T, 调用代码应该保证* mut T的安全性
Unique::as_ptr(self)->* mut T
Unique::as_ref(&self)->& T 因为Unique具备所有权,此处&T的生命周期与self相同,不必特别声明声明周期
Unique::as_mut(&mut self)->& mut T 同上

所有权转移的底层实现

所有权的转移实际上是两步:1.栈上内存的浅拷贝;2:原先的变量置标志表示所有权已转移。置标志的变量如果没有重新绑定其他变量,则在生命周期结束的时候被drop。 引用及指针自身也是一个isize的值变量,也有所有权,也具备生命周期。

变量调用drop的时机

如下例子:

struct TestPtr {a: i32, b:i32}
impl Drop for TestPtr {
    fn drop(&mut self) {
        println!("{} {}", self.a, self.b);
    }
}
fn main() {
   let test = Box::new(TestPtr{a:1,b:2});
   let test1 = *test;
   let mut test2 = TestPtr{a:2, b:3};
   //此行代码会导致先释放test2拥有所有权的变量,然后再给test2赋值。代码后的输出会给出证据
   //将test1的所有权转移给test2,无疑代表着test2现有的所有权会在后继无法访问,因此drop被立即调用。
   test2 = test1;
   println!("{:?}", test2);
}

输出:
2 3
TestPtr { a: 1, b: 2 }
1 2

小结

在RUST标准库的ptr, mem,alloc模块提供了RUST内存的底层操作。内存的底层操作是其他RUST库模块的基础设施。不能理解内存的底层操作,就无法驾驭RUST完成较复杂的任务。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容