Всегда ли можно полагаться на ctypes.data_as для хранения ссылок на временные файлы? - PullRequest
4 голосов
/ 29 января 2020

При передаче массивов из python во внутреннюю библиотеку c++ можно ли полагаться на следующее? Раньше это работало в python <= 3.6, но, похоже, приводит к sporadi c сбоям в python >= 3.7:

(это значительно упрощенная версия «реального» кода, в пользовательский интерфейс python передает данные назад и вперед между базовыми c++ lib)

# a 2d array, possibly not order="F"
xmat = np.ones((16, 32), dtype=np.float64)

# get a pointer to a version of xmat that is guaranteed to have order="F"
# if xmat already has order="F": no temporary
# if not, a temporary copy is made, reordered and a ptr to that returned
xptr = np.asfortranarray(xmat).ctypes.data_as(ctypes.POINTER(ctypes.c_double))

# pass xptr to c++ back-end to do things (expects order="F" data)

Как я (в настоящее время!) понимаю, ctypes.data_as должен :

Возвращает указатель данных, приведенный к конкретному объекту c -типа ...

Возвращенный указатель сохранит ссылку на массив.

с дополнительным примером, показывающим, что в случаях, когда создаются временные данные, такие как (a + b).ctypes.data_as(ctypes.c_void_p), использование data_as является правильным решением.

В python >= 3.7 кажется что data_as означает , а не , сохраняя ссылку на временное хранилище, и что в приведенном выше примере xptr заканчивается указанием на освобожденную память ...

Я что-то не так делаю? Это ошибка в python >= 3.7? Есть ли лучший способ сделать это?


Полный пример (с некоторыми дополнительными шаблонами, которые маршалы array в struct для внутренней библиотеки) приведен здесь:

import numpy as np
import ctypes as ct

lib_REALS_t = ct.c_double
lib_INDEX_t = ct.c_int32
lib_REALS_p = ct.POINTER(lib_REALS_t)

class lib_REALS_array_t(ct.Structure):
    _fields_ = [("size", lib_INDEX_t),
                ("data", lib_REALS_p)]

class lib_t(ct.Structure):
    _fields_ = [
    ("value", lib_REALS_array_t)]

def bug():

    libt = lib_t()

    # a 2d array, user-specified, possibly not order="F"
    xmat = np.ones((16, 32), dtype=np.float64, order="C")

    # get a pointer to a version of xmat that is guaranteed to have order="F"
    # if xmat already has order="F": no temporary
    # if not, a temporary copy is made, reordered and a ptr to that returned
    libt.value.size = xmat.size
    libt.value.data = np.asfortranarray(xmat).ctypes.data_as(ct.POINTER(lib_REALS_t))

    # pass xptr to c++ back-end to do things (expects order="F" data)

    # just "simulate" this by trying to access data using the pointer
    print(libt.value.data[1])

    return


if (__name__ == "__main__"): bug()

Для меня python <= 3.6 печатает 1.0 (как и ожидалось), в то время как python >= 3.7 печатает 6.92213454250094e-310 (т.е. временное должно быть свободным, поэтому указывает на неинициализированную память).

Ответы [ 2 ]

1 голос
/ 06 февраля 2020

Листинг [Python 3.Docs]: ctypes - библиотека сторонних функций для Python.

После исследований и поиска кода я пришел к выводу (я интуитивно, что происходит с самого начала).

Кажется, что [SciPy.Docs]: numpy .ndarray.ctypes :

_ctypes.data_as ( self, obj )

...

В возвращенном указателе будет сохранена ссылка на массив.

вводит в заблуждение. Сохранение ссылки указывает, что он будет содержать адрес буфера массива (внутреннего) (в том смысле, что не будет копировать содержимое памяти ), а не Python ссылка ( Py_XINCREF ).

Просмотр [Github]: numpy / numpy - numpy / numpy / core / _internal. py :

def data_as(self, obj):
    # Comments
    return self._ctypes.cast(self._data, obj)

это вызов ctypes.cast , который содержит только адрес буфера массива источника.

В результате np.asfortranarray(xmat) создает временный массив (на лету), а затем ctypes.data_as возвращает адрес буфера. После строки временный объект выходит из области действия (как и его буфер), но на его адрес все еще ссылаются, что приводит к неопределенному поведению ( UB ).

В v1.15.0 ( [SciPy.Docs]: numpy .ndarray.ctypes ( выделение is мой)) это упоминается:

Будьте осторожны, используя атрибут ctypes - особенно для временных массивов или массивов, созданных на лету. Например, вызов (a+b).ctypes.data_as(ctypes.c_void_p) возвращает указатель на память, которая недопустима, поскольку массив, созданный как (a + b), освобождается перед следующим Python оператором . Вы можете избежать этой проблемы, используя c=a+b или ct=(a+b).ctypes. В последнем случае ct будет хранить ссылку на массив до тех пор, пока ct не будет удален или переназначен.

, но впоследствии они его удалили (хотя код не был изменен (относительно этого поведение)).

Чтобы обойти ошибку, «сохраните» временный массив или сохраните (Python) ссылку на него. Та же проблема возникла в [SO]: нарушение прав доступа при попытке считывания объекта, созданного в Python, переданного в std :: vector на стороне C ++ и затем возвращенного в Python (ответ @ CristiFati) .

Я немного изменил ваш код (включая эти ужасные имена :)).

code00.py :

#!/usr/bin/env python3

import sys
import ctypes as ct
import numpy as np
from collections import defaultdict


DblPtr = ct.POINTER(ct.c_double)

class Struct0(ct.Structure):
    _fields_ = [
        ("size", ct.c_uint32),
        ("data", DblPtr),
    ]


class Wrapper(ct.Structure):
    _fields_ = [
        ("value", Struct0),
    ]


def test_np(np_array, save_intermediary_array):
    wrapper = Wrapper()
    wrapper.value.size = np_array.size

    if save_intermediary_array:
        fortran_array = np.asfortranarray(np_array)
        wrapper.value.data = fortran_array.ctypes.data_as(DblPtr)
    else:
        wrapper.value.data = np.asfortranarray(np_array).ctypes.data_as(DblPtr)
    #print(wrapper.value.data[0])
    return wrapper.value.data[1]


def main(*argv):
    dim1, dim0 = 16, 32
    mat = np.ones((dim1, dim0), dtype=np.float64, order="C")
    print("NumPy CTypes data: {0:}\n{1:}".format(mat.ctypes, mat.ctypes._ctypes))

    dd = defaultdict(int)
    flag = 0  # Change to 1 to avoid problem
    print("Saving intermediary array: {0:d}".format(flag))
    for i in range(100):
        dd[test_np(mat, flag)] += 1
    print("\nResult: {0:}".format(dd))


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    print("NumPy version: {0:}".format(np.version.version))
    main(*sys.argv[1:])
    print("\nDone.")

Вывод :

e:\Work\Dev\StackOverflow\q059959608>sopr.bat
*** Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ***

[prompt]> "e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code01.py
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32

NumPy version: 1.18.0
NumPy CTypes data: <numpy.core._internal._ctypes object at 0x000001C9744B0348>
<module 'ctypes' from 'c:\\Install\\pc064\\Python\\Python\\03.07.06\\Lib\\ctypes\\__init__.py'>
Saving intermediary array: 0

Result: defaultdict(<class 'int'>, {9.707134377684e-312: 100})

Done.

[prompt]> "e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code01.py
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32

NumPy version: 1.18.0
NumPy CTypes data: <numpy.core._internal._ctypes object at 0x000001842ECA4FC8>
<module 'ctypes' from 'c:\\Install\\pc064\\Python\\Python\\03.07.06\\Lib\\ctypes\\__init__.py'>
Saving intermediary array: 0

Result: defaultdict(<class 'int'>, {1.0: 100})

Done.

[prompt]> "e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code01.py
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32

NumPy version: 1.18.0
NumPy CTypes data: <numpy.core._internal._ctypes object at 0x000001AD586E91C8>
<module 'ctypes' from 'c:\\Install\\pc064\\Python\\Python\\03.07.06\\Lib\\ctypes\\__init__.py'>
Saving intermediary array: 0

Result: defaultdict(<class 'int'>, {9.110668798574e-312: 100})

Done.

[prompt]> "e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code01.py
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32

NumPy version: 1.18.0
NumPy CTypes data: <numpy.core._internal._ctypes object at 0x0000012F903A9188>
<module 'ctypes' from 'c:\\Install\\pc064\\Python\\Python\\03.07.06\\Lib\\ctypes\\__init__.py'>
Saving intermediary array: 0

Result: defaultdict(<class 'int'>, {6.44158096444e-312: 100})

Done.

Примечания :

  • Как видно, результаты довольно случайные, как правило, UB индикатор
  • Интересно то, что при одном и том же прогоне это всегда одно и то же значение ( defaultdict имеет только один элемент)
  • Изменение flag to 1 (или что-либо, что оценивается как True ) заставит проблему исчезнуть
0 голосов
/ 06 февраля 2020

После некоторого времени чтения ctypes и поиска каких-либо критических изменений я смог решить эту проблему, просто добавив переменную прокси. Я мог бы легко воспроизвести проблему, просто вставив ваш пример кода.

Я не очень уверен в том, почему это происходит, но я могу предположить, что назначение указателя непосредственно другому указателю вызывает ошибку в ctypes , Я буду искать и другие возможности. Но сейчас вы можете решить эту проблему, добавив прокси-переменную, как показано ниже:

def bug():

    libt = lib_t()

    # a 2d array, user-specified, possibly not order="F"
    xmat = np.ones((16, 32), dtype=np.float64, order="C")

    # get a pointer to a version of xmat that is guaranteed to have order="F"
    # if xmat already has order="F": no temporary
    # if not, a temporary copy is made, reordered and a ptr to that returned
    libt.value.size = xmat.size
    temp_p = np.asfortranarray(xmat).ctypes.data_as(ct.POINTER(lib_REALS_t))
    libt.value.data = temp_p

    # pass xptr to c++ back-end to do things (expects order="F" data)

    # just "simulate" this by trying to access data using the pointer
    print(libt.value.data[1])

    return

ОБНОВЛЕНИЕ

Что ж, согласно ответу @ CristiFati, оказалось, что я на самом деле сделал все правильно случайно. Это имело смысл для меня, чтобы сохранить ссылку на фактический массив. Я попытался напечатать точное местоположение указателя, и оно менялось каждый раз. Так что я подумал, может быть, если я сохраню это один раз, это больше не изменится; и все работало нормально.

Очень хорошее расследование, проведенное @ CristiFati.

...