2

Introduction

I am working on a code generator that would generate functions around ctypes.cdll loaded functions. The generator would take information about the ctypes of the arguments and the return value, and generate something that behaves (and, to some extent, looks) like this:

func = getattr(dll, 'UglyLongAndUselessCName')
func.argtypes = [ctypes.c_uint32, ctypes.c_int8, ctypes.c_char_p]
func.restype = ctypes.c_int16

def nice_python_name(handle: int, arg1: int, arg2: str) -> int:
    return func(handle, arg1, arg2)

Notice how python type annotations play nicely with the ctypes data types of the function arguments. Also notice that there is no conversion code between the python types in nice_python_name function and func function. This is what my question is about.

Getting close to the problem

The ctypes docs say that if the argtypes attribute of a loaded DLL function is specified using "fundamental data types", then when calling the loaded DLL functions, ctypes will do the conversion to python types for you. This is great, because in this case my generated code will look like the example above - I won't need to explicitly convert ctypes objects to python-typed values for returned values, and the reverse for the arguments.

However, the docs also say that for "subclasses of the fundamental data types" this trick will not work, and calling a loaded DLL function will require ctypes objects for arguments, and the result will be a ctypes object.

This is the excerpt from the ctypes docs about it:

Fundamental data types, when returned as foreign function call results, or, for example, by retrieving structure field members or array items, are transparently converted to native Python types. In other words, if a foreign function has a restype of c_char_p, you will always receive a Python bytes object, not a c_char_p instance.

Subclasses of fundamental data types do not inherit this behavior. So, if a foreign functions restype is a subclass of c_void_p, you will receive an instance of this subclass from the function call. Of course, you can get the value of the pointer by accessing the value attribute.

So, I would like to get around this.

It seems that I need to know if a type is "fundamental" or a "subclass". This will help me define the way the code is generated, i.e. for "fundamental" types the generated code would look similar to the above example, and for "subclasses of fundamental" types it will have the extra conversion from ctypes objects into reasonable python types (or the generator will just throw an exception saying "this is not supported").

Question:

How can I distinguish between "fundamental ctypes data types" and "subclasses of fundamental ctypes data types"?

I looked into the code of ctypes python module and discovered that both c_void_p and c_char_p are subclasses of ctypes._SimpleCData, hence one is not a subclass of the other in any way.

Also, do I misunderstand it that this problem also applies to the input arguments, or is this only the deal for the returned values?

Community
  • 1
  • 1
astafan8
  • 143
  • 2
  • 7

1 Answers1

2

... do I misunderstand it that this problem also applies to the input arguments, or is this only the deal for the returned values?

It does not apply to input arguments, as the sequence below shows:

>>> dll=CDLL('msvcrt')
>>> dll.printf.argtypes = c_char_p,
>>> dll.printf(b'abc') # Note: 3 is the return value of printf
abc3
>>> class LPCSTR(c_char_p): # define a subtype
...  pass
...
>>> dll.printf.argtypes = LPCSTR,
>>> dll.printf(b'abc')
abc3

Conversions still work for input subtypes; however, output subtypes work differently as your doc quote mentioned:

>>> dll.ctime.argtypes = c_void_p,
>>> dll.ctime.restype = c_char_p
>>> dll.ctime(byref(c_int(5)))
b'Wed Dec 31 16:00:05 1969\n'
>>> dll.ctime.restype = LPCSTR
>>> dll.ctime(byref(c_int(5))) # not converted to Python byte string
LPCSTR(1989707373328)
>>> x = dll.ctime(byref(c_int(5))) # but can get the value
>>> x.value
b'Wed Dec 31 16:00:05 1969\n'
Mark Tolonen
  • 166,664
  • 26
  • 169
  • 251