ctypes help
Python
python at bladeshadow.org
Sat Nov 18 20:17:33 EST 2017
Eryk,
Thanks much for the excellent and highly detailed response! That made
a lot of things clear.
On Sat, Nov 18, 2017 at 10:56:27AM +0000, eryk sun wrote:
> 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