Передайте FILE * в функцию из Python / ctypes

У меня есть библиотечная функция (написана на C), которая генерирует текст, записывая вывод в FILE *, Я хочу обернуть это в Python (2.7.x) кодом, который создает временный файл или канал, передает его в функцию, читает результат из файла и возвращает его в виде строки Python.

Вот упрощенный пример, чтобы проиллюстрировать, что я ищу:

/* Library function */
void write_numbers(FILE * f, int arg1, int arg2)
{
   fprintf(f, "%d %d\n", arg1, arg2);
}

Обертка Python:

from ctypes import *
mylib = CDLL('mylib.so')


def write_numbers( a, b ):
   rd, wr = os.pipe()

   write_fp = MAGIC_HERE(wr)
   mylib.write_numbers(write_fp, a, b)
   os.close(wr)

   read_file = os.fdopen(rd)
   res = read_file.read()
   read_file.close()

   return res

#Should result in '1 2\n' being printed.
print write_numbers(1,2)

Мне интересно, что мой лучший выбор для MAGIC_HERE(),

Я соблазн просто использовать ctypes и создать libc.fdopen() обертка, которая возвращает Python c_void_t, а затем передает это в библиотечную функцию. Мне кажется, что это должно быть безопасно в теории - просто интересно, есть ли проблемы с этим подходом или существующим Python-измом для решения этой проблемы.

Кроме того, это будет продолжительным процессом (давайте предположим, что "навсегда"), поэтому любые утечки файловых дескрипторов будут проблематичными.

1 ответ

Решение

Во-первых, обратите внимание, что FILE* является специфичным для stdio объектом. Это не существует на системном уровне. Вещи, которые существуют на системном уровне, являются дескрипторами (извлекаются с file.fileno()) в UNIX (os.pipe() возвращает уже простые дескрипторы) и дескрипторы (извлекается с помощью msvcrt.get_osfhandle()) в Windows. Таким образом, это плохой выбор в качестве формата межбиблиотечного обмена, если в действии может быть более одной среды выполнения C. У вас будут проблемы, если ваша библиотека будет скомпилирована с использованием другой среды выполнения C, отличной от вашей копии Python: 1) двоичные макеты структуры могут отличаться (например, из-за выравнивания или дополнительных членов для целей отладки или даже из-за разного размера шрифта); 2) в Windows файловые дескрипторы, на которые ссылается структура, также являются специфичными для C сущностями, и их таблица поддерживается средой выполнения C внутренне 1.

Кроме того, в Python 3, I/O был пересмотрен, чтобы распутать его от stdio, Так, FILE* чуждо этому вкусу Python (и, скорее всего, большинству не-C).

Теперь, что вам нужно, это

  • как-то угадать, какая C-среда вам нужна, и
  • назовите его fdopen() (или эквивалент).

(В конце концов, один из девизов Python - "сделать правильную вещь легкой, а неправильную - трудной", в конце концов)


Самый чистый метод - использовать точный экземпляр, с которым связана библиотека (молитесь, чтобы он был связан с ним динамически, иначе не будет экспортированного символа для вызова)

Что касается первого пункта, я не смог найти ни одного модуля Python, который мог бы анализировать метаданные загруженных динамических модулей, чтобы выяснить, с какими библиотеками / с которыми они связаны (просто имени или даже имени + версии недостаточно, вы знаете, из-за возможных нескольких экземпляров библиотеки в системе). Хотя это определенно возможно, так как информация о его формате широко доступна.

Для 2-го пункта это тривиально ctypes.cdll('path').fdopen (_fdopen для MSVCRT).


Во-вторых, вы можете создать небольшой вспомогательный модуль, который будет скомпилирован в ту же (или гарантированно совместимую) среду выполнения, что и библиотека, и будет выполнять преобразование из вышеупомянутого дескриптора / дескриптора для вас. Это эффективный обходной путь к редактированию самой библиотеки.


Наконец, есть самый простой (и самый грязный) метод, использующий экземпляр среды выполнения Python C (так что все вышеупомянутые предупреждения применяются полностью) через API Python C, доступный через ctypes.pythonapi, Это использует в своих интересах

  • тот факт, что файловые объекты Python 2 являются обертками над stdio "s FILE* (Python 3 не)
  • PyFile_AsFile API, который возвращает упакованный FILE* (обратите внимание, что он отсутствует в Python 3)
    • для автономного fd необходимо сначала создать файлоподобный объект (чтобы FILE* возвращать;))
  • дело в том, что id() объекта - это адрес его памяти (специфичный для CPython) 2

    >>> open("test.txt")
    <open file 'test.txt', mode 'r' at 0x017F8F40>
    >>> f=_
    >>> f.fileno()
    3
    >>> ctypes.pythonapi
    <PyDLL 'python dll', handle 1e000000 at 12808b0>
    >>> api=_
    >>> api.PyFile_AsFile
    <_FuncPtr object at 0x018557B0>
    >>> api.PyFile_AsFile.restype=ctypes.c_void_p   #as per ctypes docs,
                                             # pythonapi assumes all fns
                                             # to return int by default
    >>> api.PyFile_AsFile.argtypes=(ctypes.c_void_p,) # as of 2.7.10, long integers are
                    #silently truncated to ints, see http://bugs.python.org/issue24747
    >>> api.PyFile_AsFile(id(f))
    2019259400
    

Имейте в виду, что с fd Указатели s и C, вам нужно вручную обеспечить правильное время жизни объекта!

  • файловые объекты, возвращаемые os.fdopen() закройте дескриптор .close()
    • поэтому дубликаты дескрипторов с os.dup() если они вам нужны после закрытия файлового объекта / сборки мусора
  • при работе со структурой C настройте счетчик ссылок соответствующего объекта с помощью PyFile_IncUseCount() / PyFile_DecUseCount(),
  • не обеспечивает никаких других операций ввода / вывода для дескрипторов / файловых объектов, поскольку это приведет к порче данных (например, с тех пор, как iter(f) / for l in f внутреннее кэширование выполняется независимо от stdio кеширование)
Другие вопросы по тегам