python利用ctypes调用C/C++的动态库

ctypes 是 Python 的外部函数库。它提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数。可使用该模块以纯 Python 形式对这些库进行封装。

一、加载

加载库的方式

from ctypes import *     #用到类CDLL
mydll =  CDLL("/xx/xx/libxxxx.dll")   # windows平台
或者mydll =  cdll.LoadLibrary("/xx/xx/libxxxx.dll")
mydll =  CDLL("/xx/xx/libxxxx.so")   # linux平台
#调用其函数func
result = mydll.func(arg1)

根据当前平台分别加载Windows和Linux上的C的标准动态库msvcrt.dll和libc.so.6。

注意这里我们使用的ctypes.cdll来load动态库,实际上ctypes中总共有以下四种方式加载动态库:

  1. class ctypes.CDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)

    此类的实例即已加载的动态链接库。库中的函数使用标准 C 调用约定,并假定返回 int .在 Windows 上创建 CDLL 实例可能会失败,即使 DLL 名称确实存在

  2. class ctypes.OleDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)

    仅 Windows : 此类的实例即加载好的动态链接库,其中的函数使用 stdcall 调用约定,并且假定返回 windows 指定的 HRESULT 返回码。 HRESULT 的值包含的信息说明函数调用成功还是失败,以及额外错误码。 如果返回值表示失败,会自动抛出 OSError 异常。

  3. class ctypes.WinDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)

    仅 Windows: 此类的实例即加载好的动态链接库,其中的函数使用 stdcall 调用约定,并假定默认返回 int

  4. class ctypes.PyDLL(name, mode=DEFAULT_MODE, handle=None)

    这个类实例的行为与 CDLL 类似,只不过 不会 在调用函数的时候释放 GIL 锁,且调用结束后会检查 Python 错误码。 如果错误码被设置,会抛出一个 Python 异常。所以,它只在直接调用 Python C 接口函数的时候有用
    通过使用至少一个参数(共享库的路径名)调用它们,可以实例化所有这些类。也可以传入一个已加载的动态链接库作为 handler 参数,其他情况会调用系统底层的 dlopenLoadLibrary 函数将库加载到进程,并获取其句柄。如cdll.LoadLibrary()、oledll.LoadLibrary()、windll.LoadLibrary()、pydll.LoadLibrary()

WinDll虽然是可以应用于windows平台上,但是其只能加载标准函数调用约定为__stdcall的动态库;

msvcrt.dll中函数调用约定是C/C++默认的调用约定__cdecl,就不能用WinDll,得用CDLL

其中OleDLL对数据类型比较严格要求, 比如C代码中,如果让int跟float相加,返回不能是float,只能是int,而且结果还是错的.

方法/属性访问

这些类的实例没有共用方法。动态链接库的导出函数可以通过属性或者索引的方式访问。注意,通过属性的方式访问会缓存这个函数,因而每次访问它时返回的都是同一个对象。另一方面,通过索引访问,每次都会返回一个新的对象:

>>> from ctypes import CDLL
>>> libc = CDLL("libc.so.6")  # On Linux
>>> libc.time == libc.time
True
>>> libc['time'] == libc['time']
False

寻找动态库

ctypes.util.find_library(name)

尝试寻找一个库然后返回其路径名, name 是库名称, 且去除了 lib 等前缀和 .so.dylib 、版本号等后缀(这是 posix 连接器 -l 选项使用的格式)。如果没有找到对应的库,则返回 None

在 Linux 上, find_library() 会尝试运行外部程序(/sbin/ldconfig, gcc, objdump 以及 ld) 来寻找库文件。返回库文件的文件名。

在 3.6 版更改: 在Linux 上,如果其他方式找不到的话,会使用环境变量 LD_LIBRARY_PATH 搜索动态链接库。

在 Windows 上, find_library() 在系统路径中搜索,然后返回全路径,但是如果没有预定义的命名方案, find_library("c") 调用会返回 None
使用 ctypes 包装动态链接库,更好的方式 可能 是在开发的时候就确定名称,然后硬编码到包装模块中去,而不是在运行时使用 find_library() 寻找库。

把动态库设置环境变量

windows:只需要把关联动态库复制到加载的动态库同级目录下;

linux:需要添加环境变量
sudo echo /home/seetaFace6Python/seetaface/lib/centos  > /etc/ld.so.conf.d/seetaface6.conf
sudo ldconfig

可能遇到ldconfig: /lib64/libstdc++.so.6 不是符号连接

解决办法

[root@localhost lib64]# ln -sf /usr/lib64/libstdc++.so.6.0.19 /usr/lib64/libstdc++.so.6
[root@localhost lib64]# sudo ldconfig

容器设置方法:

  1. 直接在Dockefile中 利用COPY把动态库文件复制进镜像中,并且设置动态库环境变量.但是这种通用性不好

  2. 在Dockerfile中,新建lib文件夹,可以新建多个如/home/lib/lib1、/home/lib/lib2.并且设置环境变量,然后再docker run中利用-v进行挂载即可

    二、数据类型

ctypes 类型 C 类型 Python 数据类型
c_bool _Bool bool (1)
c_char char 单字符字节串对象
c_wchar wchar_t 单字符字符串
c_byte char int
c_ubyte unsigned char int
POINTER(c_ubyte) uchar* int
c_short short int
c_ushort unsigned short int
c_int int int
c_uint unsigned int int
c_long long int
c_ulong unsigned long int
c_longlong __int64long long int
c_ulonglong unsigned __int64unsigned long long int
c_size_t size_t int
c_ssize_t ssize_tPy_ssize_t int
c_float float float
c_double double float
c_longdouble long double float
c_char_p char * (NUL terminated) 字节串对象或 None
c_wchar_p wchar_t * (NUL terminated) 字符串或 None
c_void_p void * int 或 None

该表格列举了ctypes、c和python之间基本数据的对应关系,在定义函数的参数和返回值时,需记住几点:

必须使用ctypes的数据类型
参数类型用关键字argtypes定义argtypes必须是一个序列,如tuple或list,否则会报错

返回类型用restype定义,使用 None 表示 void,即不返回任何结果的函数

若没有显式定义参数类型和返回类型,python默认为int型
cast() 函数可以将一个指针实例强制转换为另一种 ctypes 类型。 cast() 接收两个参数,一个 ctypes 指针对象或者可以被转换为指针的其他类型对象,和一个 ctypes 指针类型。 返回第二个类型的一个实例,该返回实例和第一个参数指向同一片内存空间:

>>> a = (c_byte * 4)()
>>> cast(a, POINTER(c_int))
<ctypes.LP_c_long object at ...>

所以 cast() 可以用来给结构体 Barvalues 字段赋值:

>>> bar = Bar()
>>> bar.values = cast((c_byte * 4)(), POINTER(c_int))
>>> print(bar.values[0])
0

cast应用:获取numpy数组指针

a = np.asarray(range(16), dtype=np.int32).reshape([4,4])
if not a.flags['C_CONTIGUOUS']:
  a = np.ascontiguous(a, dtype=a.dtype) # 如果不是C连续的内存,必须强制转换
a_ctypes_ptr = cast(a.ctypes.data, POINTER(c_int))  #转换为ctypes,这里转换后的可以直接利用ctypes转换为c语言中的int*,然后在c中使用
for i in range(16):
  print(a_ctypes_ptr[i])

三、C代码编译

如下c代码,deme.c

/******C端代码*********/
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

int add(int a,float b){
    printf("a=%d\n", a);
    printf("b=%f\n", b);
    return a+b;
}

int hello()
{
    printf("Hello world\n");
    return 0;    
}

编译

gcc -fPIC -shared -o libdeme.so deme.c

四、C++代码编译

由于ctypes是与C兼容的数据类型,也就是针对C进行编译后进行调用,所以直接对C++代码编译,在python调用时,会提示找不到函数

Traceback (most recent call last):
  File "d:\AI\C++_study\Test\demo.py", line 7, in <module>
    dll.hello()
  File "D:\Anaconda3\envs\py36\lib\ctypes\__init__.py", line 361, in __getattr__
    func = self.__getitem__(name)
  File "D:\Anaconda3\envs\py36\lib\ctypes\__init__.py", line 366, in __getitem__
    func = self._FuncPtr((name_or_ordinal, self))
AttributeError: function 'hello' not found

特别注意在调用C++函数需要在函数声明时,加入前缀“ extern "C" ”,这是由于C++支持函数重载功能,在编译时会更改函数名。在函数声明时,前缀extern "C"则确保按C的方式编译。

c++需要demo.cpp,如下

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

#define DLLEXPORT extern "C" __declspec(dllexport)  
DLLEXPORT float __stdcall add(int a,float b){
    printf("a=%d\n", a);
    printf("b=%f\n", b);
    return a+b;
}

DLLEXPORT int __stdcall hello()
{
    printf("Hello world\n");
    return 0;    
}

__declspec(dllexport)可以省略,其他都不可以

或者如下简单书写:

extern "C" float add(int a,float b){
    printf("a=%d\n", a);
    printf("b=%f\n", b);
    return a+b;
}

四、python代码

demo.py

# -*- coding: utf-8 -*-
from ctypes import *

# dll =CDLL("./libdemo.so")
# dll = cdll.LoadLibrary("./libdemo.so")
# dll = windll.LoadLibrary("./libdemo.so")

dll = PyDLL("./libdemo.so")
dll.hello()
dll.add.argtypes=[c_int,c_float]
dll.add.restype=c_float
a=c_int(10)
b=c_float(20.5)
res= dll.add(a,b)
print("res=",res)

结果如下

Hello world
a=10
b=20.500000
res= 30.5

五、指针的使用

5.1 创建指针

函数 说明
byref(x [, offset]) 返回 x 的地址,x 必须为 ctypes 类型的一个实例。相当于 c 的 &x 。 offset 表示偏移量。
pointer(x) 创建并返回一个指向 x 的指针实例, x 是一个实例对象。
POINTER(type) 返回一个类型,这个类型是指向 type 类型的指针类型, type 是 ctypes 的一个类型。

byref 很好理解,传递参数的时候就用这个,用 pointer 创建一个指针变量也行,不过 byref 更快。
而 pointer 和 POINTER 的区别是,pointer 返回一个实例,POINTER 返回一个类型。甚至你可以用 POINTER 来做 pointer 的工作:

>>> a = c_int(66)         # 创建一个 c_int 实例
>>> b = pointer(a)        # 创建指针
>>> c = POINTER(c_int)(a) # 创建指针
>>> b
<__main__.LP_c_long object at 0x00E12AD0>
>>> c
<__main__.LP_c_long object at 0x00E12B20>
>>> b.contents            # 输出 a 的值
c_long(66)
>>> c.contents            # 输出 a 的值
c_long(66)

可以将 ctypes 类型数据传入 pointer() 函数创建指针:

>>> from ctypes import *
>>> i = c_int(42)
>>> pi = pointer(i)

指针实例拥有 contents 属性,它返回指针指向的真实对象,如上面的 i 对象:

>>> pi.contents
c_long(42)

注意 ctypes 并没有 OOR (返回原始对象), 每次访问这个属性时都会构造返回一个新的相同对象:

>>> pi.contents is i
False
>>> pi.contents is pi.contents
False

将这个指针的 contents 属性赋值为另一个 c_int 实例将会导致该指针指向该实例的内存地址

指针对象也可以通过整数下标进行访问和赋值,赋值会把原来的值内容覆盖

>>> print(i)
c_long(99)
>>> pi[0] = 22
>>> print(i)
c_long(22)

无参调用指针类型可以创建一个 NULL 指针。 NULL 指针的布尔值是 False

>>> null_ptr = POINTER(c_int)()
>>> print(bool(null_ptr))
False

有时候 C 函数接口可能由于要往某个地址写入值,或者数据太大不适合作为值传递,从而希望接收一个 指针 作为数据参数类型。

使用bytef()来引用传递参数

5.2 指针传递值:

用byref()

C/C++:

DLLEXPORT void __stdcall add_point(float* a, float* b, float* c)
{
    *c = *a + *b;
    *a = 129.7;
}

python

x1 = ctypes.c_float(1.9)
x2 = ctypes.c_float(10.1)
x3 = ctypes.c_float(0)
dll.add_point(byref(x1),byref(x2),byref(x3))
print("x1=",x1)
print("x2=",x2)
print("x3=",x3)

结果为:

x1= 129.6999969482422
x2= 10.100000381469727
x3= 12.0

值随着指针进行改变,另外在小数位上会进行值变动.小数位保留7位的话,基本一致

5.3 接收指针数据:

利用POINT()来接收指针数据,在接收类型中声明

C/C++

DLLEXPORT int* __stdcall point(int* x)
{
    int* y=NULL;
    y = x;
    return y;
}

PYTHON:

x = ctypes.c_int(2560)
Cfun.point.restype = ctypes.POINTER(ctypes.c_int)  ##声明函数返回值为int*
y = Cfun.point(ctypes.byref(x))
print("y is %d" % y[0])

不可接收返回数组,因为返回的为内存地址

5.4 数组的使用

在C中创建array函数:

DLLEXPORT void __stdcall get_array(int x[])
{
     printf("x[0]= %d x[1]=%d x[2]=%d x[3]=%d \n", *x,x[1],x[2],x[3]);
     *x = 100;
}

python:

Array = ctypes.c_int * 4;  ##声明一维数组,数组长度为4
a = Array(0, 1, 2, 3)  ##初始化数组
dll.get_array(a)
print(a[0], a[1], a[2], a[3])   # 数组没办法打印整体
# 这一把数组转为列表再打印
a_list=[]
for i in range(4):
    a_list.append(a[i])
print(a_list)

结果:

x[0]= 0 x[1]=1 x[2]=2 x[3]=3 
 100 1 2 3

还可以初始化一个空,再赋值

Array = c_int * 4
func_list = Array()
for i in range(4):
    func_list[i] = i

还可以初始化一个列表再转为数组

pyarr=[1,2,3,4]
arr = (ctypes.c_int * len(pyarr))(*pyarr)

在C中修改数组的值,在python中确实被修改。

声明二维数组的方法:

Array = (ctypes.c_int * 4)*5  ##声明二维数组
a=Array()
###使用循环对其进行赋值
for i in range(5):
    for j in range(4):
        a[i][j]=i*j

三维:

list3d = [
    [[0.0, 1.0, 2.0, 3.0], [4.0, 5.0, 6.0, 7.0]], 
    [[0.2, 1.2, 2.2, 3.2], [4.2, 5.2, 6.2, 7.2]],
    [[0.4, 1.4, 2.4, 3.4], [4.4, 5.4, 6.4, 7.4]],
]

arr = (c_double * 4 * 2 * 3)(*(tuple(tuple(j) for j in i) for i in list3d))

检查它是否以行优先顺序正确初始化:

>>> (c_double * 24).from_buffer(arr)[:]
[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 
 0.2, 1.2, 2.2, 3.2, 4.2, 5.2, 6.2, 7.2, 
 0.4, 1.4, 2.4, 3.4, 4.4, 5.4, 6.4, 7.4]

不可接收返回数组,因为范围的为内存地址

数组长度问题,python数组传到c++后,使用sizeof(arr) / sizeof(arr[0]) 的结果是错的,所以需要把数组的长度当做参数传入

5.5 字符串传递

字符串

C/C++接收的类型为char* ,即byte类型,需要对字符串进行编码,,利用b"内容",或者内容.encode()后进行传输,或者用bytes("nihao", 'utf-8')返回值需要decode,

C++:

DLLEXPORT char* __stdcall get_str(char * path)
{
    cout<<"path:"<<path<<endl;
    char* ret;
    ret = (char *)malloc(10);
    strcpy(ret, "你好hello123,./");
    return ret;
}

python:

dll.get_str.argtypes=[c_char_p]
tex= "你好呀dsf123,./"   #或者b"你好呀dsf123,./"
texd=tex.encode()  #或者bytes("nihao", 'utf-8')
text = c_char_p(texd)
dll.get_str.restype=c_char_p
rt_str=dll.get_str(text)
print(rt_str.decode())

字符串传输给C/C++,如果有中文会乱码,但返回有中文不会有乱码

不用了要free释放,否则会造成内存泄漏

字符串列表

c/c++

//构建字符串数组,2个元素
struct struct_str_arr
{
    char* str_ptr[2];
};
struct_str_arr str_arr;
struct_str_arr* str_arr_ptr = (struct_str_arr*)malloc(sizeof(str_arr));

DLLEXPORT struct_str_arr* __stdcall get_str_list(int n, char *b[2])
{
    for(int i=0;i<n;i++)
    {
        printf("%s", *(b+i));
        printf("\n");
    }
    str_arr_ptr->str_ptr[0]="你好";
    str_arr_ptr->str_ptr[1]="hell";
    return str_arr_ptr;
}

python:

# 返回的数据
class StructPointer(ctypes.Structure):  
    _fields_ = [("str_ptr", ctypes.c_char_p * 2)]  


dll.test_str_arr.restype = ctypes.POINTER(StructPointer)
str1 = c_char_p(bytes("nihao", 'utf-8'))
str2 = c_char_p(bytes("shijie", 'utf-8'))
a = (c_char_p*2)(str1, str2)
ret_ptr =lib.get_str_list(2, a)
#ret_ptr =lib.get_str_list(2, pointer(a))
ret_ptr.contents.str_ptr[1].decode()

5.6 结构体传递

5.6.1 cvMat传递

python中opencv存储一幅图像的数据类型是array,而在C++中opencv存储一幅图像的数据类型是Mat,这两者之间的转换需要通过unsigned char * 来完成。

unsigned char*等价于uchar*

数据类型对应关系

python:     ctypes.POINTER(ctypes.c_ubyte) 或者ctypes.c_char_p
C++:        unsigned char *

python中将array转换成ctypes.POINTER(ctypes.c_ubyte)

import ctypes as C
import cv2

img = cv2.imread('ROI0.png')
#将img转换成可被传入dll的数据类型
input = img.ctypes.data_as(C.POINTER(C.c_ubyte))

C++ 中将uchar 转为cvMat*

Mat src = Mat(rows,cols,CV_8UC3,src_data);
//或者分为两步,利用Mat.data
// Mat src = Mat(Size(cols, rows), CV_8UC3, Scalar(255, 255, 255));  //建立空图
// src.data = src_data;

C++中将uchar 复制的方法*

ret_data在入参中作为指针传递:
memcp(ret_data,src.data,rows*cols*3);
ret_data作为返回结果传递:
    vector<uchar> data_encode;
    imencode(".png", dst, data_encode);  //把图片dst信息保存到缓存data_encode中
    std::string str_encode(data_encode.begin(), data_encode.end());
    uchar* char_r = new uchar[str_encode.size() + 10];     
    memcpy(char_r, str_encode.data(), sizeof(char) * (str_encode.size()));
    return char_r;

python中将uchar转为array*

#a为uchar*的数据
b =string_at(a,cols*rows*channels)   # 类似于base64
nparr = np.frombuffer(b, np.uint8)
img_decode= cv2.imdecode(nparr,cv2.IMREAD_COLOR)

完整代码

C++:

#include <iostream>
#include <opencv2/opencv.hpp>
using namespace cv;

//作为返回值返回
extern "C" uchar* mattostring(uchar* src_data,int rows,int cols){
    Mat dst = Mat(Size(cols, rows), CV_8UC3, Scalar(255, 255, 255));  //建立空图
    dst.data = mat_data;
    circle(dst, Point(60, 60), 10, Scalar(255, 0, 0)); //画图

    vector<uchar> data_encode;
    imencode(".png", dst, data_encode);
    std::string str_encode(data_encode.begin(), data_encode.end());
    uchar* char_r = new uchar[str_encode.size() + 10];     
    memcpy(char_r, str_encode.data(), sizeof(char) * (str_encode.size()));
    return char_r;
}

//作为入参指针传递
extern "C" void draw_circle(int rows, int cols, unsigned char *src_data , unsigned char *ret_data)
{
    Mat src = Mat(rows, cols, CV_8UC3, src_data);  //uchar* 转cvMat
    circle(src, Point(60, 60), 10, Scalar(255, 0, 0));  //画图
    //将Mat转换成unsigned char
    memcpy(ret_data, src.data, rows*cols*3);
}

python:

from ctypes import *
import cv2
import numpy as np
from PIL import Image
MYDLL= CDLL("./build/libhello.dll")
MYDLL.hello()

image=cv2.imread("./images/ch1.jpg")
rows = image.shape[0]
cols = image.shape[1]
channels =3

MYDLL.mattostring.argtypes = (POINTER(c_ubyte), c_int,c_int) #c_char_p也可以
MYDLL.mattostring.restype = c_void_p   # POINTER(c_ubyte) 跟c_void_p都可以
MYDLL.draw_circle.argtypes=[c_int,c_int,POINTER(c_ubyte),POINTER(c_ubyte)]
MYDLL.draw_circle.restype=c_void_p

ret_img = np.zeros(dtype=np.uint8, shape=(rows, cols, 3))
srcPointer=image.ctypes.data_as(POINTER(c_ubyte))  #方式1.1
#srcPointer=image.ctypes.data_as(c_char_p)     #方式1.2
# srcPointer = image.astype(np.uint8).tostring()     #方式2

a=MYDLL.mattostring(srcPointer,rows,cols)
b =string_at(a,cols*rows*channels)   # 类似于base64

nparr = np.frombuffer(b, np.uint8)  # 转array,但是维度不是图片
img_decode= cv2.imdecode(nparr,cv2.IMREAD_COLOR) #转cvMat 
img_decode=Image.fromarray(img_decode[:,:,::-1])  # 由于直接cv2.imshow()显示出来的图是错误的,保存或者转为Image格式,显示正确
img_decode.show()

retPoint = ret_img.ctypes.data_as(POINTER(c_ubyte))
MYDLL.draw_circle(rows, cols, srcPointer, retPoint)  
ret_img_out = Image.fromarray(ret_img[:,:,::-1])  # 参数指针传递,不需要从uchar*转换,只需要取他的源头数据即可.
ret_img_out.show()

第二种借助numpy来进行转换,两者的区别就是,第一种传的是指针,如果参数进去,在mattostring函数内对变量srcPointer进行修改则会影响最终输出的内容,第二种方式不会有影响。方式2 的argtypes注释掉如上面的代码,在最后加上

cv2.imshow("image",image)  #方式1有画圈
cv2.waitKey(0)   # 方式2还是原图,

这里用到了opencv,所以编译还需要把opencv的头文件跟库文件加入

CMakeLists.txt:

cmake_minimum_required (VERSION 2.6)
project(hello)
set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
set(OpenCV_DIR D:/opencv/opencv-4.5.2)  # 该地址为OpenCVConfig.cmake所在的目录地址
find_package(OpenCV REQUIRED)
set(SRC_LIST hello.cpp)
# 添加头文件
include_directories( ${OpenCV_INCLUDE_DIRS} )    # 可省略,在find_package中已经实现
message("CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}")
add_library( hello SHARED hello.cpp)
# 链接OpenCV库
target_link_libraries( hello ${OpenCV_LIBS} )

上述方法把图片的尺寸写死了,但一般图片都是动态的,所以需要把尺寸信息也要传递回来.这时采用dtypes的结构体方式

C++结构体及使用代码:

struct CvMatImage{
    //cv图片结构体
    int rows;
    int cols;
    int channels;
    uchar *data;
};

extern "C" CvMatImage mattostring(uchar* src_data,int rows,int cols){
    Mat dst = Mat(rows, cols, CV_8UC3, src_data);
    circle(dst, Point(60, 60), 10, Scalar(255, 0, 0)); //画图
    vector<uchar> data_encode;
    imencode(".png", dst, data_encode);
    std::string str_encode(data_encode.begin(), data_encode.end());
    uchar* char_r = new uchar[str_encode.size() + 10];     
    memcpy(char_r, str_encode.data(), sizeof(char) * (str_encode.size()));
    CvMatImage cvimage{310,310,3,char_r};
    return cvimage;

}

python结构体及使用代码:(名称、属性名要一致,类型要对应上)

from ctypes import *
import cv2
import numpy as np
from PIL import Image
from typing import List

class CvMatImage(Structure):
    # cvMatImage的结构体
    rows:int
    cols:int
    channels:int
    data:(List[c_ubyte])
    _fields_ = [("rows",c_int32),("cols",c_int32),("channels",c_int32),("data",POINTER(c_ubyte))]
    def __str__(self):
        return "CvImageData(rows={},cols={},channels={},data:{})".format(self.rows,self.cols,self.channels,List[c_ubyte])

def get_numpy_by_cvImage(cvimage):
    """
    结构体转为numpy图片
    param  cvimage:cvimage的结构体,包含rows,cols,channels,data
    return  :numpy图片
    """
    data = cvimage.data
    cv_rows = cvimage.rows
    cv_cols =cvimage.cols
    cv_channels = cvimage.channels
    b =string_at(data,cv_cols*cv_rows*cv_channels)   # 类似于base64
    nparr = np.frombuffer(b, np.uint8)
    img_decode= cv2.imdecode(nparr,cv2.IMREAD_COLOR)
    return img_decode

MYDLL = CDLL("./build/libhello.dll")
image = cv2.imread("./images/ch1.jpg")
(rows,cols,channels) = image.shape
MYDLL.mattostring.argtypes = (POINTER(c_ubyte), c_int, c_int)
MYDLL.mattostring.restype = CvMatImage   # todo 这里设置非常重要
srcPointer = image.ctypes.data_as(POINTER(c_ubyte))  
cvimage = MYDLL.mattostring(srcPointer, rows, cols)
img_decode = get_numpy_by_cvImage(cvimage)
img_decode = Image.fromarray(img_decode[:, :, ::-1])
img_decode.show()

5.6.2 seetaImageData传递

seetaImageData为seetaface的图片数据格式,本身有头文件#include <seeta/Common/Struct.h>

struct SeetaImageData
{
    int width;              // 图像宽度
    int height;             // 图像高度
    int channels;           // 图像通道
    unsigned char *data;    // 图像数据
};

故在python中定义一样的结构体

class SeetaImageData(Structure):
    width: int
    height: int
    channels: int
    data:List[c_ubyte]
    _fields_=[('width',c_int32),('height',c_int32),('channels',c_int32),("data",POINTER(c_ubyte))]

    def __str__(self):
        return "SeetaImageData(width={},height={},channels={},data:{})".format(self.width,self.height,self.channels,List[c_ubyte])

C++代码(函数部分):

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <seeta/Common/Struct.h>
#include <seeta/FaceDetector.h>

using namespace std;
using namespace cv;

seeta::FaceDetector* new_fd() {
    seeta::ModelSetting setting;
    setting.device = SEETA_DEVICE_GPU;  //GPU CPU AUTO
     setting.id = 0;
    setting.append("./models/face_detector.csta");
  //按原始cpp文件所在的路径为参考,而不是动态库所在路径.但如果是执行文件,那么按执行文件的位置
    return new seeta::FaceDetector(setting);
}

extern "C" SeetaFaceInfoArray Detect(SeetaImageData simage){
    seeta::FaceDetector *faceDetector = new_fd();
    SeetaFaceInfoArray faces = faceDetector->detect(simage);
    return faces;
 }

注意: C++代码所涉及的文件的相对地址:

  • 生成动态库时,,参考为原始cpp文件,
  • 生成执行文件,参考编译后的执行文件

python代码(调用部分):

def get_seetaImageData_by_numpy(image_np: np.array) -> SeetaImageData:
    """
    
    param  image_np:numpy数组
    return  :seetaImageData结构体
    """
    
    seetaImageData = SeetaImageData()
    height, width, channels = image_np.shape
    seetaImageData.height = int(height)
    seetaImageData.width = int(width)
    seetaImageData.channels = int(channels)
    seetaImageData.data = image_np.ctypes.data_as(POINTER(c_ubyte))
    return seetaImageData

MYDLL = CDLL("./lib/centos/libFaceAPI.so")
image = cv2.imread("./images/ch1.jpg")
(rows,cols,channels) = image.shape
simage = get_seetaImageData_by_numpy(image)
MYDLL.Detect.argtypes = (SeetaImageData,)
MYDLL.Detect.restype = SeetaFaceInfoArray
detect_result = MYDLL.Detect(simage)
#打印结果为SeetaFaceInfoArray(data:[SeetaFaceInfo(pos=SeetaRect(x=101,y=50,width=96,height=126),score=0.9995892643928528)],size:1)
rect_list = detect_result.data

注:关于入参说明

  • MYDLL.Detect.argtypes = (SeetaImageData,) 传的为数据,则在C++中,入参为SeetaImageData
  • MYDLL.Detect.argtypes = (POINT(SeetaImageData),) 传的为地址,则C++中,入参为SeetaImageData&

5.7 结构体数组传递

SeetaPointF *5的数组传递

SeetaPointF 的结构体为:

# python
class SeetaPointF(Structure):
    x: int
    y: int
    _fields_=[('x',c_double),('y',c_double)]
// c++
struct SeetaPointF
{
    double x;
    double y;
};

作为参数指针传入:

C++:

extern "C" int mark5(SeetaImageData &simage, SeetaRect &box, SeetaPointF points5[5])
{
    std::vector<SeetaPointF> points = landDetector5->mark(simage, box);
    int size = points.size();
    for (int i = 0; i < size; i++)
        points5[i] = points[i];  //由于points为vecter,需要转化为数组.
    return 1;
}

python:

MYDLL.mark5.argtypes = (POINTER(SeetaImageData),POINTER(SeetaRect),POINTER(SeetaPointF))
# 或者
# MYDLL.mark5.argtypes = (POINTER(SeetaImageData),POINTER(SeetaRect),SeetaPointF*5) 
detect_result = MYDLL.Detect(simage)
rect_list = detect_result.data
if detect_result.size > 0:
    _face = detect_result.data[0].pos
    points = (SeetaPointF * 5)()  #初始化一个长度为5的空数组
    MYDLL.mark5(simage, _face,points) #作为参数以地址传递,那么在c代码中points改变了,python中的point也会改变

关于结构体数组传入后,队员原始函数的入参为结构体* 的使用方法.

说明:入参结构体* 即传入结构体数组的起始也就是第一个数据的地址. 

方法一: 把数组转为vector,那么入参就可以改为 .data()
extern "C" int Predict(SeetaImageData &simage, const SeetaRect &box, SeetaPointF points5[5])
{
    std::vector<SeetaPointF> points;
    for (int i = 0; i < 5; i++)
    {
        points.push_back(points5[i]);
    }
    auto status = liveDetector->Predict(simage, box, points.data());
    
}
方法二: 直接提取结构体数组的第一个数据的地址作为入参即可
extern "C" int Predict(SeetaImageData &simage, const SeetaRect &box, SeetaPointF points5[5])
{
 auto status = liveDetector->Predict(simage, box, &points5[0]);  //&poinst5[0]为提取第一个元素的地址
 }
 

返回结构体数组

<font color='red'>放弃吧,因为C代码返回的为数组地址.C可以调用的时候取值,但python没办法去通过内存取值</font>

六、指导说明

工具函数

  • ctypes.addressof(obj)

    以整数形式返回内存缓冲区地址。 obj 必须为一个 ctypes 类型的实例。引发一个 审计事件 ctypes.addressof,附带参数 obj

  • ctypes.alignment(obj_or_type)

    返回一个 ctypes 类型的对齐要求。 obj_or_type 必须为一个 ctypes 类型或实例。

  • ctypes.byref(obj[, offset])

    返回指向 obj 的轻量指针,该对象必须为一个 ctypes 类型的实例。 offset 默认值为零,且必须为一个将被添加到内部指针值的整数。byref(obj, offset) 对应于这段 C 代码:(((char *)&obj) + offset)返回的对象只能被用作外部函数调用形参。 它的行为类似于 pointer(obj),但构造起来要快很多。

  • ctypes.cast(obj, type)

    此函数类似于 C 的强制转换运算符。 它返回一个 type 的新实例,该实例指向与 obj 相同的内存块。 type 必须为指针类型,而 obj 必须为可以被作为指针来解读的对象。

  • ctypes.create_string_buffer(init_or_size, size=None)

    此函数会创建一个可变的字符缓冲区。 返回的对象是一个 c_char 的 ctypes 数组。init_or_size 必须是一个指明数组大小的整数,或者是一个将被用来初始化数组条目的字节串对象。如果将一个字节串对象指定为第一个参数,则将使缓冲区大小比其长度多一项以便数组的最后一项为一个 NUL 终结符。 可以传入一个整数作为第二个参数以允许在不使用字节串长度的情况下指定数组大小。引发一个 审计事件 ctypes.create_string_buffer,附带参数 init, size

  • ctypes.create_unicode_buffer(init_or_size, size=None)

    此函数会创建一个可变的 unicode 字符缓冲区。 返回的对象是一个 c_wchar 的 ctypes 数组。init_or_size 必须是一个指明数组大小的整数,或者是一个将被用来初始化数组条目的字符串。如果将一个字符串指定为第一个参数,则将使缓冲区大小比其长度多一项以便数组的最后一项为一个 NUL 终结符。 可以传入一个整数作为第二个参数以允许在不使用字符串长度的情况下指定数组大小。引发一个 审计事件 ctypes.create_unicode_buffer,附带参数 init, size

  • ctypes.DllCanUnloadNow()

    仅限 Windows:此函数是一个允许使用 ctypes 实现进程内 COM 服务的钩子。 它将由 _ctypes 扩展 dll 所导出的 DllCanUnloadNow 函数来调用。

  • ctypes.DllGetClassObject()

    仅限 Windows:此函数是一个允许使用 ctypes 实现进程内 COM 服务的钩子。 它将由 _ctypes 扩展 dll 所导出的 DllGetClassObject 函数来调用。

  • ctypes.util.find_library(name)

    尝试寻找一个库并返回路径名称。 name 是库名称并且不带任何前缀如 lib 以及后缀如 .so.dylib 或版本号(形式与 posix 链接器选项 -l 所用的一致)。 如果找不到库,则返回 None。确切的功能取决于系统。

  • ctypes.util.find_msvcrt()

    仅限 Windows:返回 Python 以及扩展模块所使用的 VC 运行时库的文件名。 如果无法确定库名称,则返回 None。如果你需要通过调用 free(void *) 来释放内存,例如某个扩展模块所分配的内存,重要的一点是你应当使用分配内存的库中的函数。

  • ctypes.FormatError([code])

    仅限 Windows:返回错误码 code 的文本描述。 如果未指定错误码,则会通过调用 Windows api 函数 GetLastError 来获得最新的错误码。

  • ctypes.GetLastError()

    仅限 Windows:返回 Windows 在调用线程中设置的最新错误码。 此函数会直接调用 Windows GetLastError() 函数,它并不返回错误码的 ctypes 私有副本。

  • ctypes.get_errno()

    返回调用线程中系统 errno 变量的 ctypes 私有副本的当前值。引发一个 审计事件 ctypes.get_errno,不附带任何参数。

  • ctypes.get_last_error()

    仅限 Windows:返回调用线程中系统 LastError 变量的 ctypes 私有副本的当前值。引发一个 审计事件 ctypes.get_last_error,不附带任何参数。

  • ctypes.memmove(dst, src, count)

    与标准 C memmove 库函数相同:将 count 个字节从 src 拷贝到 dstdstsrc 必须为整数或可被转换为指针的 ctypes 实例。

  • ctypes.memset(dst, c, count)

    与标准 C memset 库函数相同:将位于地址 dst 的内存块用 count 个字节的 c 值填充。 dst 必须为指定地址的整数或 ctypes 实例。

  • ctypes.POINTER(type)

    这个工厂函数创建并返回一个新的 ctypes 指针类型。 指针类型会被缓存并在内部重用,因此重复调用此函数耗费不大。 type 必须为 ctypes 类型。

  • ctypes.pointer(obj)

    此函数会创建一个新的指向 obj 的指针实例。 返回的对象类型为 POINTER(type(obj))。注意:如果你只是想向外部函数调用传递一个对象指针,你应当使用更为快速的 byref(obj)

  • ctypes.``resize(obj, size)

    此函数可改变 obj 的内部内存缓冲区大小,其参数必须为 ctypes 类型的实例。 没有可能将缓冲区设为小于对象类型的本机大小值,该值由 sizeof(type(obj)) 给出,但将缓冲区加大则是可能的。

  • ctypes.``set_errno(value)

    设置调用线程中系统 errno 变量的 ctypes 私有副本的当前值为 value 并返回原来的值。引发一个 审计事件 ctypes.set_errno 附带参数 errno

  • ctypes.set_last_error(value)

    仅限 Windows:设置调用线程中系统 LastError 变量的 ctypes 私有副本的当前值为 value 并返回原来的值。引发一个 审计事件 ctypes.set_last_error,附带参数 error

  • ctypes.sizeof(obj_or_type)

    返回 ctypes 类型或实例的内存缓冲区以字节表示的大小。 其功能与 C sizeof 运算符相同。

  • ctypes.string_at(address, size=-1)

    此函数返回从内存地址 address 开始的以字节串表示的 C 字符串。 如果指定了 size,则将其用作长度,否则将假定字符串以零值结尾。引发一个 审计事件 ctypes.string_at,附带参数 address, size

  • ctypes.WinError(code=None, descr=None)

    仅限 Windows:此函数可能是 ctypes 中名字起得最差的函数。 它会创建一个 OSError 的实例。 如果未指定 code,则会调用 GetLastError 来确定错误码。 如果未指定 descr,则会调用 FormatError() 来获取错误的文本描述。在 3.3 版更改: 以前是会创建一个 WindowsError 的实例。

  • ctypes.wstring_at(address, size=-1)

    此函数返回从内存地址 address 开始的以字符串表示的宽字节字符串。 如果指定了 size,则将其用作字符串中的字符数量,否则将假定字符串以零值结尾。引发一个 审计事件 ctypes.wstring_at,附带参数 address, size

数据类型

  • class ctypes._CData

    这个非公有类是所有 ctypes 数据类型的共同基类。 另外,所有 ctypes 类型的实例都包含一个存放 C 兼容数据的内存块;该内存块的地址可由 addressof() 辅助函数返回。 还有一个实例变量被公开为 _objects;此变量包含其他在内存块包含指针的情况下需要保持存活的 Python 对象。ctypes 数据类型的通用方法,它们都是类方法(严谨地说,它们是 metaclass 的方法):

    from_buffer(source[, offset])此方法返回一个共享 source 对象缓冲区的 ctypes 实例。 source 对象必须支持可写缓冲区接口。 可选的 offset 形参指定以字节表示的源缓冲区内偏移量;默认值为零。 如果源缓冲区不够大则会引发 ValueError。引发一个 审计事件 ctypes.cdata/buffer 附带参数 pointer, size, offset

    from_buffer_copy(source[, offset])此方法创建一个 ctypes 实例,从 source 对象缓冲区拷贝缓冲区,该对象必须是可读的。 可选的 offset 形参指定以字节表示的源缓冲区内偏移量;默认值为零。 如果源缓冲区不够大则会引发 ValueError。引发一个 审计事件 ctypes.cdata/buffer 附带参数 pointer, size, offset

    from_address(address)此方法会使用 address 所指定的内存返回一个 ctypes 类型的实例,该参数必须为一个整数。引发一个 审计事件 ctypes.cdata,附带参数 address

    from_param(obj)此方法会将 obj 适配为一个 ctypes 类型。 它调用时会在当该类型存在于外部函数的 argtypes 元组时传入外部函数调用所使用的实际对象;它必须返回一个可被用作函数调用参数的对象。所有 ctypes 数据类型都带有这个类方法的默认实现,它通常会返回 obj,如果该对象是此类型的实例的话。 某些类型也能接受其他对象。

    in_dll(library, name)此方法返回一个由共享库导出的 ctypes 类型。 name 为导出数据的符号名称,library 为所加载的共享库。

    ctypes 数据类型的通用实例变量:

    _b_base_有时 ctypes 数据实例并不拥有它们所包含的内存块,它们只是共享了某个基对象的部分内存块。 _b_base_ 只读成员是拥有内存块的根 ctypes 对象。

    _b_needsfree_这个只读变量在 ctypes 数据实例自身已分配了内存块时为真值,否则为假值。

    _objects这个成员或者为 None,或者为一个包含需要保持存活以使内存块的内存保持有效的 Python 对象的字典。 这个对象只是出于调试目的而对外公开;绝对不要修改此字典的内容。

基础数据类型

  • class ctypes.``_SimpleCData

    这个非公有类是所有基本 ctypes 数据类型的基类。 它在这里被提及是因为它包含基本 ctypes 数据类型共有的属性。 _SimpleCData_CData 的子类,因此继承了其方法和属性。 非指针及不包含指针的 ctypes 数据类型现在将可以被封存。实例拥有一个属性:value这个属性包含实例的实际值。 对于整数和指针类型,它是一个整数,对于字符类型,它是一个单字符字符串对象或字符串,对于字符指针类型,它是一个 Python 字节串对象或字符串。当从 ctypes 实例提取 value 属性时,通常每次会返回一个新的对象。 ctypes没有 实现原始对象返回,它总是会构造一个新的对象。 所有其他 ctypes 对象实例也同样如此。

基本数据类型当作为外部函数调用结果被返回或者作为结构字段成员或数组项被提取时,会透明地转换为原生 Python 类型。 换句话说,如果某个外部函数具有 c_char_prestype,你将总是得到一个 Python 字节串对象,而 不是 一个 c_char_p 实例。

基本数据类型的子类并 没有 继续此行为。 因此,如果一个外部函数的 restypec_void_p 的一个子类,你将从函数调用得到一个该子类的实例。 当然,你可以通过访问 value 属性来获取指针的值。

这些是基本 ctypes 数据类型:

  • class ctypes.c_byte

    代表 C signed char 数据类型,并将值解读为一个小整数。 该构造器接受一个可选的整数初始化器;不会执行溢出检查。

  • class ctypes.c_char

    代表 C char 数据类型,并将值解读为单个字符。 该构造器接受一个可选的字符串初始化器,字符串的长度必须恰好为一个字符。

  • class ctypes.c_char_p

    当指向一个以零为结束符的字符串时代表 C char * 数据类型。 对于通用字符指针来说也可能指向二进制数据,必须要使用 POINTER(c_char)。 该构造器接受一个整数地址,或者一个字节串对象。

  • class ctypes.c_double

    代表 C double 数据类型。 该构造器接受一个可选的浮点数初始化器。

  • class ctypes.c_longdouble

    代表 C long double 数据类型。 该构造器接受一个可选的浮点数初始化器。 在 sizeof(long double) == sizeof(double) 的平台上它是 c_double 的一个别名。

  • class ctypes.c_float

    代表 C float 数据类型。 该构造器接受一个可选的浮点数初始化器。

  • class ctypes.c_int

    代表 C signed int 数据类型。 该构造器接受一个可选的整数初始化器;不会执行溢出检查。 在 sizeof(int) == sizeof(long) 的平台上它是 c_long 的一个别名。

  • class ctypes.c_int8

    代表 C 8 位 signed int 数据类型。 通常是 c_byte 的一个别名。

  • class ctypes.c_int16

    代表 C 16 位 signed int 数据类型。 通常是 c_short 的一个别名。

  • class ctypes.c_int32

    代表 C 32 位 signed int 数据类型。 通常是 c_int 的一个别名。

  • class ctypes.c_int64

    代表 C 64 位 signed int 数据类型。 通常是 c_longlong 的一个别名。

  • class ctypes.c_long

    代表 C signed long 数据类型。 该构造器接受一个可选的整数初始化器;不会执行溢出检查。

  • class ctypes.c_longlong

    代表 C signed long long 数据类型。 该构造器接受一个可选的整数初始化器;不会执行溢出检查。

  • class ctypes.c_short

    代表 C signed short 数据类型。 该构造器接受一个可选的整数初始化器;不会执行溢出检查。

  • class ctypes.c_size_t

    代表 C size_t 数据类型。

  • class ctypes.c_ssize_t

    代表 C ssize_t 数据类型。3.2 新版功能.

  • class ctypes.c_ubyte

    代表 C unsigned char 数据类型,它将值解读为一个小整数。 该构造器接受一个可选的整数初始化器;不会执行溢出检查。

  • class ctypes.c_uint

    代表 C unsigned int 数据类型。 该构造器接受一个可选的整数初始化器;不会执行溢出检查。 在 sizeof(int) == sizeof(long) 的平台上它是 c_ulong 的一个别名。

  • class ctypes.c_uint8

    代表 C 8 位 unsigned int 数据类型。 通常是 c_ubyte 的一个别名。

  • class ctypes.c_uint16

    代表 C 16 位 unsigned int 数据类型。 通常是 c_ushort 的一个别名。

  • class ctypes.c_uint32

    代表 C 32 位 unsigned int 数据类型。 通常是 c_uint 的一个别名。

  • class ctypes.c_uint64

    代表 C 64 位 unsigned int 数据类型。 通常是 c_ulonglong 的一个别名。

  • class ctypes.c_ulong

    代表 C unsigned long 数据类型。 该构造器接受一个可选的整数初始化器;不会执行溢出检查。

  • class ctypes.c_ulonglong

    代表 C unsigned long long 数据类型。 该构造器接受一个可选的整数初始化器;不会执行溢出检查。

  • class ctypes.c_ushort

    代表 C unsigned short 数据类型。 该构造器接受一个可选的整数初始化器;不会执行溢出检查。

  • class ctypes.c_void_p

    代表 C void * 类型。 该值被表示为整数形式。 该构造器接受一个可选的整数初始化器。

  • class ctypes.c_wchar

    代表 C wchar_t 数据类型,并将值解读为一单个字符的 unicode 字符串。 该构造器接受一个可选的字符串初始化器,字符串的长度必须恰好为一个字符。

  • class ctypes.c_wchar_p

    代表 C wchar_t * 数据类型,它必须为指向以零为结束符的宽字符串的指针。 该构造器接受一个整数地址或者一个字符串。

  • class ctypes.c_bool

    代表 C bool 数据类型 (更准确地说是 C99 _Bool)。 它的值可以为 TrueFalse,并且该构造器接受任何具有逻辑值的对象。

  • class ctypes.HRESULT

    Windows 专属:代表一个 HRESULT 值,它包含某个函数或方法调用的成功或错误信息。

  • class ctypes.py_object

    代表 C PyObject * 数据类型。 不带参数地调用此构造器将创建一个 NULL PyObject * 指针。

ctypes.wintypes 模块提供了其他许多 Windows 专属的数据类型,例如 HWND, WPARAMDWORD。 还定义了一些有用的结构体例如 MSGRECT

结构化数据类型

class ctypes.Structure(*args, **kw)

_fields_
一个定义结构体字段的序列。 其中的条目必须为 2 元组或 3 元组。 元组的第一项是字段名称,第二项指明字段类型;它可以是任何 ctypes 数据类型。

对于整数类型字段例如 c_int,可以给定第三个可选项。 它必须是一个定义字段比特位宽度的小正整数。

字段名称在一个结构体或联合中必须唯一。 不会检查这个唯一性,但当名称出现重复时将只有一个字段可被访问。

可以在定义 Structure 子类的类语句 之后 再定义 _fields_ 类变量,这将允许创建直接或间接引用其自身的数据类型:

class List(Structure):
    pass
List._fields_ = [("pnext", POINTER(List)),
                 ...
                ]

数组与指针

  • class ctypes.Array(*args)

    数组的抽象基类。

    创建实际数组类型的推荐方式是通过将任意 ctypes 类型与一个正整数相乘。 作为替代方式,你也可以子类化这个类型并定义 _length__type_ 类变量。 数组元素可使用标准的抽取和切片方式来读写;对于切片读取,结果对象本身 并非 一个 Array

    _length_一个指明数组中元素数量的正整数。 超出范围的抽取会导致 IndexError。 该值将由 len() 返回。

    _type_指明数组中每个元素的类型。Array 子类构造器可接受位置参数,用来按顺序初始化元素。

  • class ctypes._Pointer

    私有对象,指针的抽象基类。

    实际的指针类型是通过调用 POINTER() 并附带其将指向的类型来创建的;这会由 pointer() 自动完成。

    如果一个指针指向的是数组,则其元素可使用标准的抽取和切片方式来读写。 指针对象没有长度,因此 len() 将引发 TypeError。 抽取负值将会从指针 之前 的内存中读取(与 C 一样),并且超出范围的抽取将可能因非法访问而导致崩溃(视你的运气而定)。

    _type_指明所指向的类型。

    contents返回指针所指向的对象。 对此属性赋值会使指针改为指向所赋值的对象。
    [参考1]https://docs.python.org/zh-cn/3.9/library/ctypes.html#pointers

[参考2] https://zhuanlan.zhihu.com/p/36772947

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

推荐阅读更多精彩内容