Weibw's World Weibw's World
首页
  • HTML
  • Python

    • Python基础知识
    • Python CookBook第三版
    • Flask
  • MySQL

    • MySQL基础知识
    • MySQL调优
    • MySQL面试题
算法
  • FineReport
  • Kettle
  • Git
  • 微信公众号文章
  • 优秀博客文章
  • 其他
收藏夹
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Weibw

一个没有梦想的咸鱼
首页
  • HTML
  • Python

    • Python基础知识
    • Python CookBook第三版
    • Flask
  • MySQL

    • MySQL基础知识
    • MySQL调优
    • MySQL面试题
算法
  • FineReport
  • Kettle
  • Git
  • 微信公众号文章
  • 优秀博客文章
  • 其他
收藏夹
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 《Flask》

  • 《Python Cookbook》第三版

    • 第一章:数据结构与算法

    • 第二章:字符串和文本

    • 第三章:数字日期和时间

    • 第四章:迭代器与生成器

    • 第五章:文件与IO

    • 第六章:数据编码和处理

    • 第七章:函数

    • 第八章:类与对象

    • 第九章:元编程

    • 第十章:模块与包

    • 第十一章:网络与Web编程

    • 第十二章:并发编程

    • 第十三章:脚本编程与系统管理

    • 第十四章:测试、调试和异常

    • 第十五章:C语言扩展

      • 使用ctypes访问C代码
      • 简单的C扩展模块
      • 编写扩展函数操作数组
      • 在C扩展模块中操作隐形指针
      • 从扩展模块中定义和导出C的API
      • 从C语言中调用Python代码
      • 从C扩展中释放全局锁
      • C和Python中的线程混用
      • 用SWIG包装C代码
      • 用Cython包装C代码
      • 用Cython写高性能的数组操作
      • 将函数指针转换为可调用对象
      • 传递NULL结尾的字符串给C函数库
      • 传递Unicode字符串给C函数库
        • 问题
        • 解决方案
        • 讨论
      • C字符串转换为Python字符串
      • 不确定编码格式的C字符串
      • 传递文件名给C扩展
      • 传递已打开的文件给C扩展
      • 从C语言中读取类文件对象
      • 处理C语言中的可迭代对象
      • 诊断分段错误
  • Python基础

  • Python
  • 《Python Cookbook》第三版
  • 第十五章:C语言扩展
weibw
2022-01-18

传递Unicode字符串给C函数库

# 问题

你要写一个扩展模块,需要将一个Python字符串传递给C的某个库函数,但是这个函数不知道该怎么处理Unicode。

# 解决方案

这里我们需要考虑很多的问题,但是最主要的问题是现存的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");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

对于面向字节的函数 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;
}
1
2
3
4
5
6
7
8
9
10

对于那些需要处理机器本地 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;
}
1
2
3
4
5
6
7
8
9
10

下面是一个交互会话来演示这个函数是如何工作的:

>>> 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
>>>
1
2
3
4
5
6

仔细观察这个面向字节的函数 print_chars() 是怎样接受UTF-8编码数据的, 以及 print_wchars() 是怎样接受Unicode编码值的

# 讨论

在继续本节之前,你应该首先学习你访问的C函数库的特征。 对于很多C函数库,通常传递字节而不是字符串会比较好些。要这样做,请使用如下的转换代码:

static PyObject *py_print_chars(PyObject *self, PyObject *args) {
  char *s;
  Py_ssize_t  len;

  /* accepts bytes, bytearray, or other byte-like object */
  if (!PyArg_ParseTuple(args, "y#", &s, &len)) {
    return NULL;
  }
  print_chars(s, len);
  Py_RETURN_NONE;
}
1
2
3
4
5
6
7
8
9
10
11

如果你仍然还是想要传递字符串, 你需要知道Python 3可使用一个合适的字符串表示, 它并不直接映射到使用标准类型 char * 或 wchar_t * (更多细节参考PEP 393)的C函数库。 因此,要在C中表示这个字符串数据,一些转换还是必须要的。 在 PyArg_ParseTuple() 中使用”s#” 和”u#”格式化码可以安全的执行这样的转换。

不过这种转换有个缺点就是它可能会导致原始字符串对象的尺寸增大。 一旦转换过后,会有一个转换数据的复制附加到原始字符串对象上面,之后可以被重用。 你可以观察下这种效果:

>>> 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)
103
>>> print_wchars(s)
53 70 69 63 79 20 4a 61 6c 61 70 65 f1 6f
>>> sys.getsizeof(s)
163
>>>
1
2
3
4
5
6
7
8
9
10
11
12
13

对于少量的字符串对象,可能没什么影响, 但是如果你需要在扩展中处理大量的文本,你可能想避免这个损耗了。 下面是一个修订版本可以避免这种内存损耗:

static PyObject *py_print_chars(PyObject *self, PyObject *args) {
  PyObject *obj, *bytes;
  char *s;
  Py_ssize_t   len;

  if (!PyArg_ParseTuple(args, "U", &obj)) {
    return NULL;
  }
  bytes = PyUnicode_AsUTF8String(obj);
  PyBytes_AsStringAndSize(bytes, &s, &len);
  print_chars(s, len);
  Py_DECREF(bytes);
  Py_RETURN_NONE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

而对 wchar_t 的处理时想要避免内存损耗就更加难办了。 在内部,Python使用最高效的表示来存储字符串。 例如,只包含ASCII的字符串被存储为字节数组, 而包含范围从U+0000到U+FFFF的字符的字符串使用双字节表示。 由于对于数据的表示形式不是单一的,你不能将内部数组转换为 wchar_t * 然后期望它能正确的工作。 你应该创建一个 wchar_t 数组并向其中复制文本。 PyArg_ParseTuple() 的”u#”格式码可以帮助你高效的完成它(它将复制结果附加到字符串对象上)。

如果你想避免长时间内存损耗,你唯一的选择就是复制Unicode数据懂啊一个临时的数组, 将它传递给C函数,然后回收这个数组的内存。下面是一个可能的实现:

static PyObject *py_print_wchars(PyObject *self, PyObject *args) {
  PyObject *obj;
  wchar_t *s;
  Py_ssize_t len;

  if (!PyArg_ParseTuple(args, "U", &obj)) {
    return NULL;
  }
  if ((s = PyUnicode_AsWideCharString(obj, &len)) == NULL) {
    return NULL;
  }
  print_wchars(s, len);
  PyMem_Free(s);
  Py_RETURN_NONE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在这个实现中,PyUnicode_AsWideCharString() 创建一个临时的wchar_t缓冲并复制数据进去。 这个缓冲被传递给C然后被释放掉。 但是我写这本书的时候,这里可能有个bug,后面的Python问题页有介绍。

如果你知道C函数库需要的字节编码并不是UTF-8, 你可以强制Python使用扩展码来执行正确的转换,就像下面这样:

static PyObject *py_print_chars(PyObject *self, PyObject *args) {
  char *s = 0;
  int   len;
  if (!PyArg_ParseTuple(args, "es#", "encoding-name", &s, &len)) {
    return NULL;
  }
  print_chars(s, len);
  PyMem_Free(s);
  Py_RETURN_NONE;
}
1
2
3
4
5
6
7
8
9
10

最后,如果你想直接处理Unicode字符串,下面的是例子,演示了底层操作访问:

static PyObject *py_print_wchars(PyObject *self, PyObject *args) {
  PyObject *obj;
  int n, len;
  int kind;
  void *data;

  if (!PyArg_ParseTuple(args, "U", &obj)) {
    return NULL;
  }
  if (PyUnicode_READY(obj) < 0) {
    return NULL;
  }

  len = PyUnicode_GET_LENGTH(obj);
  kind = PyUnicode_KIND(obj);
  data = PyUnicode_DATA(obj);

  for (n = 0; n < len; n++) {
    Py_UCS4 ch = PyUnicode_READ(kind, data, n);
    printf("%x ", ch);
  }
  printf("\n");
  Py_RETURN_NONE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在这个代码中,PyUnicode_KIND() 和 PyUnicode_DATA() 这两个宏和Unicode的可变宽度存储有关,这个在PEP 393中有描述。 kind 变量编码底层存储(8位、16位或32位)以及指向缓存的数据指针相关的信息。 在实际情况中,你并不需要知道任何跟这些值有关的东西, 只需要在提取字符的时候将它们传给 PyUnicode_READ() 宏。

还有最后几句:当从Python传递Unicode字符串给C的时候,你应该尽量简单点。 如果有UTF-8和宽字符两种选择,请选择UTF-8. 对UTF-8的支持更加普遍一些,也不容易犯错,解释器也能支持的更好些。 最后,确保你仔细阅读了 关于处理Unicode的相关文档 (opens new window)

编辑 (opens new window)
上次更新: 2023/10/13, 17:39:25
传递NULL结尾的字符串给C函数库
C字符串转换为Python字符串

← 传递NULL结尾的字符串给C函数库 C字符串转换为Python字符串→

最近更新
01
牛客网非技术快速入门SQL练习题
03-08
02
其他日常SQL题
03-07
03
用户与权限管理
03-05
更多文章>
Theme by Vdoing | Copyright © 2021-2023 | Weibw | 辽ICP备18015889号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式