pythonctypeserrno

How to (correctly) use `ctypes.get_errno()`?


I'm trying to test some binary library with ctypes and some of my tests involve errno.

I'm therefore trying to retrieve it to check the error cases handling but when trying to use ctypes.get_errno() I weirdly get 0 as errno ("Success") which isn't what I was expecting.

Why does this occur? Is ctypes.get_errno() actually reliable?

#!/usr/bin/env python3

import os
import ctypes
import errno

libc = ctypes.cdll.LoadLibrary("libc.so.6")
libc.write.restype = ctypes.c_ssize_t
libc.write.argtypes = ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t

TMP_FILE = "/tmp/foo"

def main():
    fd: int
    errno: int = 0

    fd = os.open(TMP_FILE, os.O_RDONLY | os.O_CREAT)
    if fd == -1:
        errno = ctypes.get_errno()
        print(strerror(errno))

    if (not errno and libc.write(fd, "foo", 3) == -1):
        errno = ctypes.get_errno()
        print(f"ERRNO: {errno}")
        print(os.strerror(errno))

    os.close(fd);
    os.remove(TMP_FILE)

    if errno:
        raise OSError(errno, os.strerror(errno))


if __name__ == "__main__":
    main()

$ ./test.py
ERRNO: 0
Success

NB: I already have a workaround from an answer under an other post (see MRE below) but I'd like to understand what's going on with ctypes.get_errno().

#!/usr/bin/env python3

import os
import ctypes

libc = ctypes.cdll.LoadLibrary("libc.so.6")
libc.write.restype = ctypes.c_ssize_t
libc.write.argtypes = ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t

TMP_FILE = "/tmp/foo"

_get_errno_loc = libc.__errno_location
_get_errno_loc.restype = ctypes.POINTER(ctypes.c_int)

def get_errno() -> int:
    return _get_errno_loc()[0]

def main():
    fd: int
    errno: int = 0

    fd = os.open(TMP_FILE, os.O_RDONLY | os.O_CREAT)
    if fd == -1:
        errno = get_errno()
        print(strerror(errno))

    if (not errno and libc.write(fd, "foo", 3) == -1):
        errno = get_errno()
        print(f"ERRNO: {errno}")
        print(os.strerror(errno))

    os.close(fd);
    os.remove(TMP_FILE)

    if errno:
        raise OSError(errno, os.strerror(errno))


if __name__ == "__main__":
    main()

$ ./test_with_workaround.py
ERRNO: 9
Bad file descriptor
Traceback (most recent call last):
  File "/mnt/nfs/homes/vmonteco/Code/MREs/MRE_python_fdopen_cause_errno/simple_python_test/./test_with_workaround.py", line 41, in <module>
    main()
  File "/mnt/nfs/homes/vmonteco/Code/MREs/MRE_python_fdopen_cause_errno/simple_python_test/./test_with_workaround.py", line 37, in main
    raise OSError(errno, os.strerror(errno))
OSError: [Errno 9] Bad file descriptor

Solution

  • Unlike your handwritten get_errno, ctypes get_errno does not access the os errno value, but a private copy that is filled after certain function calls. The docs state:

    [...] a ctypes mechanism that allows accessing the system errno error number in a safe way. ctypes maintains a thread-local copy of the systems errno variable; if you call foreign functions created with use_errno=True then the errno value before the function call is swapped with the ctypes private copy, the same happens immediately after the function call.

    Thus ctypes.get_errno() will never work for getting errno that might have been set as a side-effect of os.open. But you can (and probably should) use it for your function that you call via ctypes. But there you need to set use_errno to True.

    use_errno is a parameter of function definition creation. When you use the mylib.myfunc syntax as you did with libc.write, the function creation is implicit and inherits some defaults from the library loader. Here you use ctypes.cdll, which sets use_errno to False. You an change that by loading the Library more explicitly:

    libc = ctypes.CDll("libc.so.6", use_errno=True)
    

    Note that this will apply use_errno (and the associated overhead) to all functions. If you want to use use_errno for single functions, you can use function prototypes instead.