深入理解CMake(2):初步解读Caffe的CMake脚本

预备说明

分析的是官方Caffe(https://github.com/BVLC/caffe)的CMake脚本,主要分析了根目录的CMakeLists.txt
Caffe代码的commit id为99bd99795dcdf0b1d3086a8d67ab1782a8a08383

所谓CMake脚本这里指的是CMakeLists.txt和xxx.cmake的统称。

$CAFFE_ROOT/CMakeLists.txt解读

cmake_minimum_required(VERSION 2.8.7)

设定cmake最低版本。高版本cmake提供更多的功能(例如cmake3.13开始提供target_link_directories())或解决bug(例如OpenMP的设定问题),低版本有更好的兼容性。VERSION必须大写,否则不识别而报错。非必须但常规都写。放在最开始一行。


if(POLICY CMP0046)
  cmake_policy(SET CMP0046 NEW)
endif()

cmake中也有if判断语句,需要配对的endif()。
POLICY是策略的意思,cmake中的poilcy用来在新版本的cmake中开启、关闭老版本中逐渐被放弃的功能特性:

Policies in CMake are used to preserve backward compatible behavior across multiple releases


project(Caffe C CXX)

project()指令,给工程起名字,很正常不过了。这列还写明了是C/C++工程,其实没必要写出来,因为CMake默认是开启了这两个的。
这句命令执行后,自动产生了5个变量:

  • PROJECT_NAME,值等于Caffe
  • PROJECT_SOURCE_DIR,是CMakeLists.txt所在目录,通常是项目根目录(奇葩的项目比如protobuf,把CMakeLists.txt放在cmake子目录的也有)
  • PROJECT_BINARY_DIR,是执行cmake命令时所在的目录,通常是build一类的用户自行创建的目录。
  • Caffe_SOURCE_DIR,此时同PROJECT_SOURCE_DIR
  • Caffe_BINARY_DIR,此时同PROJECT_BINARY_DIR
    官方cmake文档对PROJECT_SOURCE_DIRPROJECT_BINARY_DIR的解释很晦涩:
    image.png
image.png

自行实践验证下:


image.png

image.png


set(CAFFE_TARGET_VERSION "1.0.0" CACHE STRING "Caffe logical version")
set(CAFFE_TARGET_SOVERSION "1.0.0" CACHE STRING "Caffe soname version")

set()指令是设定变量的名字和取值,CACHE意思是缓存类型,是说在外部执行CMake时可以临时指定这一变量的新取值来覆盖cmake脚本中它的取值:CMAKE -Dvar_name=var_value

而最后面的双引号包起来的取值可以认为是”注释“。STRING是类型,不过据我目前看到和了解到的,CMake的变量99.9%是字符串类型,而且这个字符串类型变量和字符串数组类型毫无区分。

变量在定义的时候直接写名字,使用它的时候则需要用${VAR_NAME}的形式。此外还可以使用系统的环境变量,形式为$ENV{ENV_VAR_NAME},例如$ENV{PATH}$ENV{HOME}等。

除了缓存变量,option()指令设定的东西也可以被用CMake -Dxxx=ON的形式来覆盖。


add_definitions(-DCAFFE_VERSION=${CAFFE_TARGET_VERSION})

add_definitions()命令通常用来添加C/C++中的宏,例如:

  • add_defitions(-DCPY_ONLY) ,给编译器传递了预定义的宏CPU_ONLY,相当于代码中增加了一句#define CPU_ONLY
  • add_defitions(-DMAX_PATH_LEN=256),则相当于#define MAX_PATH_LEN 256
    根据文档,实际上add_definitions()可以添加任意的编译器flags,只不过像添加头文件搜索路径等flags被交给include_directory()等命令了。

在这里具体的作用是,设定CAFFE_VERSION这一C/C++宏的值为CAFFE_TARGET_VERSION变量的取值,而这一变量在前面分析过,它是缓存变量,有一个预设的默认值,也可以通过cmake .. -DCAFFE_TARGET_VERSION=x.y.z来指定为x.y.z


list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/Modules)

这里首先是list(APPEND VAR_NAME VAR_VALUE)这一用法,表示给变量VAR_NAME追加一个元素VAR_VALUE。虽然我写成VAR_NAME,但前面有提到,cmake中的变量几乎都是字符串或字符串数组,这里VAR_NAME你就当它是一个数组就好了,而当后续使用${VAR_NAME}时输出的是”整个数组的值“。(吐槽:这不就是字符串么?为什么用list这个名字呢?搞得像是在写不纯正的LIPS)

具体的说,这里是把项目根目录(CMakeLists.txt在项目根目录,${PROJECT_SOURCE_DIR}表示CMakeLists.txt所在目录)下的cmake/Modules子目录对应的路径值,追加到CMAKE_MODULE_PATH中;CMAKE_MODULE_PATH后续可能被include()find_package()等命令所使用。


include(ExternalProject)
include(GNUInstallDirs)

include()命令的作用:

  • 包含文件,
  • 或者,包含模块
    所谓包含文件,例如include(utils.cmake),把当前路径下的utils.cmake包含进来,基本等同于C/C++中的#include指令。通常,include文件的话文件应该是带有后缀名的。
    所谓包含模块,比如include(xxx),是说在CMAKE_MODULE_PATH变量对应的目录,或者CMake安装包自带的Modules目录(比如mac下brew装的cmake对应的是/usr/local/share/cmake/Modules)里面寻找xxx.cmake文件。注意,此时不需要写".cmake"这一后缀。

具体的说,这里是把CMake安装包提供的ExternalProject.cmake(例如我的是/usr/local/share/cmake/Modules/ExternalProject.cmake)文件包含进来。ExternalProject,顾名思义,引入外部工程,各种第三方库什么的都可以考虑用它来弄;

GNUInstallDirs也是对应到CMake安装包提供的GNUInstallDirs.cmake文件,这个包具体细节还不太了解,可自行翻阅该文件。


include(cmake/Utils.cmake)
include(cmake/Targets.cmake)
include(cmake/Misc.cmake)
include(cmake/Summary.cmake)
include(cmake/ConfigGen.cmake)

这里是实打实的包含了在项目cmake子目录下的5各cmake脚本文件了,是Caffe作者们(注意,完整的Caffe不是Yangqing Jia一个人写的)提供的,粗略看了下:

  • cmake/Utils.cmake: 定义了一些通用的(适用于其他项目的)函数,用于变量(数组)的打印、合并、去重、比较等(吐槽:cmake语法比较奇葩,相当一段时间之后我才发现它是lisp方式的语法,也就是函数(命令)是一等公民)
  • cmake/Targets.cmake: 定义了Caffe项目本身的一些函数和宏,例如源码文件组织、目录组织等。
  • cmake/Misc.cmake:杂项,比较抠细节的一些设定,比如通常CMAKE_BUILD_TYPE基本够用了,但是这里通过CMAKE_CONFIGURATION_TYPES来辅助设定CMAKE_BUILD_TYPE,等等
  • cmake/Summary.cmake:定义了4个打印函数,用来打印Caffe的一些信息,执行CMake时会在终端输出,相比于散落在各个地方的message()语句会更加系统一些
  • cmake/ConfigGen.cmake: 整个caffe编译好之后,如果别的项目要用它,那它也应该用cmake脚本提供配置信息。

这5个cmake脚本中具体的函数比较多,这里先放过,后续可能考虑逐一解读。


caffe_option(CPU_ONLY  "Build Caffe without CUDA support" OFF) # TODO: rename to USE_CUDA
caffe_option(USE_CUDNN "Build Caffe with cuDNN library support" ON IF NOT CPU_ONLY)
caffe_option(USE_NCCL "Build Caffe with NCCL library support" OFF)
caffe_option(BUILD_SHARED_LIBS "Build shared libraries" ON)
caffe_option(BUILD_python "Build Python wrapper" ON)
set(python_version "2" CACHE STRING "Specify which Python version to use")
caffe_option(BUILD_matlab "Build Matlab wrapper" OFF IF UNIX OR APPLE)
caffe_option(BUILD_docs   "Build documentation" ON IF UNIX OR APPLE)
caffe_option(BUILD_python_layer "Build the Caffe Python layer" ON)
caffe_option(USE_OPENCV "Build with OpenCV support" ON)
caffe_option(USE_LEVELDB "Build with levelDB" ON)
caffe_option(USE_LMDB "Build with lmdb" ON)
caffe_option(ALLOW_LMDB_NOLOCK "Allow MDB_NOLOCK when reading LMDB files (only if necessary)" OFF)
caffe_option(USE_OPENMP "Link with OpenMP (when your BLAS wants OpenMP and you get linker errors)" OFF)

# This code is taken from https://github.com/sh1r0/caffe-android-lib
caffe_option(USE_HDF5 "Build with hdf5" ON)

这里是设定各种option,也就是”开关“,然后后续根据开关的取值(布尔类型的变量,利用ifelse来判断),编写各自的构建规则。
其中caffe_option()cmake/Utils.cmake中定义的,它相比于cmake自带的option()命令,增加了可选的条件控制字段:

image.png

caffe_option()的具体实现还没有看懂,不过看一下所有用到的地方也都是很直观的:

image.png

具体的说,这里就是设定一些“高层级的编译选项开关”,比如是否编matlab接口、是否编python接口,是否用hdf5,是否用openmp,等等。


include(cmake/Dependencies.cmake)

这里是包含Dependencies.cmake,它里面配置了Caffe的绝大多数依赖库:

Boost
Threads
OpenMP
Google-glog
Google-gflags
Google-protobuf
HDF5
LMDB
LevelDB
Snappy
CUDA
OpenCV
BLAS
Python
Matlab
Doxygen

其中每一个依赖库库都直接(在Dependencies.cmake中)或间接(在各自的cmake脚本文件中)使用find_package()命令来查找包

使用find_package(),需要明确两点:

  1. find_package(Xxx)如果执行成功,则提供相应的Xxx_INCLUDE_DIRXxx_LIBRARY_DIR等变量,看起来挺方便,但其实并不是所有的库都提供了同样的变量后缀,其实都是由库的官方作者或第三方提供的xxx.cmake等脚本来得到的,依赖于生态。
  2. find_packge(Xxx)实际中往往是翻车重灾区。它其实有N大查找顺序,而CSDN上的博客中往往就瞎弄一个,你照搬后还是不行。具体例子:
  • 系统包管理工具装的OpenCV不带contrib模块,想使用自行编译的OpenCV但是git clone下来的开源代码执行后找不到自己编译的OpenCV。其实只要知道N大查找顺序,设定CMAKE_PREFIX_PATH中包含OpenCV路径后基本都能找到
  • Caffe基于cmake编译,依赖于Boost,系统里用apt或brew装了Boost,同时也自行编译了高版本Boost,现在Caffe编译时cmake只认自行编译版的Boost,指定N大查找顺序也不能找到系统的Boost。切换已安装的多个Boost给CMake find_package(),这时候需要看看FindBoost.cmake是怎么写的,必须提供它里面说的字样的变量(表示include和lib的查找路径),才能让find_package()起作用。
  • CMake编译安装了多个版本的Caffe(比如官方Caffe、SSD的Caffe),~/.cmake目录下会缓存一个caffe,而现在手头有一个做人脸检测的工程依赖Caffe,而你希望它用官方Caffe而不是SSD-Caffe,这个缓存目录很可能捣乱,这个我认为是某些项目比如Caffe的export输出是多余的,反而容易造成混淆

这里暂时不逐一分析每一个包的find_package()情况,只需要注意如果某个包你安装了但是cmake却没有找到,那就需要在find_package()前进行设定,以及之后排查。


if(UNIX OR APPLE)
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -Wall")
endif()

通过设定CMAKE_CXX_FLAGS,cmake生成各自平台的makefile、.sln或xcodeproject文件时设定同样的CXXFLAGS给编译器。如果是.c文件,则由c编译器编译,对应的是CMAKE_C_FLAGS

这里的set()指令设定CMAKE_CXX_FLAGS的值,加入了两个新的flags:"-fPIC"和"-Wall"。实际上用list(APPEND CMAKE_CXX_FLAGS "-fPIC -Wall")是完全可以的。set()只不过是有时候可能考虑设定变量默认值的时候用一用。

-fPIC作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
-Wall则是开启所有警告。根据个人的开发经验,C编译器的警告不能完全忽视,有些wanring其实应当当做error来对待,例如:

  • 函数未定义而被使用(忘记#include头文件)
  • 指针类型不兼容(incompatible)
    都有可能引发seg fault,甚至bus error。


caffe_set_caffe_link()

这里是设置Caffe_LINK这一变量,后续链接阶段会用到。它定义在cmake/Targets.cmake中:

image.png

可以看到,如果是编共享库(动态库),则就叫caffe;否则,则增加一些链接器的flags:-Wl是告诉编译器,后面紧跟的是链接器的flags而不是编译器的flags(现在的编译器往往是包含了调用连接器的步骤)。

这里的几个链接器参数,目前我没有细究过,具体看ld文档:https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_3.html


if(USE_libstdcpp)
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libstdc++")
  message("-- Warning: forcing libstdc++ (controlled by USE_libstdcpp option in cmake)")
endif()

USE_libstdcpp这个变量的含义:
在前面已经include(cmake/Dependencies.cmake)的情况下,Dependencies.cmake中的include(cmake/Cuda.cmake)使得Cuda的设定也被载入。而Cuda.cmake中的最后,判断如果当前操作系统是苹果系统并且>10.8、cuda版本小于7.0,那么使用libstdc++而不是libc++

image.png

这时候想起来还没毕业那会儿的一个新闻,说苹果移除了libstdc++而让大家换libc++的事情了,这个USE_libstdcpp就是这个意思了:如果cuda版本老(<7.0)并且OSX版本高(>10.8),就应该用libstdc++来兼容cuda。

这里还有一个小插曲:通常执行cmake后最前面会输出它所使用的C、C++编译器的可执行文件完整路径,然后一个同事的机器上把CXX环境变量设为/usr/bin/gcc,导致编译.cpp文件时是用CXX这一环境变量——也就是gcc——来编译.cpp文件。编译.cpp,如果是C++编译器来编译,链接阶段默认会把标准库链接进去,而现在是C编译器,没有明确指出要链接C++标准库,就会导致链接出问题,虽然他的CMakeLists.txt中曾经加入过libstdc++库,但是显然这很容易翻车,CXX环境变量不应该设定为/usr/bin/gcc


caffe_warnings_disable(CMAKE_CXX_FLAGS -Wno-sign-compare -Wno-uninitialized)

这里添加的编译器flags,是用来屏蔽特定类型的警告的。虽说眼不见心不烦,关掉后少些warning输出,但是0error0warning不应该是中级目标吗?


configure_file(cmake/Templates/caffe_config.h.in "${PROJECT_BINARY_DIR}/caffe_config.h")

这是设定configure file。configure_file()命令是把输入文件(第一个参数)里面的一些内容做替换(比如${var}@var@替换为具体的值,宏定义等),然后放到指定的输出文件(第二个参数)。其实还有其他没有列出的参数。

具体说,这里生成了build/caffe_config.h,里面define了几个变量:

image.png


set(Caffe_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include)
set(Caffe_SRC_DIR ${PROJECT_SOURCE_DIR}/src)
include_directories(${PROJECT_BINARY_DIR})

这里是设定两个自定义变量Caffe_INCLUDE_DIRCaffe_SRC_DIR的值,只不过它俩比较特殊,想想:如果以后别人find_package(Caffe),其实就需要其中的Caffe_INCLUDE_DIR的值。anyway,那些是后续export命令干的事情,这里忽略。

这里第三句include_directories()命令,把build目录加入到头文件搜索路径了,其实就是为了确保caffe_config.h能被正常include(就一个地方用到它):

image.png


# cuda_compile() does not have per-call dependencies or include pathes
# (cuda_compile() has per-call flags, but we set them here too for clarity)
#
# list(REMOVE_ITEM ...) invocations remove PRIVATE and PUBLIC keywords from collected    definitions and include pathes
if(HAVE_CUDA)
  # pass include pathes to cuda_include_directories()
  set(Caffe_ALL_INCLUDE_DIRS ${Caffe_INCLUDE_DIRS})
  list(REMOVE_ITEM Caffe_ALL_INCLUDE_DIRS PRIVATE PUBLIC)
  cuda_include_directories(${Caffe_INCLUDE_DIR} ${Caffe_SRC_DIR}                         ${Caffe_ALL_INCLUDE_DIRS})

  # add definitions to nvcc flags directly
  set(Caffe_ALL_DEFINITIONS ${Caffe_DEFINITIONS})
  list(REMOVE_ITEM Caffe_ALL_DEFINITIONS PRIVATE PUBLIC)
  list(APPEND CUDA_NVCC_FLAGS ${Caffe_ALL_DEFINITIONS})
endif()

擦亮眼睛:Caffe的cmake脚本中分别定义了Caffe_INCLUDE_DIRCaffe_INCLUDE_DIRS两个变量,只相差一个S,稍不留神容易混掉:不带S的值是$Caffe_ROOT/include,带S的值是各个依赖库的头文件搜索路径(在Dependencies.cmake中多次list(APPEND得到的。类似的,Caffe_DEFINITIONS也是在Dependencies.cmake中设定的。

这里判断出如果有CUDA的话就把Caffe_INCLUDE_DIRS变量中的PUBLICPRIVATE都去掉,把Caffe_DEFINITIONS中的PUBLICPRIVATE也去掉。

add_definitions()中添加的宏,用PUBLIC或PRIVATE修饰,有什么用?
以及,set()或list(APPEND来设定、更新的库名字,用PUBLIC、PRIVATE或INTERFACE修饰,有什么用?这里比较疑惑,尽管我找到了stack overflow上的这篇回答,但是仍然一头雾水:https://stackoverflow.com/questions/26037954/cmake-target-link-libraries-interface-dependencies

anyway,反正这里最后都做了list(REMOTE_ITEM操作,把PUBLICPRIVATE去掉了。


add_subdirectory(src/gtest)
add_subdirectory(src/caffe)
add_subdirectory(tools)
add_subdirectory(examples)
add_subdirectory(python)
add_subdirectory(matlab)
add_subdirectory(docs)

使用add_subdirectory(),意思是说把子目录中的CMakeLists.txt文件加载过来执行,从这个角度看似乎等同于include()命令。实则不然,因为它除了按给定目录名字后需要追加"/CMakeLists.txt"来构成完整路径外,往往都是包含一个target(类似于git中的submodule了),同时还可以设定别的一些参数:

  • 指定binary_dir
  • 设定EXCLUDE_FROM_ALL,也就是”搞一个独立的子工程“,此时需要有project()指令,并且不被包含在生成的.sln工程的ALL目标中,需要单独构建。

粗略看看各个子目录都是做什么的:

  • src/gtest,googletest的源码
  • src/caffe,caffe的源码构建,因为前面做了很多操作(依赖库、路径,etc),这里写的就比较少。任务只有2个:构建一个叫做caffe的库,以及test
  • tools,这一子目录下每一个cpp文件都生成一个xxx.bin的目标,而最常用的就是caffe训练接口build/caffe这个可执行文件了。
  • examples,这一子目录下有cpp_classification的C++代码,以及mnist,cifar10,siamse这三个例子的数据转换的代码,这四个都是C++文件,每一个都被编译出一个可执行
  • python,pycaffe接口,python/caffe/_caffe.cpp编译出动态库
  • matlab,matlab接口,./+caffe/private/caffe_.cpp编译出?编译出一个定制的目标,至于是啥类型,也许是动态库吧,玩过matlab和C/C++混编的都知道,用mex编译C/C++为.mexa文件,然后matlab调用.mexa文件,其实就是动态库
  • docs,文档,doxygen、jekyll都来了,以及拷贝每一个.ipynb文件。没错,add_custom_command()能定制各种target,只要你把想要执行的shell脚本命令用cmake的语法来写就可以了,很强大。

add_custom_target(lint COMMAND ${CMAKE_COMMAND} -P ${PROJECT_SOURCE_DIR}/cmake/lint.cmake)

这里依然是定制的target,具体看来是调用scripts/cpplint.py(谷歌官方C++代码风格检查工具)来执行代码风格检查。(个人觉得G家的C++风格有一点不太好:缩进两个空格太少了,费眼睛,强烈建议和Visual Studio保持一致,用tab并且tab宽度为4个空格)。

所谓linter就是语法检查器,除了cpplint其实还可以用cpp_checkgccclang等,我的vim中配置的就是用cpp_checkgcc,不妨试试:https://github.com/zchrissirhcz/dotvim


if(BUILD_python)
  add_custom_target(pytest COMMAND python${python_version} -m unittest discover -s caffe/test WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/python )
  add_dependencies(pytest pycaffe)
endif()

如果开启了BUILD_python开关,那么执行一个定制的target(执行pytest)。
add_dependencies()意思是指定依赖关系,这里要求pycaffe目标完成后再执行pytest目标,因为pytest需要用到pycaffe生成的caffe模块。pycaffe在前面提到的add_subdirectory(python)中被构建。


configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Uninstall.cmake.in
    ${CMAKE_CURRENT_BINARY_DIR}/cmake/Uninstall.cmake
    IMMEDIATE @ONLY)

add_custom_target(uninstall
    COMMAND ${CMAKE_COMMAND} -P
    ${CMAKE_CURRENT_BINARY_DIR}/cmake/Uninstall.cmake)

这里是添加”uninstall"这一target,具体定制的target其实就是执行cmake/Uninstall.cmake脚本。这个脚本根据cmake/Uninstall.cmake.in做变量取值替换等来生成得到。


# ---[ Configuration summary
caffe_print_configuration_summary()

# ---[ Export configs generation
caffe_generate_export_configs()

在Caffe根目录的CMakeLists.txt的最后,是打印各种配置的总的情况,以及输出各种配置(后者其实包含了install()指令的调用)

(2019-03-03 00:31:09 本篇写之前觉得不难,但是断断续续分析下来竟然用了大半天时间,对于CMake的一些指令细节重新查过,发现之前的掌握确实还不够)

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

推荐阅读更多精彩内容