暂无个人介绍
2020年04月
faulthandler 模块能被用来帮你解决这个问题。 在你的程序中引入下列代码:
import faulthandler
faulthandler.enable()
另外还可以像下面这样使用 -Xfaulthandler 来运行Python:
bash % python3 -Xfaulthandler program.py
最后,你可以设置 PYTHONFAULTHANDLER 环境变量。 开启faulthandler后,在C扩展中的致命错误会导致一个Python错误堆栈被打印出来。例如:
Fatal Python error: Segmentation fault
Current thread 0x00007fff71106cc0:
File "example.py", line 6 in foo
File "example.py", line 10 in bar
File "example.py", line 14 in spam
File "example.py", line 19 in <module>
Segmentation fault
尽管这个并不能告诉你C代码中哪里出错了,但是至少能告诉你Python里面哪里有错。
下面是一个C扩展函数例子,演示了怎样处理可迭代对象中的元素:
static PyObject *py_consume_iterable(PyObject *self, PyObject *args) {
PyObject *obj;
PyObject *iter;
PyObject *item;
if (!PyArg_ParseTuple(args, "O", &obj)) {
return NULL;
}
if ((iter = PyObject_GetIter(obj)) == NULL) {
return NULL;
}
while ((item = PyIter_Next(iter)) != NULL) {
/* Use item */
...
Py_DECREF(item);
}
Py_DECREF(iter);
return Py_BuildValue("");
}
要读取一个类文件对象的数据,你需要重复调用 read() 方法,然后正确的解码获得的数据。
下面是一个C扩展函数例子,仅仅只是读取一个类文件对象中的所有数据并将其输出到标准输出:
#define CHUNK_SIZE 8192
/* Consume a "file-like" object and write bytes to stdout */
static PyObject *py_consume_file(PyObject *self, PyObject *args) {
PyObject *obj;
PyObject *read_meth;
PyObject *result = NULL;
PyObject *read_args;
if (!PyArg_ParseTuple(args,"O", &obj)) {
return NULL;
}
/* Get the read method of the passed object */
if ((read_meth = PyObject_GetAttrString(obj, "read")) == NULL) {
return NULL;
}
/* Build the argument list to read() */
read_args = Py_BuildValue("(i)", CHUNK_SIZE);
while (1) {
PyObject *data;
PyObject *enc_data;
char *buf;
Py_ssize_t len;
/* Call read() */
if ((data = PyObject_Call(read_meth, read_args, NULL)) == NULL) {
goto final;
}
/* Check for EOF */
if (PySequence_Length(data) == 0) {
Py_DECREF(data);
break;
}
/* Encode Unicode as Bytes for C */
if ((enc_data=PyUnicode_AsEncodedString(data,"utf-8","strict"))==NULL) {
Py_DECREF(data);
goto final;
}
/* Extract underlying buffer data */
PyBytes_AsStringAndSize(enc_data, &buf, &len);
/* Write to stdout (replace with something more useful) */
write(1, buf, len);
/* Cleanup */
Py_DECREF(enc_data);
Py_DECREF(data);
}
result = Py_BuildValue("");
final:
/* Cleanup */
Py_DECREF(read_meth);
Py_DECREF(read_args);
return result;
}
要测试这个代码,先构造一个类文件对象比如一个StringIO实例,然后传递进来:
>>> import io
>>> f = io.StringIO('Hello\nWorld\n')
>>> import sample
>>> sample.consume_file(f)
Hello
World
>>>
要将一个文件转换为一个整型的文件描述符,使用 PyFile_FromFd() ,如下:
PyObject *fobj; /* File object (already obtained somehow) */
int fd = PyObject_AsFileDescriptor(fobj);
if (fd < 0) {
return NULL;
}
结果文件描述符是通过调用 fobj 中的 fileno() 方法获得的。 因此,任何以这种方式暴露给一个描述器的对象都适用(比如文件、套接字等)。 一旦你有了这个描述器,它就能被传递给多个低级的可处理文件的C函数。
如果你需要转换一个整型文件描述符为一个Python对象,适用下面的 PyFile_FromFd() :
int fd; /* Existing file descriptor (already open) */
PyObject *fobj = PyFile_FromFd(fd, "filename","r",-1,NULL,NULL,NULL,1);
PyFile_FromFd() 的参数对应内置的 open() 函数。 NULL表示编码、错误和换行参数使用默认值。
写一个接受一个文件名为参数的扩展函数,如下这样:
static PyObject *py_get_filename(PyObject *self, PyObject *args) {
PyObject *bytes;
char *filename;
Py_ssize_t len;
if (!PyArg_ParseTuple(args,"O&", PyUnicode_FSConverter, &bytes)) {
return NULL;
}
PyBytes_AsStringAndSize(bytes, &filename, &len);
/* Use filename */
...
/* Cleanup and return */
Py_DECREF(bytes)
Py_RETURN_NONE;
}
如果你已经有了一个 PyObject * ,希望将其转换成一个文件名,可以像下面这样做:
PyObject *obj; /* Object with the filename */
PyObject *bytes;
char *filename;
Py_ssize_t len;
bytes = PyUnicode_EncodeFSDefault(obj);
PyBytes_AsStringAndSize(bytes, &filename, &len);
/* Use filename */
...
/* Cleanup */
Py_DECREF(bytes);
If you need to return a filename back to Python, use the following code:
/* Turn a filename into a Python object */
char *filename; /* Already set */
int filename_len; /* Already set */
PyObject *obj = PyUnicode_DecodeFSDefaultAndSize(filename, filename_len);
面是一些C的数据和一个函数来演示这个问题:
/* Some dubious string data (malformed UTF-8) */
const char *sdata = "Spicy Jalape\xc3\xb1o\xae";
int slen = 16;
/* Output character data */
void print_chars(char *s, int len) {
int n = 0;
while (n < len) {
printf("%2x ", (unsigned char) s[n]);
n++;
}
printf("\n");
}
在这个代码中,字符串 sdata 包含了UTF-8和不合格数据。 不过,如果用户在C中调用 print_chars(sdata, slen) ,它缺能正常工作。 现在假设你想将 sdata 的内容转换为一个Python字符串。 进一步假设你在后面还想通过一个扩展将那个字符串传个 print_chars() 函数。 下面是一种用来保护原始数据的方法,就算它编码有问题。
/* Return the C string back to Python */
static PyObject *py_retstr(PyObject *self, PyObject *args) {
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
return PyUnicode_Decode(sdata, slen, "utf-8", "surrogateescape");
}
/* Wrapper for the print_chars() function */
static PyObject *py_print_chars(PyObject *self, PyObject *args) {
PyObject *obj, *bytes;
char *s = 0;
Py_ssize_t len;
if (!PyArg_ParseTuple(args, "U", &obj)) {
return NULL;
}
if ((bytes = PyUnicode_AsEncodedString(obj,"utf-8","surrogateescape"))
== NULL) {
return NULL;
}
PyBytes_AsStringAndSize(bytes, &s, &len);
print_chars(s, len);
Py_DECREF(bytes);
Py_RETURN_NONE;
}
如果你在Python中尝试这些函数,下面是运行效果:
>>> s = retstr()
>>> s
'Spicy Jalapeño\udcae'
>>> print_chars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 c3 b1 6f ae
>>>
仔细观察结果你会发现,不合格字符串被编码到一个Python字符串中,并且并没有产生错误, 并且当它被回传给C的时候,被转换为和之前原始C字符串一样的字节。
C字符串使用一对 char * 和 int 来表示, 你需要决定字符串到底是用一个原始字节字符串还是一个Unicode字符串来表示。 字节对象可以像下面这样使用 Py_BuildValue() 来构建:
char *s; /* Pointer to C string data */
int len; /* Length of data */
/* Make a bytes object */
PyObject *obj = Py_BuildValue("y#", s, len);
如果你要创建一个Unicode字符串,并且你知道 s 指向了UTF-8编码的数据,可以使用下面的方式:
PyObject *obj = Py_BuildValue("s#", s, len);
如果 s 使用其他编码方式,那么可以像下面使用 PyUnicode_Decode() 来构建一个字符串:
PyObject *obj = PyUnicode_Decode(s, len, "encoding", "errors");
/* Examples /*
obj = PyUnicode_Decode(s, len, "latin-1", "strict");
obj = PyUnicode_Decode(s, len, "ascii", "ignore");
如果你恰好有一个用 wchar_t *, len 对表示的宽字符串, 有几种选择性。首先你可以使用 Py_BuildValue() :
wchar_t *w; /* Wide character string */
int len; /* Length */
PyObject *obj = Py_BuildValue("u#", w, len);
另外,你还可以使用 PyUnicode_FromWideChar() :
PyObject *obj = PyUnicode_FromWideChar(w, len);
对于宽字符串,并没有对字符数据进行解析——它被假定是原始Unicode编码指针,可以被直接转换成Python。
这里我们需要考虑很多的问题,但是最主要的问题是现存的C函数库并不理解Python的原生Unicode表示。 因此,你的挑战是将Python字符串转换为一个能被C理解的形式。
为了演示的目的,下面有两个C函数,用来操作字符串数据并输出它来调试和测试。 一个使用形式为 char *, int 形式的字节, 而另一个使用形式为 wchar_t *, int 的宽字符形式:
void print_chars(char *s, int len) {
int n = 0;
while (n < len) {
printf("%2x ", (unsigned char) s[n]);
n++;
}
printf("\n");
}
void print_wchars(wchar_t *s, int len) {
int n = 0;
while (n < len) {
printf("%x ", s[n]);
n++;
}
printf("\n");
}
对于面向字节的函数 print_chars() ,你需要将Python字符串转换为一个合适的编码比如UTF-8. 下面是一个这样的扩展函数例子:
static PyObject *py_print_chars(PyObject *self, PyObject *args) {
char *s;
Py_ssize_t len;
if (!PyArg_ParseTuple(args, "s#", &s, &len)) {
return NULL;
}
print_chars(s, len);
Py_RETURN_NONE;
}
对于那些需要处理机器本地 wchar_t 类型的库函数,你可以像下面这样编写扩展代码:
static PyObject *py_print_wchars(PyObject *self, PyObject *args) {
wchar_t *s;
Py_ssize_t len;
if (!PyArg_ParseTuple(args, "u#", &s, &len)) {
return NULL;
}
print_wchars(s,len);
Py_RETURN_NONE;
}
下面是一个交互会话来演示这个函数是如何工作的:
>>> s = 'Spicy Jalape\u00f1o'
>>> print_chars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 c3 b1 6f
>>> print_wchars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 f1 6f
>>>
仔细观察这个面向字节的函数 print_chars() 是怎样接受UTF-8编码数据的, 以及 print_wchars() 是怎样接受Unicode编码值的
许多C函数库包含一些操作NULL结尾的字符串,被声明类型为 char * . 考虑如下的C函数,我们用来做演示和测试用的:
void print_chars(char *s) {
while (*s) {
printf("%2x ", (unsigned char) *s);
s++;
}
printf("\n");
}
此函数会打印被传进来字符串的每个字符的十六进制表示,这样的话可以很容易的进行调试了。例如:
print_chars("Hello"); // Outputs: 48 65 6c 6c 6f
对于在Python中调用这样的C函数,你有几种选择。 首先,你可以通过调用 PyArg_ParseTuple() 并指定”y“转换码来限制它只能操作字节,如下:
static PyObject *py_print_chars(PyObject *self, PyObject *args) {
char *s;
if (!PyArg_ParseTuple(args, "y", &s)) {
return NULL;
}
print_chars(s);
Py_RETURN_NONE;
}
结果函数的使用方法如下。仔细观察嵌入了NULL字节的字符串以及Unicode支持是怎样被拒绝的:
>>> print_chars(b'Hello World')
48 65 6c 6c 6f 20 57 6f 72 6c 64
>>> print_chars(b'Hello\x00World')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: must be bytes without null bytes, not bytes
>>> print_chars('Hello World')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' does not support the buffer interface
>>>
如果你想传递Unicode字符串,在 PyArg_ParseTuple() 中使用”s“格式码,如下:
static PyObject *py_print_chars(PyObject *self, PyObject *args) {
char *s;
if (!PyArg_ParseTuple(args, "s", &s)) {
return NULL;
}
print_chars(s);
Py_RETURN_NONE;
}
当被使用的时候,它会自动将所有字符串转换为以NULL结尾的UTF-8编码。例如:
>>> print_chars('Hello World')
48 65 6c 6c 6f 20 57 6f 72 6c 64
>>> print_chars('Spicy Jalape\u00f1o') # Note: UTF-8 encoding
53 70 69 63 79 20 4a 61 6c 61 70 65 c3 b1 6f
>>> print_chars('Hello\x00World')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: must be str without null characters, not str
>>> print_chars(b'Hello World')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: must be str, not bytes
>>>
如果因为某些原因,你要直接使用 PyObject * 而不能使用 PyArg_ParseTuple() , 下面的例子向你展示了怎样从字节和字符串对象中检查和提取一个合适的 char * 引用:
/* Some Python Object (obtained somehow) */
PyObject *obj;
/* Conversion from bytes */
{
char *s;
s = PyBytes_AsString(o);
if (!s) {
return NULL; /* TypeError already raised */
}
print_chars(s);
}
/* Conversion to UTF-8 bytes from a string */
{
PyObject *bytes;
char *s;
if (!PyUnicode_Check(obj)) {
PyErr_SetString(PyExc_TypeError, "Expected string");
return NULL;
}
bytes = PyUnicode_AsUTF8String(obj);
s = PyBytes_AsString(bytes);
print_chars(s);
Py_DECREF(bytes);
}
前面两种转换都可以确保是NULL结尾的数据, 但是它们并不检查字符串中间是否嵌入了NULL字节。 因此,如果这个很重要的话,那你需要自己去做检查了。
讨论
ctypes 模块可被用来创建包装任意内存地址的Python可调用对象。 下面的例子演示了怎样获取C函数的原始、底层地址,以及如何将其转换为一个可调用对象:
>>> import ctypes
>>> lib = ctypes.cdll.LoadLibrary(None)
>>> # Get the address of sin() from the C math library
>>> addr = ctypes.cast(lib.sin, ctypes.c_void_p).value
>>> addr
140735505915760
>>> # Turn the address into a callable function
>>> functype = ctypes.CFUNCTYPE(ctypes.c_double, ctypes.c_double)
>>> func = functype(addr)
>>> func
<CFunctionType object at 0x1006816d0>
>>> # Call the resulting function
>>> func(2)
0.9092974268256817
>>> func(0)
0.0
>>>