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

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

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

/* Library function */
void write_numbers(FILE * f, int arg1, int arg2)
{
   fprintf(f, "%d %dn", 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 2n' being printed.
print write_numbers(1,2)

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

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

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

1 ответов


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

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

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

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

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


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

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

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


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


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

  • тот факт, что файловые объекты Python 2 являются обертками над stdio ' s FILE* (Python 3 не являются)
  • PyFile_AsFile API, который возвращает обернутый FILE* (заметим, что он отсутствует в Python 3)
    • для автономной fd, вам нужно построить сначала объект, подобный файлу (так что будет FILE* вернуться ;) )
  • тот факт, что id() объекта является его адрес памяти (CPython-specific)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() do закрыть дескриптор .close()
    • поэтому дублируйте дескрипторы с os.dup() если они вам нужны после того, как файловый объект закрыт / собран мусор
  • при работе со структурой C настройте счетчик ссылок соответствующего объекта с помощью PyFile_IncUseCount()/PyFile_DecUseCount().
  • не гарантируйте никакого другого ввода-вывода на дескрипторах / файловых объектах, так как это испортит данные (например, с момента вызова iter(f)/for l in f, внутреннее кэширование выполняется независимо от stdioс кэшированием)