首页 > 解决方案 > 存储临时 Python 引用的不安全 C 派生

问题描述

考虑以下人为的 Cython 函数来加入字符串列表:

# cython: language_level=3
cpdef test_join():
    """ ["abc", "def", "ghi"] -> "abcdefghi" """
    cdef:
        list lines = ["abc", "def", "ghi"]
        char* out = ""
        char* line = ""
        int i
    for i in range(len(lines)):
        line = lines[i]
        out = out + line
    return out

它将无法编译,并出现以下错误:

存储临时 Python 引用的不安全 C 派生

我假设这与line类型char*和不断重新分配有关。我已经看到了一个类似问题的答案,但无法为这个基本示例修改该答案。(而且它还涉及大量我不熟悉的 C-API。)

如何修改上述函数以按预期编译和返回?


更广泛地说,我想更好地理解这个错误。Commit 37e4a20有一点解释:

char*一个临时的 Python 字符串对象中获取一个...只有当这样的指针被分配给一个变量并且因此会超过字符串本身的生命周期时,才会引发编译时错误。


更新:为了进一步简化事情,看起来这是导致问题的分配:

cpdef int will_succeed():
    cdef char* a = b"hello"
    cdef char* b = b" world"
    print(a + b)  # no new assignment
    return 1

cpdef will_fail():
    cdef char* a = b"hello"
    cdef char* b = b" world"
    a = a + b  # won't compile
    return a

我怀疑可能有一种更合适的方法可以使用来自string.pxd/的东西来做到这一点string.h,但我在 C 内存管理和效率方面相当薄弱:

from libc.string cimport strcat, strcpy

cpdef use_strcat():
    cdef char out[1024]
    strcpy(out, b"")

    cdef char* a = b"hello"
    cdef char* b = b" world"

    strcat(out, a)
    strcat(out, b)
    return out

标签: cython

解决方案


我认为问题在于

out = out + line

Cython 没有+为 C 字符串定义运算符。相反,它将它们转换为 Python 字符串并将它们连接起来:

tmp1 = str(out)
tmp2 = str(line)
tmp3 = tmp1 + tmp2
out = get_c_string_from(tmp3)

out因此,一旦tmp3被销毁(即立即),它就会成为无效指针。


我会避免使用strcat,因为它对于重复使用不是很有效。而是跟踪当前的字符串长度并自己复制数据。鉴于您的长度未知,您可能希望分配字符串malloc(在这种情况下,您负责释放它)

from libc.stdlib cimport free, malloc, realloc
from libc.string cimport memcpy

from cython import Py_ssize_t

cdef char         *line
cdef Py_ssize_t   i
cdef Py_ssize_t   length = 0
cdef Py_ssize_t   incrlength
cdef char         *out = <char *>malloc(1)  # Reallocate as needed

try:
    out[0] = b'\x00' # keep C-strings null-terminated
    for i in range(len(lines)):
        line = lines[i]
        incrlength = len(line)
        out = <char *>realloc(out, length + incrlength + 1)
        memcpy(out + length, line, incrlength)
        length += incrlength
        out[length] = '\x00'  # keep C-strings null-terminated
    return out  # autoconversion back to a Python string

finally:
   free(out)

这是我认为您应该做的粗略概述,并且没有经过真正的测试。


推荐阅读