Skip to content

Commit

Permalink
Merge pull request #114 from dgelessus/ctypes_patch_no_private_api
Browse files Browse the repository at this point in the history
Remove use of non-public C functions in ctypes_patch
  • Loading branch information
freakboy3742 authored Jul 2, 2018
2 parents cdaca74 + dc16fc4 commit 481dd6b
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 8 deletions.
58 changes: 54 additions & 4 deletions rubicon/objc/ctypes_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import ctypes
import sys
import types
import warnings

# This module relies on the layout of a few internal Python and ctypes structures.
Expand Down Expand Up @@ -72,6 +73,15 @@ class PyDictObject(ctypes.Structure):
]


# The mappingproxyobject struct from "Objects/descrobject.c".
# This structure is not officially stable across Python versions, but its layout hasn't changed since 2001.
class mappingproxyobject(ctypes.Structure):
_fields_ = [
("ob_base", PyObject),
("mapping", ctypes.py_object),
]


# The ffi_type structure from libffi's "include/ffi.h".
# This is a forward declaration, because the structure contains pointers to itself.
class ffi_type(ctypes.Structure):
Expand Down Expand Up @@ -108,9 +118,48 @@ class StgDictObject(ctypes.Structure):
]


# The PyObject_stgdict function from "Modules/_ctypes/ctypes.h".
ctypes.pythonapi.PyType_stgdict.restype = ctypes.POINTER(StgDictObject)
ctypes.pythonapi.PyType_stgdict.argtypes = [ctypes.py_object]
def unwrap_mappingproxy(proxy):
"""Return the mapping contained in a mapping proxy object."""

if not isinstance(proxy, types.MappingProxyType):
raise TypeError(
'Expected a mapping proxy object, not {tp.__module__}.{tp.__qualname__}'
.format(tp=type(proxy))
)

return mappingproxyobject.from_address(id(proxy)).mapping


def get_stgdict_of_type(tp):
"""Return the given ctypes type's StgDict object. If the object's dict is not a StgDict, an error is raised.
This function is roughly equivalent to the PyType_stgdict function in the ctypes source code. We cannot use that
function directly, because it is not part of CPython's public C API, and thus not accessible on some systems
(see #113).
"""

if not isinstance(tp, type):
raise TypeError(
'Expected a type object, not {tptp.__module__}.{tptp.__qualname__}'
.format(tptp=type(tp))
)

stgdict = tp.__dict__
if isinstance(stgdict, types.MappingProxyType):
# If the type's __dict__ is wrapped in a mapping proxy, we need to unwrap it.
# (This appears to always be the case, so the isinstance check above could perhaps be left out, but it
# doesn't hurt to check.)
stgdict = unwrap_mappingproxy(stgdict)

# The StgDict type is not publicly exposed anywhere, so we can't use isinstance. Checking the name is the best
# we can do here.
if type(stgdict).__name__ != 'StgDict':
raise TypeError(
"The given type's dict must be a StgDict, not {tp.__module__}.{tp.__qualname__}"
.format(tp=type(stgdict))
)

return stgdict


def make_callback_returnable(ctype):
Expand All @@ -127,7 +176,8 @@ def make_callback_returnable(ctype):
return ctype

# Extract the StgDict from the ctype.
stgdict_c = ctypes.pythonapi.PyType_stgdict(ctype).contents
stgdict = get_stgdict_of_type(ctype)
stgdict_c = StgDictObject.from_address(id(stgdict))

# Ensure that there is no existing getfunc or setfunc on the stgdict.
if ctypes.cast(stgdict_c.getfunc, ctypes.c_void_p).value is not None:
Expand Down
9 changes: 5 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ setenv =

[testenv:flake8]
basepython = python3.6
# FIXME Remove the pyflakes git dependency once a new version of pyflakes (> 1.6.0) is released.
# Currently we need to use the development version of pyflakes in order to use __class__, because pyflakes <= 1.6.0
# doesn't understand __class__. This has been fixed in the development version, but not in any release yet.
# FIXME Remove the manual pyflakes dependency once flake8 depends on pyflakes >= 2.0.0.
# Currently we need to manually update pyflakes in order to use __class__, because pyflakes <= 1.6.0
# doesn't understand __class__. This has been fixed in pyflakes 2.0.0, but flake8's requirements don't allow that
# version of pyflakes yet.
deps =
flake8
git+https://github.com/PyCQA/pyflakes.git
pyflakes >= 2.0.0
commands = flake8 {posargs}

0 comments on commit 481dd6b

Please sign in to comment.