我正在一个非常旧的Red Hat系统上编写Python C扩展程序。该系统使用的是zlib 1.2.3,它不支持大文件。不幸的是,我不能升级系统zlib到新版本,因为一些软件包会访问zlib内部结构,并在较新的zlib版本上出现问题。
我想构建我的扩展程序,使所有的zlib调用(如gzopen(), gzseek()等)都解析到我安装在用户目录中的自定义zlib,而不影响Python可执行文件和其他扩展程序。
我已经尝试过通过将libz.a添加到gcc命令行中进行静态链接来解决这个问题,但没有成功(例如,仍然无法使用gzopen()创建大文件)。我还尝试通过传递-z origin -Wl,-rpath=/path/to/zlib -lz
给gcc来解决,但也没有成功。
由于较新版本的zlib仍然被命名为"zlib 1.x",因此soname相同,所以我认为符号版本控制不起作用。有没有办法实现我想做的事情?
我在32位Linux系统上。 Python 版本是2.6,是自定义构建的。
编辑:
我创建了一个最小化的示例。我正在使用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('%lld\n', <long long>ifp.tell())
printf('%lld\n', <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 GB的内容。
当我使用静态链接的gztest.so
时,它会崩溃:
$ python -c 'import gztest; gztest.read_test()'
error (2): No such file or directory (0: )
0
Segmentation fault (core dumped)
关于“没有这样的文件或目录”的错误是误导性的——该文件存在且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:
我最终做了以下操作:
- 使用带有
z_
前缀的导出所有符号的自定义zlib进行编译。zlib
的configure
脚本使这非常容易:只需运行./configure --zprefix ...
。 - 在我的Cython代码中调用
gzopen64()
而不是gzopen()
。 这是因为我想确保使用正确的“底层”符号。 - 显式使用
z_off64_t
。 - 将我的自定义
zlib.a
静态链接到由Cython生成的共享库中。 我在使用gcc链接时使用'-Wl,--whole-archive /home/alok/zlib_lfs_z/lib/libz.a -Wl,--no-whole-archive'
。 可能还有其他方法或者可能不需要这样做,但似乎这是确保使用正确库的最简单方法。
通过以上更改,大文件可以正常工作,而Python扩展模块/进程的其余部分与以前一样工作。
libz.a
应该可以工作,假设 bug 明确在 libz 中,并且您已经编译了一个没有这个 bug 的版本。您确定您选择的是正确的版本吗?尝试将其重命名为libmyz.a
并使用-lmyz
。 - Ayazlibmodule.c
使用zlib而不是直接调用它,那么有一个bug,看起来直到Python 2.7才得以修复。 - Ayazlibmodule.c
。 - Alok Singhal