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?
test.py
:#!/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()
.
test_with_workaround.py
:#!/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
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.