Skip to content

make_type raises KeyError(b'd') for unregistered struct return types on Python 3 #148

Description

@ElliotGarbus

Summary

Calling any method that returns an unregistered struct with double fields
(e.g. UIEdgeInsets) raises KeyError(b'd') on Python 3. The root cause
is a bytes/str mismatch introduced by the Python 2→3 migration: the
types dict in objc_py_types.py has string keys ('d') but the
Cython layer passes bytes when looking up struct field types (b'd').

Reproducer

from pyobjus import autoclass

UIApplication = autoclass("UIApplication")
app = UIApplication.sharedApplication()
win = app.windows.firstObject()
insets = win.safeAreaInsets()   # KeyError(b'd')

Traceback (innermost relevant frame):

File ".../pyobjus/objc_py_types.py", line 144, in make_type
    field_list.append((field_name, types[_type]))
KeyError: b'd'

Root cause

objc_py_types.py defines the type-encoding map with string keys:

types = {
    ...
    'f': ctypes.c_float,
    'd': ctypes.c_double,   # str key
    ...
}

The Cython layer passes bytes when iterating struct field types. On Python 2
'd' == b'd'; on Python 3 they are different objects, so the lookup fails.

The common UIKit struct types (CGRect, CGPoint, CGSize, NSRange) are
pre-registered in Factory.init, so find_object returns them from the
registry before ever calling make_type. Any struct that is not
pre-registered — including UIEdgeInsets — falls through to the broken path.

Fix

In objc_py_types.py, decode _type before the lookup in make_type:

# line ~144 in make_type
_key = _type.decode("ascii") if isinstance(_type, bytes) else _type
field_list.append((field_name, types[_key]))

Alternatively, change the types dict to use bytes keys throughout to
match what the Cython layer actually passes.

Workaround

def get_safe_area() -> dict[str, float]:
    try:
        from pyobjus import autoclass

        # Derive insets from safeAreaLayoutGuide.layoutFrame vs window.frame.
        # Both return CGRect, which pyobjus pre-registers as NSRect — this
        # avoids the UIEdgeInsets struct-return path that triggers a pyobjus
        # 1.2.x Python-3 bug (KeyError on unregistered struct field types).
        app = autoclass("UIApplication").sharedApplication()
        win = app.windows.firstObject()
        guide = win.safeAreaLayoutGuide().layoutFrame  # ObjcProperty → NSRect
        frame = win.frame()  # ObjcMethod → NSRect
        return {
            "top": float(guide.origin.y),
            "left": float(guide.origin.x),
            "bottom": float(frame.size.height - (guide.origin.y + guide.size.height)),
            "right": float(frame.size.width - (guide.origin.x + guide.size.width)),
        }
    except Exception:
        return {"top": 0.0, "bottom": 0.0, "left": 0.0, "right": 0.0}

Environment

  • pyobjus 1.2.4
  • Python 3.15 (iOS wheel, cp315) Self built and locally hosted.
  • iOS 16+ simulator and device (arm64)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions