Передайте 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
"sFILE*
(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
кеширование)