rodrigocam / RustPython

A Python Interpreter written in Rust
https://rustpython.github.io/demo/
MIT License
1 stars 0 forks source link

[RFC] Ctypes I: Overview of other implementations (mainly CPython) #1

Open darleybarreto opened 3 years ago

darleybarreto commented 3 years ago

This is the #1 (first) tracking issue w.r.t the implementation of _ctypes. The main goal is to discuss the core ideas of the some interpreters such as CPython, PyPy and IronPython.

darleybarreto commented 3 years ago

CPython Ctypes

Important types

typedef struct tagPyCArgObject PyCArgObject;
typedef struct tagCDataObject CDataObject;
typedef PyObject *(* GETFUNC)(void *, Py_ssize_t size);
typedef PyObject *(* SETFUNC)(void *, PyObject *value, Py_ssize_t size);
typedef PyCArgObject *(* PARAMFUNC)(CDataObject *obj);

struct tagCDataObject {
    PyObject_HEAD
    char *b_ptr;                /* pointer to memory block */
    int  b_needsfree;           /* need _we_ free the memory? */
    CDataObject *b_base;        /* pointer to base object or NULL */
    Py_ssize_t b_size;          /* size of memory block in bytes */
    Py_ssize_t b_length;        /* number of references we need */
    Py_ssize_t b_index;         /* index of this object into base's
                               b_object list */
    PyObject *b_objects;        /* dictionary of references we need to keep, or Py_None */
    union value b_value;
};

typedef struct {
    PyObject_VAR_HEAD
    ffi_closure *pcl_write; /* the C callable, writeable */
    void *pcl_exec;         /* the C callable, executable */
    ffi_cif cif;
    int flags;
    PyObject *converters;
    PyObject *callable;
    PyObject *restype;
    SETFUNC setfunc;
    ffi_type *ffi_restype;
    ffi_type *atypes[1];
} CThunkObject;
extern PyTypeObject PyCThunk_Type;
#define CThunk_CheckExact(v)        Py_IS_TYPE(v, &PyCThunk_Type)

typedef struct {
    /* First part identical to tagCDataObject */
    PyObject_HEAD
    char *b_ptr;                /* pointer to memory block */
    int  b_needsfree;           /* need _we_ free the memory? */
    CDataObject *b_base;        /* pointer to base object or NULL */
    Py_ssize_t b_size;          /* size of memory block in bytes */
    Py_ssize_t b_length;        /* number of references we need */
    Py_ssize_t b_index;         /* index of this object into base's
                               b_object list */
    PyObject *b_objects;        /* list of references we need to keep */
    union value b_value;
    /* end of tagCDataObject, additional fields follow */

    CThunkObject *thunk;
    PyObject *callable;

    /* These two fields will override the ones in the type's stgdict if
       they are set */
    PyObject *converters;
    PyObject *argtypes;
    PyObject *restype;
    PyObject *checker;
    PyObject *errcheck;
#ifdef MS_WIN32
    int index;
    GUID *iid;
#endif
    PyObject *paramflags;
} PyCFuncPtrObject;

/* a table entry describing a predefined ctypes type */
struct fielddesc {
    char code;
    SETFUNC setfunc;
    GETFUNC getfunc;
    ffi_type *pffi_type; /* always statically allocated */
    SETFUNC setfunc_swapped;
    GETFUNC getfunc_swapped;
};

typedef struct {
    PyObject_HEAD
    Py_ssize_t offset;
    Py_ssize_t size;
    Py_ssize_t index;                   /* Index into CDataObject's
                                       object array */
    PyObject *proto;                    /* a type or NULL */
    GETFUNC getfunc;                    /* getter function if proto is NULL */
    SETFUNC setfunc;                    /* setter function if proto is NULL */
    int anonymous;
} CFieldObject;

/* A subclass of PyDictObject, used as the instance dictionary of ctypes
   metatypes */
typedef struct {
    PyDictObject dict;          /* first part identical to PyDictObject */
    Py_ssize_t size;            /* number of bytes */
    Py_ssize_t align;           /* alignment requirements */
    Py_ssize_t length;          /* number of fields */
    ffi_type ffi_type_pointer;
    PyObject *proto;            /* Only for Pointer/ArrayObject */
    SETFUNC setfunc;            /* Only for simple objects */
    GETFUNC getfunc;            /* Only for simple objects */
    PARAMFUNC paramfunc;

    /* Following fields only used by PyCFuncPtrType_Type instances */
    PyObject *argtypes;         /* tuple of CDataObjects */
    PyObject *converters;       /* tuple([t.from_param for t in argtypes]) */
    PyObject *restype;          /* CDataObject or NULL */
    PyObject *checker;
    int flags;                  /* calling convention and such */

    /* pep3118 fields, pointers neeed PyMem_Free */
    char *format;
    int ndim;
    Py_ssize_t *shape;
/*      Py_ssize_t *strides;    */ /* unused in ctypes */
/*      Py_ssize_t *suboffsets; */ /* unused in ctypes */

} StgDictObject;

struct tagPyCArgObject {
    PyObject_HEAD
    ffi_type *pffi_type;
    char tag;
    union {
        char c;
        char b;
        short h;
        int i;
        long l;
        long long q;
        long double D;
        double d;
        float f;
        void *p;
    } value;
    PyObject *obj;
    Py_ssize_t size; /* for the 'V' tag */
};

I didn't give a deeper look into types like StgDictObject and tagPyCArgObject.

Metaclasses

PyTypeObject PyCArrayType_Type;
PyTypeObject PyCPointerType_Type;
PyTypeObject PyCFuncPtrType_Type;
PyTypeObject PyCStructType_Type;
PyTypeObject UnionType_Type;
PyTypeObject PyCSimpleType_Type;

All these metaclasses but PyCPointerType_Type and PyCSimpleType_Type implements CDataType_methods:

static PyMethodDef CDataType_methods[] = {
    { "from_param", CDataType_from_param, METH_O, from_param_doc },
    { "from_address", CDataType_from_address, METH_O, from_address_doc },
    { "from_buffer", CDataType_from_buffer, METH_VARARGS, from_buffer_doc, },
    { "from_buffer_copy", CDataType_from_buffer_copy, METH_VARARGS, from_buffer_copy_doc, },
    { "in_dll", CDataType_in_dll, METH_VARARGS, in_dll_doc },
};

PyCSimpleType_Type uses PyCSimpleType_methods:

static PyMethodDef PyCSimpleType_methods[] = {
    { "from_param", PyCSimpleType_from_param, METH_O, from_param_doc },
    { "from_address", CDataType_from_address, METH_O, from_address_doc },
    { "from_buffer", CDataType_from_buffer, METH_VARARGS, from_buffer_doc, },
    { "from_buffer_copy", CDataType_from_buffer_copy, METH_VARARGS, from_buffer_copy_doc, },
    { "in_dll", CDataType_in_dll, METH_VARARGS, in_dll_doc},
};

PyCPointerType_Type uses PyCPointerType_methods:

static PyMethodDef PyCPointerType_methods[] = {
    { "from_address", CDataType_from_address, METH_O, from_address_doc },
    { "from_buffer", CDataType_from_buffer, METH_VARARGS, from_buffer_doc, },
    { "from_buffer_copy", CDataType_from_buffer_copy, METH_VARARGS, from_buffer_copy_doc, },
    { "in_dll", CDataType_in_dll, METH_VARARGS, in_dll_doc},
    { "from_param", (PyCFunction)PyCPointerType_from_param, METH_O, from_param_doc},
    { "set_type", (PyCFunction)PyCPointerType_set_type, METH_O },
};

Al the above classes use CDataType_as_sequence as tp_as_sequence (sequence protocol):

static PySequenceMethods CDataType_as_sequence = {
    0,                          /* inquiry sq_length; */
    0,                          /* binaryfunc sq_concat; */
    CDataType_repeat,           /* intargfunc sq_repeat; */
    0,                          /* intargfunc sq_item; */
    0,                          /* intintargfunc sq_slice; */
    0,                          /* intobjargproc sq_ass_item; */
    0,                          /* intintobjargproc sq_ass_slice; */
    0,                          /* objobjproc sq_contains; */

    0,                          /* binaryfunc sq_inplace_concat; */
    0,                          /* intargfunc sq_inplace_repeat; */
};

Classes

PyTypeObject PyCData_Type;
PyTypeObject PyCArray_Type;      // subclass of PyCArrayType_Type
PyTypeObject PyCPointer_Type;    // subclass of PyCPointerType_Type
PyTypeObject PyCFuncPtr_Type;    // subclass of PyCFuncPtrType_Type
PyTypeObject Struct_Type;        // subclass of PyCStructType_Type
PyTypeObject Union_Type;         // subclass of UnionType_Type
PyTypeObject Simple_Type;        // subclass of PyCSimpleType_Type

All the above classes but PyCFuncPtr_Type have tp_basicsize as sizeof(CDataObject), PyCFuncPtr_Type has sizeof(PyCFuncPtrObject). They're all subclass of PyCData_Type.

Each class has some particular methods, which needs to be explored individually. Some of the shared methods, for instance Struct_Type, Union_Type, PyCArray_Type and Simple_Type have the same new. Another example is that they all have the same method on tp_as_buffer slot.

PyPy implements PyCData_Type as a metaclass, if I'm not mistaken. CDataObject is it's subclass, all the above classes implements CDataObject and their respective superclasses.

darleybarreto commented 3 years ago

Interesting comments

/**
Name                    methods, members, getsets
==============================================================================
PyCStructType_Type              __new__(), from_address(), __mul__(), from_param()
UnionType_Type          __new__(), from_address(), __mul__(), from_param()
PyCPointerType_Type     __new__(), from_address(), __mul__(), from_param(), set_type()
PyCArrayType_Type               __new__(), from_address(), __mul__(), from_param()
PyCSimpleType_Type              __new__(), from_address(), __mul__(), from_param()
PyCData_Type
  Struct_Type           __new__(), __init__()
  PyCPointer_Type               __new__(), __init__(), _as_parameter_, contents
  PyCArray_Type         __new__(), __init__(), _as_parameter_, __get/setitem__(), __len__()
  Simple_Type           __new__(), __init__(), _as_parameter_
PyCField_Type
PyCStgDict_Type
==============================================================================
**/

Metaclass `*Type_Type` methods
------------------------------------

It has some similarity to the byref() construct compared to pointer()
from_address(addr)
    - construct an instance from a given memory block (sharing this memory block)
from_param(obj)
    - typecheck and convert a Python object into a C function call parameter
      The result may be an instance of the type, or an integer or tuple
      (typecode, value[, obj])

PyCData_Type subclasses instance methods/properties
---------------------------------------------------

_as_parameter_
    - convert self into a C function call parameter
      This is either an integer, or a 3-tuple (typecode, value, obj)
functions
---------
sizeof(cdata)
    - return the number of bytes the buffer contains
sizeof(ctype)
    - return the number of bytes the buffer of an instance would contain
byref(cdata)
addressof(cdata)
pointer(cdata)
POINTER(ctype)
bytes(cdata)
    - return the buffer contents as a sequence of bytes (which is currently a string)

PyCStructType_Type - a meta type/class.  Creating a new class using this one as
  __metaclass__ will call the constructor StructUnionType_new.  It replaces the
  tp_dict member with a new instance of StgDict, and initializes the C
  accessible fields somehow.

The PyCPointerType_Type metaclass must ensure that the subclass of Pointer can be
created. It must check for a _type_ attribute in the class. Since are no
runtime created properties, a CField is probably *not* needed ?

class IntPointer(Pointer):
    _type_ = "i"

The PyCPointer_Type provides the functionality: a contents method/property, a
size property/method, and the sequence protocol.

PyCArrayType_new ensures that the new Array subclass created has a _length_
  attribute, and a _type_ attribute.

PyCSimpleType_new ensures that the new Simple_Type subclass created has a valid
_type_ attribute.

default __ctypes_from_outparam__ method returns self
rodrigocam commented 3 years ago

Function to load shared libraries - unix

static PyObject *py_dl_open(PyObject *self, PyObject *args)
{
    PyObject *name, *name2;
    const char *name_str;
    void * handle;
#if HAVE_DECL_RTLD_LOCAL
    int mode = RTLD_NOW | RTLD_LOCAL;
#else
    /* cygwin doesn't define RTLD_LOCAL */
    int mode = RTLD_NOW;
#endif
    if (!PyArg_ParseTuple(args, "O|i:dlopen", &name, &mode))
        return NULL;
    mode |= RTLD_NOW;
    if (name != Py_None) {
        if (PyUnicode_FSConverter(name, &name2) == 0)
            return NULL;
        name_str = PyBytes_AS_STRING(name2);
    } else {
        name_str = NULL;
        name2 = NULL;
    }
    if (PySys_Audit("ctypes.dlopen", "O", name) < 0) {
        return NULL;
    }
    handle = ctypes_dlopen(name_str, mode);
    Py_XDECREF(name2);
    if (!handle) {
        const char *errmsg = ctypes_dlerror();
        if (!errmsg)
            errmsg = "dlopen() error";
        PyErr_SetString(PyExc_OSError,
                               errmsg);
        return NULL;
    }
    return PyLong_FromVoidPtr(handle);
}

Function to load shared libraries - windows

static const char load_library_doc[] =
"LoadLibrary(name, load_flags) -> handle\n\
\n\
Load an executable (usually a DLL), and return a handle to it.\n\
The handle may be used to locate exported functions in this\n\
module. load_flags are as defined for LoadLibraryEx in the\n\
Windows API.\n";
static PyObject *load_library(PyObject *self, PyObject *args)
{
    PyObject *nameobj;
    int load_flags = 0;
    HMODULE hMod;
    DWORD err;

    if (!PyArg_ParseTuple(args, "U|i:LoadLibrary", &nameobj, &load_flags))
        return NULL;

    if (PySys_Audit("ctypes.dlopen", "O", nameobj) < 0) {
        return NULL;
    }

    WCHAR *name = PyUnicode_AsWideCharString(nameobj, NULL);
    if (!name)
        return NULL;

    Py_BEGIN_ALLOW_THREADS
    /* bpo-36085: Limit DLL search directories to avoid pre-loading
     * attacks and enable use of the AddDllDirectory function.
     */
    hMod = LoadLibraryExW(name, NULL, (DWORD)load_flags);
    err = hMod ? 0 : GetLastError();
    Py_END_ALLOW_THREADS

    PyMem_Free(name);
    if (err == ERROR_MOD_NOT_FOUND) {
        PyErr_Format(PyExc_FileNotFoundError,
                     ("Could not find module '%.500S' (or one of its "
                      "dependencies). Try using the full path with "
                      "constructor syntax."),
                     nameobj);
        return NULL;
    } else if (err) {
        return PyErr_SetFromWindowsErr(err);
    }
#ifdef _WIN64
    return PyLong_FromVoidPtr(hMod);
#else
    return Py_BuildValue("i", hMod);
#endif
}
rodrigocam commented 3 years ago

Function to get symbol addr from lib

static PyObject *py_dl_sym(PyObject *self, PyObject *args)
{
    char *name;
    void *handle;
    void *ptr;

    if (!PyArg_ParseTuple(args, "O&s:dlsym",
                          &_parse_voidp, &handle, &name))
        return NULL;
    if (PySys_Audit("ctypes.dlsym/handle", "O", args) < 0) {
        return NULL;
    }
    ptr = ctypes_dlsym((void*)handle, name);
    if (!ptr) {
        PyErr_SetString(PyExc_OSError,
                               ctypes_dlerror());
        return NULL;
    }
    return PyLong_FromVoidPtr(ptr);
}
rodrigocam commented 3 years ago

Function to call a function from shared library

/*
 * Only for debugging so far: So that we can call CFunction instances
 *
 * XXX Needs to accept more arguments: flags, argtypes, restype
 */
static PyObject *
call_function(PyObject *self, PyObject *args)
{
    void *func;
    PyObject *arguments;
    PyObject *result;

    if (!PyArg_ParseTuple(args,
                          "O&O!",
                          &_parse_voidp, &func,
                          &PyTuple_Type, &arguments))
        return NULL;
    if (PySys_Audit("ctypes.call_function", "nO",
                    (Py_ssize_t)func, arguments) < 0) {
        return NULL;
    }

    result =  _ctypes_callproc((PPROC)func,
                        arguments,
#ifdef MS_WIN32
                        NULL,
                        NULL,
#endif
                        0, /* flags */
                NULL, /* self->argtypes */
                NULL, /* self->restype */
                NULL); /* checker */
    return result;
}

Actually calling the function

/*
 * Requirements, must be ensured by the caller:
 * - argtuple is tuple of arguments
 * - argtypes is either NULL, or a tuple of the same size as argtuple
 *
 * - XXX various requirements for restype, not yet collected
 */
PyObject *_ctypes_callproc(PPROC pProc,
                    PyObject *argtuple,
#ifdef MS_WIN32
                    IUnknown *pIunk,
                    GUID *iid,
#endif
                    int flags,
                    PyObject *argtypes, /* misleading name: This is a tuple of
                                           methods, not types: the .from_param
                                           class methods of the types */
            PyObject *restype,
            PyObject *checker)
{
    Py_ssize_t i, n, argcount, argtype_count;
    void *resbuf;
    struct argument *args, *pa;
    ffi_type **atypes;
    ffi_type *rtype;
    void **avalues;
    PyObject *retval = NULL;

    n = argcount = PyTuple_GET_SIZE(argtuple);
#ifdef MS_WIN32
    /* an optional COM object this pointer */
    if (pIunk)
        ++argcount;
#endif

    if (argcount > CTYPES_MAX_ARGCOUNT)
    {
        PyErr_Format(PyExc_ArgError, "too many arguments (%zi), maximum is %i",
                     argcount, CTYPES_MAX_ARGCOUNT);
        return NULL;
    }

    args = (struct argument *)alloca(sizeof(struct argument) * argcount);
    if (!args) {
        PyErr_NoMemory();
        return NULL;
    }
    memset(args, 0, sizeof(struct argument) * argcount);
    argtype_count = argtypes ? PyTuple_GET_SIZE(argtypes) : 0;
#ifdef MS_WIN32
    if (pIunk) {
        args[0].ffi_type = &ffi_type_pointer;
        args[0].value.p = pIunk;
        pa = &args[1];
    } else
#endif
        pa = &args[0];

    /* Convert the arguments */
    for (i = 0; i < n; ++i, ++pa) {
        PyObject *converter;
        PyObject *arg;
        int err;

        arg = PyTuple_GET_ITEM(argtuple, i);            /* borrowed ref */
        /* For cdecl functions, we allow more actual arguments
           than the length of the argtypes tuple.
           This is checked in _ctypes::PyCFuncPtr_Call
        */
        if (argtypes && argtype_count > i) {
            PyObject *v;
            converter = PyTuple_GET_ITEM(argtypes, i);
            v = PyObject_CallOneArg(converter, arg);
            if (v == NULL) {
                _ctypes_extend_error(PyExc_ArgError, "argument %zd: ", i+1);
                goto cleanup;
            }

            err = ConvParam(v, i+1, pa);
            Py_DECREF(v);
            if (-1 == err) {
                _ctypes_extend_error(PyExc_ArgError, "argument %zd: ", i+1);
                goto cleanup;
            }
        } else {
            err = ConvParam(arg, i+1, pa);
            if (-1 == err) {
                _ctypes_extend_error(PyExc_ArgError, "argument %zd: ", i+1);
                goto cleanup; /* leaking ? */
            }
        }
    }

    rtype = _ctypes_get_ffi_type(restype);
    resbuf = alloca(max(rtype->size, sizeof(ffi_arg)));

#ifdef _Py_MEMORY_SANITIZER
    /* ffi_call actually initializes resbuf, but from asm, which
     * MemorySanitizer can't detect. Avoid false positives from MSan. */
    if (resbuf != NULL) {
        __msan_unpoison(resbuf, max(rtype->size, sizeof(ffi_arg)));
    }
#endif
    avalues = (void **)alloca(sizeof(void *) * argcount);
    atypes = (ffi_type **)alloca(sizeof(ffi_type *) * argcount);
    if (!resbuf || !avalues || !atypes) {
        PyErr_NoMemory();
        goto cleanup;
    }
    for (i = 0; i < argcount; ++i) {
        atypes[i] = args[i].ffi_type;
#ifdef CTYPES_PASS_BY_REF_HACK
        size_t size = atypes[i]->size;
        if (IS_PASS_BY_REF(size)) {
            void *tmp = alloca(size);
            if (atypes[i]->type == FFI_TYPE_STRUCT)
                memcpy(tmp, args[i].value.p, size);
            else
                memcpy(tmp, (void*)&args[i].value, size);

            avalues[i] = tmp;
        }
        else
#endif
        if (atypes[i]->type == FFI_TYPE_STRUCT)
            avalues[i] = (void *)args[i].value.p;
        else
            avalues[i] = (void *)&args[i].value;
    }

    if (-1 == _call_function_pointer(flags, pProc, avalues, atypes,
                                     rtype, resbuf,
                                     Py_SAFE_DOWNCAST(argcount,
                                                      Py_ssize_t,
                                                      int)))
        goto cleanup;

#ifdef WORDS_BIGENDIAN
    /* libffi returns the result in a buffer with sizeof(ffi_arg). This
       causes problems on big endian machines, since the result buffer
       address cannot simply be used as result pointer, instead we must
       adjust the pointer value:
     */
    /*
      XXX I should find out and clarify why this is needed at all,
      especially why adjusting for ffi_type_float must be avoided on
      64-bit platforms.
     */
    if (rtype->type != FFI_TYPE_FLOAT
        && rtype->type != FFI_TYPE_STRUCT
        && rtype->size < sizeof(ffi_arg))
    {
        resbuf = (char *)resbuf + sizeof(ffi_arg) - rtype->size;
    }
#endif

#ifdef MS_WIN32
    if (iid && pIunk) {
        if (*(int *)resbuf & 0x80000000)
            retval = GetComError(*(HRESULT *)resbuf, iid, pIunk);
        else
            retval = PyLong_FromLong(*(int *)resbuf);
    } else if (flags & FUNCFLAG_HRESULT) {
        if (*(int *)resbuf & 0x80000000)
            retval = PyErr_SetFromWindowsErr(*(int *)resbuf);
        else
            retval = PyLong_FromLong(*(int *)resbuf);
    } else
#endif
        retval = GetResult(restype, resbuf, checker);
  cleanup:
    for (i = 0; i < argcount; ++i)
        Py_XDECREF(args[i].keep);
    return retval;
}
darleybarreto commented 3 years ago

In the snippet bellow we can see that _CFuncPtr uses _argtypes_, _restype_ and _flags_

...
class CDLL(object):
    ...
        class _FuncPtr(_CFuncPtr):
            _flags_ = flags
            _restype_ = self._func_restype_
    ...

...
_c_functype_cache = {}
def CFUNCTYPE(restype, *argtypes, **kw):
    ...
    try:
        return _c_functype_cache[(restype, argtypes, flags)]
    except KeyError:
        class CFunctionType(_CFuncPtr):
            _argtypes_ = argtypes
            _restype_ = restype
            _flags_ = flags
        _c_functype_cache[(restype, argtypes, flags)] = CFunctionType
        return CFunctionType
...

As far I can tell, these are represented as static types in the backend. They use StgDictObject, It's seems to be a tweaked version of a dict to handle callable symbols inside of the .so.

darleybarreto commented 3 years ago

PyCFuncPtr_Type is _CFuncPtr. This class has these getters and setters:

static PyGetSetDef PyCFuncPtr_getsets[] = {
    { "errcheck", (getter)PyCFuncPtr_get_errcheck, (setter)PyCFuncPtr_set_errcheck,
      "a function to check for errors", NULL },
    { "restype", (getter)PyCFuncPtr_get_restype, (setter)PyCFuncPtr_set_restype,
      "specify the result type", NULL },
    { "argtypes", (getter)PyCFuncPtr_get_argtypes,
      (setter)PyCFuncPtr_set_argtypes,
      "specify the argument types", NULL },
    { NULL, NULL }
};

Looking at argtypes, we see:

static int
PyCFuncPtr_set_argtypes(PyCFuncPtrObject *self, PyObject *ob, void *Py_UNUSED(ignored))
{
    PyObject *converters;

    if (ob == NULL || ob == Py_None) {
        Py_CLEAR(self->converters);
        Py_CLEAR(self->argtypes);
    } else {
        converters = converters_from_argtypes(ob);
        if (!converters)
            return -1;
        Py_XSETREF(self->converters, converters);
        Py_INCREF(ob);
        Py_XSETREF(self->argtypes, ob);
    }
    return 0;
}

static PyObject *
PyCFuncPtr_get_argtypes(PyCFuncPtrObject *self, void *Py_UNUSED(ignored))
{
    StgDictObject *dict;
    if (self->argtypes) {
        Py_INCREF(self->argtypes);
        return self->argtypes;
    }
    dict = PyObject_stgdict((PyObject *)self);
    assert(dict); /* Cannot be NULL for PyCFuncPtrObject instances */
    if (dict->argtypes) {
        Py_INCREF(dict->argtypes);
        return dict->argtypes;
    } else {
        Py_RETURN_NONE;
    }
}

And converters_from_argtypes:

static PyObject *
converters_from_argtypes(PyObject *ob)
{
    _Py_IDENTIFIER(from_param);
    PyObject *converters;
    Py_ssize_t i;
    Py_ssize_t nArgs;

    ob = PySequence_Tuple(ob); /* new reference */
    if (!ob) {
        PyErr_SetString(PyExc_TypeError,
                        "_argtypes_ must be a sequence of types");
        return NULL;
    }

    nArgs = PyTuple_GET_SIZE(ob);
    converters = PyTuple_New(nArgs);
    if (!converters) {
        Py_DECREF(ob);
        return NULL;
    }

    /* I have to check if this is correct. Using c_char, which has a size
       of 1, will be assumed to be pushed as only one byte!
       Aren't these promoted to integers by the C compiler and pushed as 4 bytes?
    */

    for (i = 0; i < nArgs; ++i) {
        PyObject *cnv;
        PyObject *tp = PyTuple_GET_ITEM(ob, i);

        if (_PyObject_LookupAttrId(tp, &PyId_from_param, &cnv) <= 0) {
            Py_DECREF(converters);
            Py_DECREF(ob);
            if (!PyErr_Occurred()) {
                PyErr_Format(PyExc_TypeError,
                             "item %zd in _argtypes_ has no from_param method",
                             i+1);
            }
            return NULL;
        }
        PyTuple_SET_ITEM(converters, i, cnv);
    }
    Py_DECREF(ob);
    return converters;
}

which checks if the element on _argtypes_ has from_param, if it does, than place it on converters tuple (idk the purpose of this field yet)