ctypes help

eryk sun eryksun at gmail.com
Sat Nov 18 05:56:27 EST 2017


 On Fri, Nov 17, 2017 at 10:11 PM, Python <python at bladeshadow.org> wrote:
>
> I'm starting to play with ctypes, as I'd like to provide Python
> interfaces to a C/C++ library I have.  For now I'm just messing with a
> very simple piece of code to get things sorted out.  I'm working with
> this example C++ library, which just wraps a call to stat():

Calling POSIX system functions via ctypes can be difficult if they use
struct layouts that vary with the target platform and architecture.

> class Stat(ctypes.Structure):
>     _fields_ = [("st_dev", ctypes.c_ulong),
>                 ("st_ino", ctypes.c_ulong),
>                 ("st_mode", ctypes.c_ulong),
>                 ("st_nlink", ctypes.c_ulong),
>                 ("st_uid", ctypes.c_ulong),
>                 ("st_gid", ctypes.c_ulong),

For x64 the above incorrectly uses 64-bit integers for mode, uid, and
gid. Also, it has mode and nlink flipped around in the x86 order
instead of the new x64 order.

>                 ("st_rdev", ctypes.c_ulong),
>                 ("st_size", ctypes.c_ulonglong),
>                 ("st_blksize", ctypes.c_ulonglong),
>                 ("st_blocks", ctypes.c_ulonglong),
>                 ("st_atim", Timespec),
>                 ("st_mtim", Timespec),
>                 ("st_ctim", Timespec)]

Try to strictly adhere to the defined types. Don't use `long long` for
a `long`, even if it happens to be the same size in a given
architecture. Also, avoid using definitions from docs and man pages.
Use the exact headers that the compiler sees. In this case, you missed
the 3 reserved long-integer fields at the end on x64 builds. Your Stat
struct is 128 bytes overall instead of the required 144 bytes. glibc
will corrupt the heap when it writes to the last 16 bytes. The best
case is that this immediately crashes Python.

Below I've included an example ctypes wrapper for calling stat() on
Linux x64 and x86 systems. I verified the sizes and offsets and tested
in 64-bit Python. From a C test program, I also have the field sizes
and offsets for the two 32-bit cases (with and without large file
support), but I'd need to build 32-bit Python to verify that the
ctypes wrapper is correct. I have no personal use for 32-bit Python,
so I leave that part up to you.

C header definitions from time.h, bits/typesizes.h, bits/types.h, and
bits/stat.h:

    #define __DEV_T_TYPE       __UQUAD_TYPE
    #define __INO_T_TYPE       __SYSCALL_ULONG_TYPE
    #define __INO64_T_TYPE     __UQUAD_TYPE
    #ifdef __x86_64__
    #define __NLINK_T_TYPE     __SYSCALL_ULONG_TYPE
    #else
    #define __NLINK_T_TYPE     __UWORD_TYPE
    #endif
    #define __MODE_T_TYPE      __U32_TYPE
    #define __UID_T_TYPE       __U32_TYPE
    #define __GID_T_TYPE       __U32_TYPE
    #define __OFF_T_TYPE       __SYSCALL_SLONG_TYPE
    #define __OFF64_T_TYPE     __SQUAD_TYPE
    #define __BLKSIZE_T_TYPE   __SYSCALL_SLONG_TYPE
    #define __BLKCNT_T_TYPE    __SYSCALL_SLONG_TYPE
    #define __BLKCNT64_T_TYPE  __SQUAD_TYPE
    #define __TIME_T_TYPE      __SYSCALL_SLONG_TYPE

    struct timespec
    {
        __time_t tv_sec;
        __syscall_slong_t tv_nsec;
    };

    struct stat
    {
        __dev_t st_dev;
    #ifndef __x86_64__
        unsigned short int __pad1;
    #endif
    #if defined __x86_64__ || !defined __USE_FILE_OFFSET64
        __ino_t st_ino;
    #else
        __ino_t __st_ino;
    #endif
    #ifndef __x86_64__
        __mode_t st_mode;
        __nlink_t st_nlink;
    #else
        __nlink_t st_nlink;
        __mode_t st_mode;
    #endif
        __uid_t st_uid;
        __gid_t st_gid;
    #ifdef __x86_64__
        int __pad0;
    #endif
        __dev_t st_rdev;
    #ifndef __x86_64__
        unsigned short int __pad2;
    #endif
    #if defined __x86_64__ || !defined __USE_FILE_OFFSET64
        __off_t st_size;
    #else
        __off64_t st_size;
    #endif
        __blksize_t st_blksize;
    #if defined __x86_64__  || !defined __USE_FILE_OFFSET64
        __blkcnt_t st_blocks;
    #else
        __blkcnt64_t st_blocks;
    #endif
    #ifdef __USE_XOPEN2K8
        struct timespec st_atim;
        struct timespec st_mtim;
        struct timespec st_ctim;
    #else
        __time_t st_atime;
        __syscall_ulong_t st_atimensec;
        __time_t st_mtime;
        __syscall_ulong_t st_mtimensec;
        __time_t st_ctime;
        __syscall_ulong_t st_ctimensec;
    #endif
    #ifdef __x86_64__
        __syscall_slong_t __glibc_reserved[3];
    #else
    #ifndef __USE_FILE_OFFSET64
        unsigned long int __glibc_reserved4;
        unsigned long int __glibc_reserved5;
    #else
        __ino64_t st_ino;
    #endif
    #endif
    };


ctypes:

    import os
    import sys
    import ctypes
    import ctypes.util

    libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)

    def c_errcheck(result, func, args):
        if result == -1:
            err = ctypes.get_errno()
            raise OSError(err, os.strerror(err))
        return args

    class timespec(ctypes.Structure):
        _fields_ = (('tv_sec',  ctypes.c_long),
                    ('tv_nsec', ctypes.c_long))

    _FILE_OFFSET_BITS = 64

    class stat_result(ctypes.Structure):
        if ctypes.sizeof(ctypes.c_void_p) == 8:
            _fields_ = (('st_dev',     ctypes.c_uint64),
                        ('st_ino',     ctypes.c_ulong),
                        ('st_nlink',   ctypes.c_ulong),
                        ('st_mode',    ctypes.c_uint32),
                        ('st_uid',     ctypes.c_uint32),
                        ('st_gid',     ctypes.c_uint32),
                        ('_pad0',      ctypes.c_int),
                        ('st_rdev',    ctypes.c_uint64),
                        ('st_size',    ctypes.c_long),
                        ('st_blksize', ctypes.c_long),
                        ('st_blocks',  ctypes.c_long),
                        ('st_atim',    timespec),
                        ('st_mtim',    timespec),
                        ('st_ctim',    timespec),
                        ('_reserved',  ctypes.c_long * 3))
        elif _FILE_OFFSET_BITS == 64:
            _fields_ = (('st_dev',     ctypes.c_uint64),
                        ('_pad1',      ctypes.c_int),
                        ('_st_ino',    ctypes.c_ulong),
                        ('st_mode',    ctypes.c_uint32),
                        ('st_nlink',   ctypes.c_uint),
                        ('st_uid',     ctypes.c_uint32),
                        ('st_gid',     ctypes.c_uint32),
                        ('st_rdev',    ctypes.c_uint64),
                        ('_pad2',      ctypes.c_ushort),
                        ('st_size',    ctypes.c_int64),
                        ('st_blksize', ctypes.c_long),
                        ('st_blocks',  ctypes.c_int64),
                        ('st_atim',    timespec),
                        ('st_mtim',    timespec),
                        ('st_ctim',    timespec),
                        ('st_ino',     ctypes.c_uint64))
        else:
            _fields_ = (('st_dev',     ctypes.c_uint64),
                        ('_pad1',      ctypes.c_int),
                        ('st_ino',     ctypes.c_ulong),
                        ('st_mode',    ctypes.c_uint32),
                        ('st_nlink',   ctypes.c_uint),
                        ('st_uid',     ctypes.c_uint32),
                        ('st_gid',     ctypes.c_uint32),
                        ('st_rdev',    ctypes.c_uint64),
                        ('_pad2',      ctypes.c_ushort),
                        ('st_size',    ctypes.c_long),
                        ('st_blksize', ctypes.c_long),
                        ('st_blocks',  ctypes.c_long),
                        ('st_atim',    timespec),
                        ('st_mtim',    timespec),
                        ('st_ctim',    timespec),
                        ('_reserved4', ctypes.c_ulong),
                        ('_reserved5', ctypes.c_ulong))
        @property
        def st_atime(self):
            t = self.st_atim
            return t.tv_sec + t.tv_nsec * 1e-9
        @property
        def st_mtime(self):
            t = self.st_mtim
            return t.tv_sec + t.tv_nsec * 1e-9
        @property
        def st_ctime(self):
            t = self.st_ctim
            return t.tv_sec + t.tv_nsec * 1e-9

    libc.__xstat.errcheck = c_errcheck
    libc.__xstat.argtypes = (
        ctypes.c_int,                # API version
        ctypes.c_char_p,             # filename
        ctypes.POINTER(stat_result)) # buf

    if ctypes.sizeof(ctypes.c_void_p) == 8:
        _STAT_VER_KERNEL = 0
        _STAT_VER_LINUX  = 1
    else:
        _STAT_VER_LINUX_OLD = 1
        _STAT_VER_KERNEL    = 1
        _STAT_VER_SVR4      = 2
        _STAT_VER_LINUX     = 3

    def mystat(filename):
        if not isinstance(filename, bytes):
            if hasattr(os, 'fsencode'):
                filename = os.fsencode(filename)
            else:
                filename = filename.encode(sys.getfilesystemencoding())
        st = stat_result()
        libc.__xstat(_STAT_VER_LINUX, filename, ctypes.byref(st))
        return st

    if __name__ == '__main__':
        st = os.stat('/')
        myst = mystat('/')
        assert (st.st_dev, st.st_ino) == (myst.st_dev, myst.st_ino)
        assert st.st_mode == myst.st_mode
        assert st.st_atime == myst.st_atime
        assert st.st_mtime == myst.st_mtime



More information about the Python-list mailing list