1. 程式人生 > >Python Cookbook(第3版)中文版:15.13 傳遞NULL結尾的字符串給C函數庫

Python Cookbook(第3版)中文版:15.13 傳遞NULL結尾的字符串給C函數庫

www. parse 創建 link rom 兩種 學習 類型 encode

15.13 傳遞NULL結尾的字符串給C函數庫?

問題?

你要寫一個擴展模塊,需要傳遞一個NULL結尾的字符串給C函數庫。
不過,你不是很確定怎樣使用Python的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字節。
因此,如果這個很重要的話,那你需要自己去做檢查了。

討論?

如果可能的話,你應該避免去寫一些依賴於NULL結尾的字符串,因為Python並沒有這個需要。
最好結合使用一個指針和長度值來處理字符串。
不過,有時候你必須去處理C語言遺留代碼時就沒得選擇了。

盡管很容易使用,但是很容易忽視的一個問題是在 PyArg_ParseTuple()
中使用“s”格式化碼會有內存損耗。
但你需要使用這種轉換的時候,一個UTF-8字符串被創建並永久附加在原始字符串對象上面。
如果原始字符串包含非ASCII字符的話,就會導致字符串的尺寸增到一直到被垃圾回收。例如:

>>> import sys
>>> s = ‘Spicy Jalape\u00f1o‘
>>> sys.getsizeof(s)
87
>>> print_chars(s)     # Passing string
53 70 69 63 79 20 4a 61 6c 61 70 65 c3 b1 6f
>>> sys.getsizeof(s)   # Notice increased size
103
>>>

如果你在乎這個內存的損耗,你最好重寫你的C擴展代碼,讓它使用 PyUnicode_AsUTF8String() 函數。如下:

static PyObject *py_print_chars(PyObject *self, PyObject *args) {
  PyObject *o, *bytes;
  char *s;

  if (!PyArg_ParseTuple(args, "U", &o)) {
    return NULL;
  }
  bytes = PyUnicode_AsUTF8String(o);
  s = PyBytes_AsString(bytes);
  print_chars(s);
  Py_DECREF(bytes);
  Py_RETURN_NONE;
}

通過這個修改,一個UTF-8編碼的字符串根據需要被創建,然後在使用過後被丟棄。下面是修訂後的效果:

>>> import sys
>>> s = ‘Spicy Jalape\u00f1o‘
>>> sys.getsizeof(s)
87
>>> print_chars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 c3 b1 6f
>>> sys.getsizeof(s)
87
>>>

如果你試著傳遞NULL結尾字符串給ctypes包裝過的函數,
要註意的是ctypes只能允許傳遞字節,並且它不會檢查中間嵌入的NULL字節。例如:

>>> import ctypes
>>> lib = ctypes.cdll.LoadLibrary("./libsample.so")
>>> print_chars = lib.print_chars
>>> print_chars.argtypes = (ctypes.c_char_p,)
>>> print_chars(b‘Hello World‘)
48 65 6c 6c 6f 20 57 6f 72 6c 64
>>> print_chars(b‘Hello\x00World‘)
48 65 6c 6c 6f
>>> print_chars(‘Hello World‘)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ctypes.ArgumentError: argument 1: <class ‘TypeError‘>: wrong type
>>>

如果你想傳遞字符串而不是字節,你需要先執行手動的UTF-8編碼。例如:

>>> print_chars(‘Hello World‘.encode(‘utf-8‘))
48 65 6c 6c 6f 20 57 6f 72 6c 64
>>>

對於其他擴展工具(比如Swig、Cython),
在你使用它們傳遞字符串給C代碼時要先好好學習相應的東西了。

艾伯特(http://www.aibbt.com/)國內第一家人工智能門戶

Python Cookbook(第3版)中文版:15.13 傳遞NULL結尾的字符串給C函數庫