Ссылка расширения Python C с пользовательской общей библиотекой


Я пишу расширение Python C на очень старой системе Red Hat. Система имеет zlib 1.2.3, который неправильно поддерживает большие файлы. К сожалению, я не могу просто обновить систему zlib до более новой версии, так как некоторые пакеты проникают во внутренние структуры zlib, и это ломается на более новых версиях zlib.

Я хотел бы построить мое расширение так, чтобы все вызовы zlib(gzopen(), gzseek() и т.д.) разрешаются в пользовательский zlib, который я устанавливаю в свой пользовательский каталог, без влияет на остальные исполняемые файлы Python и другие расширения.

Я попытался статически связать в libz.a, добавив libz.a в командную строку gcc во время связывания, но это не сработало (до сих пор не удается создать большие файлы с помощью gzopen(), например). Я также попытался передать -z origin -Wl,-rpath=/path/to/zlib -lz в gcc, но это также не сработало.

Поскольку новые версии zlib все еще называются zlib 1.x, soname является тем же самым, поэтому я думаю, что версионирование символов не будет работать. Есть ли способ сделать то, что я хочешь заняться?

Я нахожусь на 32-битной системе Linux. Версия Python 2.6, которая построена на заказ.

Edit:

Я создал минимальный пример. Я использую Cython (версия 0.19.1).

Файл gztest.pyx:

from libc.stdio cimport printf, fprintf, stderr
from libc.string cimport strerror
from libc.errno cimport errno
from libc.stdint cimport int64_t

cdef extern from "zlib.h":
    ctypedef void *gzFile
    ctypedef int64_t z_off_t

    int gzclose(gzFile fp)
    gzFile gzopen(char *path, char *mode)
    int gzread(gzFile fp, void *buf, unsigned int n)
    char *gzerror(gzFile fp, int *errnum)

cdef void print_error(void *gzfp):
    cdef int errnum = 0
    cdef const char *s = gzerror(gzfp, &errnum)
    fprintf(stderr, "error (%d): %s (%d: %s)n", errno, strerror(errno), errnum, s)

cdef class GzFile:
    cdef gzFile fp
    cdef char *path
    def __init__(self, path, mode='rb'):
        self.path = path
        self.fp = gzopen(path, mode)
        if self.fp == NULL:
            raise IOError('%s: %s' % (path, strerror(errno)))

    cdef int read(self, void *buf, unsigned int n):
        cdef int r = gzread(self.fp, buf, n)
        if r <= 0:
            print_error(self.fp)
        return r

    cdef int close(self):
        cdef int r = gzclose(self.fp)
        return 0

def read_test():
    cdef GzFile ifp = GzFile('foo.gz')
    cdef char buf[8192]
    cdef int i, j
    cdef int n
    errno = 0
    for 0 <= i < 0x200:
        for 0 <= j < 0x210:
            n = ifp.read(buf, sizeof(buf))
            if n <= 0:
                break

        if n <= 0:
            break

        printf('%lldn', <long long>ifp.tell())

    printf('%lldn', <long long>ifp.tell())
    ifp.close()

Файл setup.py:

import sys
import os

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

if __name__ == '__main__':
    if 'CUSTOM_GZ' in os.environ:
        d = {
            'include_dirs': ['/home/alok/zlib_lfs/include'],
            'extra_objects': ['/home/alok/zlib_lfs/lib/libz.a'],
            'extra_compile_args': ['-D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -g3 -ggdb']
        }
    else:
        d = {'libraries': ['z']}
    ext = Extension('gztest', sources=['gztest.pyx'], **d)
    setup(name='gztest', cmdclass={'build_ext': build_ext}, ext_modules=[ext])

Пользовательские zlib в /home/alok/zlib_lfs (с zlib версии 1.2.8):

$ ls ~/zlib_lfs/lib/
libz.a  libz.so  libz.so.1  libz.so.1.2.8  pkgconfig

Чтобы скомпилировать модуль с помощью этого libz.a:

$ CUSTOM_GZ=1 python setup.py build_ext --inplace
running build_ext
cythoning gztest.pyx to gztest.c
building 'gztest' extension
gcc -fno-strict-aliasing -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/alok/zlib_lfs/include -I/opt/include/python2.6 -c gztest.c -o build/temp.linux-x86_64-2.6/gztest.o -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -g3 -ggdb
gcc -shared build/temp.linux-x86_64-2.6/gztest.o /home/alok/zlib_lfs/lib/libz.a -L/opt/lib -lpython2.6 -o /home/alok/gztest.so

gcc передается все флаги, которые я хочу (добавление полного пути к libz.a, большие флаги и т. д.).

Чтобы построить расширение без моего пользовательского zlib, я могу скомпилировать без CUSTOM_GZ определенного:

$ python setup.py build_ext --inplace
running build_ext
cythoning gztest.pyx to gztest.c
building 'gztest' extension
gcc -fno-strict-aliasing -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/opt/include/python2.6 -c gztest.c -o build/temp.linux-x86_64-2.6/gztest.o
gcc -shared build/temp.linux-x86_64-2.6/gztest.o -L/opt/lib -lz -lpython2.6 -o /home/alok/gztest.so

Мы можем проверить размер файлов gztest.so:

$ stat --format='%s %n' original/gztest.so custom/gztest.so 
62398 original/gztest.so
627744 custom/gztest.so

Таким образом, статически связанный файл намного больше, как и ожидалось.

Теперь я могу сделать:

>>> import gztest
>>> gztest.read_test()

И он попытается прочитать foo.gz в текущем каталоге.

Когда я делаю это, используя нестатически связанные gztest.so, он работает так, как ожидалось, пока не попытается прочитать более 2 ГБ.

Когда я делаю это, используя статически связанные gztest.so, он сбрасывает ядро:

$ python -c 'import gztest; gztest.read_test()'
error (2): No such file or directory (0: )
0
Segmentation fault (core dumped)

Ошибка о No such file or directory вводит в заблуждение-файл существует и gzopen() на самом деле возвращается успешно. Однако gzread() терпит неудачу.

Вот обратный путь gdb:

(gdb) bt
#0  0xf730eae4 in free () from /lib/libc.so.6
#1  0xf70725e2 in ?? () from /lib/libz.so.1
#2  0xf6ce9c70 in __pyx_f_6gztest_6GzFile_close (__pyx_v_self=0xf6f75278) at gztest.c:1140
#3  0xf6cea289 in __pyx_pf_6gztest_2read_test (__pyx_self=<optimized out>) at gztest.c:1526
#4  __pyx_pw_6gztest_3read_test (__pyx_self=0x0, unused=0x0) at gztest.c:1379
#5  0xf769910d in call_function (oparg=<optimized out>, pp_stack=<optimized out>) at Python/ceval.c:3690
#6  PyEval_EvalFrameEx (f=0x8115c64, throwflag=0) at Python/ceval.c:2389
#7  0xf769a3b4 in PyEval_EvalCodeEx (co=0xf6faada0, globals=0xf6ff81c4, locals=0xf6ff81c4, args=0x0, argcount=0, kws=0x0, kwcount=0, defs=0x0, defcount=0, closure=0x0) at Python/ceval.c:2968
#8  0xf769a433 in PyEval_EvalCode (co=0xf6faada0, globals=0xf6ff81c4, locals=0xf6ff81c4) at Python/ceval.c:522
#9  0xf76bbe1a in run_mod (arena=<optimized out>, flags=<optimized out>, locals=<optimized out>, globals=<optimized out>, filename=<optimized out>, mod=<optimized out>) at Python/pythonrun.c:1335
#10 PyRun_StringFlags (str=0x80a24c0 "import gztest; gztest.read_test()n", start=257, globals=0xf6ff81c4, locals=0xf6ff81c4, flags=0xffbf2888) at Python/pythonrun.c:1298
#11 0xf76bd003 in PyRun_SimpleStringFlags (command=0x80a24c0 "import gztest; gztest.read_test()n", flags=0xffbf2888) at Python/pythonrun.c:957
#12 0xf76ca1b9 in Py_Main (argc=1, argv=0xffbf2954) at Modules/main.c:548
#13 0x080485b2 in main ()
Одна из проблем, по-видимому, заключается в том, что вторая строка в обратном следе относится к libz.so.1! Если я делаю ldd gztest.so, я получаю, среди прочих строк:
    libz.so.1 => /lib/libz.so.1 (0xf6f87000)
Я не уверен, почему это происходит. хотя.

править 2:

В итоге я сделал следующее:

  • скомпилировал мой пользовательский zlib со всеми символами, экспортированными с префиксом z_. zlib'S configure скрипт делает это очень легко: просто запустите ./configure --zprefix ....
  • называется gzopen64() вместо gzopen() в моем коде Cython. Это потому, что я хотел убедиться, что использую правильный "базовый" символ.
  • используется z_off64_t явно.
  • статически связать мой обычай zlib.a с общим библиотека, созданная Cython. Для этого я использовал '-Wl,--whole-archive /home/alok/zlib_lfs_z/lib/libz.a -Wl,--no-whole-archive', связываясь с gcc. Может быть, есть и другие способы, или это не нужно, но это казалось самым простым способом убедиться, что правильная библиотека используется.

С приведенными выше изменениями большие файлы работают, в то время как остальные модули расширения Python/процессы работают, как и раньше.

2 6

2 ответа:

Похоже, что это похоже на проблему в другом вопросе, за исключением того, что я получаю противоположное поведение.

Я загрузил тарбол zlib-1.2.8, запустил ./configure, затем изменил следующие переменные Makefile...

CFLAGS=-O3  -fPIC -D_LARGEFILE64_SOURCE=1 -D_FILE_OFFSET_BITS=64

SFLAGS=-O3  -fPIC -D_LARGEFILE64_SOURCE=1 -D_FILE_OFFSET_BITS=64
[41]}...в основном, чтобы добавить -fPIC в libz.a, чтобы я мог ссылаться на него в общей библиотеке.

Затем я добавил некоторые операторы printf() в функции gzlib.c gzopen(), gzopen64(), и gz_open() так что я легко мог сказать, были ли они вызваны.

После строя libz.a и libz.so, я создал действительно простой foo.c...

#include "zlib-1.2.8/zlib.h"

void main()
{
    gzFile foo = gzopen("foo.gz", "rb");
}
[41]}...и скомпилировал как автономный двоичный файл foo, так и общую библиотеку foo.so...
gcc -fPIC -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -o foo.o -c foo.c
gcc -o foo foo.o zlib-1.2.8/libz.a
gcc -shared -o foo.so foo.o zlib-1.2.8/libz.a

Запуск foo работал, как и ожидалось, и печатался...

gzopen64
gz_open
[41]}...но используя foo.so в Python С...
import ctypes

foo = ctypes.CDLL('./foo.so')
foo.main()
[41]}...ничего не печатал, так что я думаю, что он использует Python libz.so...
$ ldd `which python`
        ...
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f5af2c68000)
        ...
[41]}...хотя foo.so им не пользуется...
$ ldd foo.so
        linux-vdso.so.1 =>  (0x00007fff93600000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc8bfa98000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fc8c0078000)

Единственный способ, которым я мог это сделать. работа заключалась в том, чтобы открыть обычай libz.so непосредственно С...

import ctypes

libz = ctypes.CDLL('zlib-1.2.8/libz.so.1.2.8')
libz.gzopen64('foo.gz', 'rb')
[41]}...который распечатали...
gzopen64
gz_open
Обратите внимание, что перевод с gzopen на gzopen64 выполняется препроцессором, поэтому мне пришлось вызвать gzopen64() напрямую.

Так что это один из способов исправить это, но лучшим способом, вероятно, было бы перекомпилировать пользовательский Python 2.6, чтобы либо связать его со статическим zlib-1.2.8/libz.a, либо полностью отключить zlibmodule.c, тогда у вас будет больше гибкости в связывании опции.


Обновить

Относительно _LARGEFILE_SOURCE против _LARGEFILE64_SOURCE: я только указал на это из-за этого комментария в zlib.h...

/* provide 64-bit offset functions if _LARGEFILE64_SOURCE defined, and/or
 * change the regular functions to 64 bits if _FILE_OFFSET_BITS is 64 (if
 * both are true, the application gets the *64 functions, and the regular
 * functions are changed to 64 bits) -- in case these are set on systems
 * without large file support, _LFS64_LARGEFILE must also be true
 */
[41]}...подразумевается, что функция gzopen64() не будет открыта, если вы не определите _LARGEFILE64_SOURCE. Я не уверен, относится ли _LFS64_LARGEFILE к вашей системе или нет.

Я бы рекомендовал использовать ctypes. Напишите свою библиотеку C как обычную общую библиотеку и используйте ctypes для доступа к ней. Вам нужно будет написать немного больше кода Python, чтобы перенести данные из структур данных Python в C-структуры. Большое преимущество заключается в том, что вы можете изолировать все от остальной системы. Вы можете явно указать файл *.so, который вы хотите загрузить. Python C API не требуется. У меня есть довольно хороший опыт работы с ctypes. Это должно быть не слишком сложно для вас. Вы, поскольку вы, кажется, хорошо владеете С.