Python C扩展链接自定义共享库

8

我正在一个非常旧的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进行编译。 zlibconfigure脚本使这非常容易:只需运行./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扩展模块/进程的其余部分与以前一样工作。


哪些软件包会阻止zlib的升级?我知道的唯一一个是libxml,但已经修复了。 - Mark Adler
@MarkAdler:是的,它是libxml。问题在于这是一个非常老的系统,所以系统上的libxml版本存在该bug。我想避免升级从2.6.26(已安装的版本)到2.7.7的libxml,因为libxml在许多其他地方都被使用,我不确定它是否会破坏其他东西。 - Alok Singhal
静态链接到自定义的 libz.a 应该可以工作,假设 bug 明确在 libz 中,并且您已经编译了一个没有这个 bug 的版本。您确定您选择的是正确的版本吗?尝试将其重命名为 libmyz.a 并使用 -lmyz - Aya
哦,如果你是通过Python的zlibmodule.c使用zlib而不是直接调用它,那么有一个bug,看起来直到Python 2.7才得以修复。 - Aya
@Aya,我今晚会再次尝试并尽快发布更新。我没有使用zlibmodule.c - Alok Singhal
显示剩余2条评论
2个回答

2

看起来这个问题与另一个问题相似,但我得到了相反的行为。

我下载了zlib-1.2.8的tarball,并运行了./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

主要是为了在共享库中链接它,向libz.a添加了-fPIC

然后,在gzlib.cgzopen()gzopen64()gz_open()函数中加入了一些printf()语句,以便我可以轻松地知道是否正在调用它们。

在构建了libz.alibz.so之后,我创建了一个非常简单的foo.c...

#include "zlib-1.2.8/zlib.h"

void main()
{
    gzFile foo = gzopen("foo.gz", "rb");
}

...并编译了一个独立的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

... 但使用 Python 中的 foo.so...

import ctypes

foo = ctypes.CDLL('./foo.so')
foo.main()

...没有打印出任何东西,所以我猜它正在使用Python的libz.so...

$ ldd `which python`
        ...
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f5af2c68000)
        ...

即使 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')

...打印出了...

gzopen64
gz_open

请注意,从gzopengzopen64的转换是由预处理器完成的,因此我必须直接调用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
 */

这意味着如果您没有定义_LARGEFILE64_SOURCEgzopen64()函数将不会被公开。我不确定_LFS64_LARGEFILE是否适用于您的系统。


嗯,这给了我一些想法 - 我现在要运行一些实验,沿着你用我的Cython代码所做的方向。谢谢! - Alok Singhal
@Alok 我觉得使用 ctypes 库会更容易,但如果你仍然想使用 Cython 包装器,那么在 Cython 代码中使用 dlopen() 加载 libz.so.1.2.8 库应该可以工作。我还注意到你的 'extra_compile_args': ['-D_LARGEFILE_SOURCE ... 中似乎有一个拼写错误,可能需要改为 -D_LARGEFILE64_SOURCE - Aya
问题在于我正在维护一些使用Cython编写的代码。关于_LARGEFILE64_SOURCE,我使用了getconf LFS_CFLAGS命令的输出标志,所以这不是一个打字错误。http://www.gnu.org/software/libc/manual/html_node/Feature-Test-Macros.html#index-g_t_005fLARGEFILE64_005fSOURCE-48说:“它是一个过渡接口,用于64位偏移量普遍不使用的时期(请参见`_FILE_OFFSET_BITS`)”,因此我认为我不需要定义`_LARGEFILE64_SOURCE`,但我还是会尝试一下。 - Alok Singhal
不知何故@Aya被吞了。很奇怪。 - Alok Singhal
@Alok,请查看有关 _LARGEFILE_SOURCE_LARGEFILE64_SOURCE 的更新。 - Aya
显示剩余5条评论

2

我建议使用ctypes。将你的C库编写为普通共享库,然后使用ctypes进行访问。你需要编写更多Python代码,将数据从Python数据结构转换为C数据结构。最大的优势是可以将所有内容与系统隔离。你可以明确指定要加载的*.so文件。不需要使用Python C API。我使用ctypes有很好的经验。由于你似乎精通C语言,这对你来说应该不会太困难。


感谢Mike的回答。我没有尝试过ctypes,但我确实尝试过使用dlopen()并从新共享库的完整路径显式加载符号。我本以为这种方法也可以工作,但我一直得到 gzopen64 符号未定义或段错误的错误。我很快会在我的帖子中更新更多细节。(实际上,我是在Cython中编写这个扩展。) - Alok Singhal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接