Why does a forward declaration of a struct causes errors in this case?

19 views
Skip to first unread message

shakfu

unread,
Jul 3, 2025, 3:47:41 PMJul 3
to cython-users
I ran into a weird problem recently and I could understand the source of the error until it turned out to be a forward declaration int pxd of `ctypedef struct t_context: pass`

The illustrative code to demonstrate this error is contained in the following 'demo.pyx' file. If one uncomments forward declaration (which is in the red color), it will trigger error on compilation.

from libc.stdlib cimport malloc, free
from libc.string cimport memset

cdef extern from *:
    """
    typedef struct t_context t_context;

    typedef struct t_style {
        int padding;
    } t_style;

    struct t_context {
        int frame;
        t_style* style;
    };
    """

    # HERE: Uncommenting next line triggers 
can't convert errors
    # ctypedef struct t_context: pass

    ctypedef struct t_style:
        int padding

    ctypedef struct t_context:
        int frame
        t_style* style

cdef class Style:
    cdef t_style* ptr
    cdef bint owner

    def __cinit__(self):
        self.ptr = NULL
        self.owner = False

    def __dealloc__(self):
        # De-allocate if not null and flag is set
        if self.ptr is not NULL and self.owner is True:
            free(self.ptr)
            self.ptr = NULL

    def __init__(self):
        raise TypeError("This class cannot be instantiated directly.")

    @staticmethod
    cdef Style from_ptr(t_style* ptr, bint owner=False):
        cdef Style wrapper = Style.__new__(Style)
        wrapper.ptr = ptr
        wrapper.owner = owner
        return wrapper

    @staticmethod
    cdef Style new():
        cdef t_style* _ptr = <t_style*>malloc(sizeof(t_style))
        if _ptr is NULL:
            raise MemoryError("Failed to allocate Style")
        memset(_ptr, 0, sizeof(t_style))
        return Style.from_ptr(_ptr, owner=True)
   
    @property
    def padding(self) -> int:
        return self.ptr.padding
   
    @padding.setter
    def padding(self, int value):
        self.ptr.padding = value

cdef class Context:
    cdef t_context* ptr
    cdef bint owner

    def __cinit__(self):
        self.ptr = NULL
        self.owner = False

    def __dealloc__(self):
        # De-allocate if not null and flag is set
        if self.ptr is not NULL and self.owner is True:
            free(self.ptr.style)
            free(self.ptr)
            self.ptr = NULL

    def __init__(self):
        self.ptr = <t_context*>malloc(sizeof(t_context))
        if self.ptr is NULL:
            raise MemoryError("Failed to allocate Context")
        self.ptr.frame = 0
        self.ptr.style = <t_style*>malloc(sizeof(t_style))
        if self.ptr.style is NULL:
            raise MemoryError("Failed to allocate context.style")
        self.owner = True            

    @staticmethod
    cdef Context from_ptr(t_context* ptr, bint owner=False):
        cdef Context wrapper = Context.__new__(Context)
        wrapper.ptr = ptr
        wrapper.owner = owner
        return wrapper

    @staticmethod
    cdef Context new():
        cdef t_context* _ptr = <t_context*>malloc(sizeof(t_context))
        if _ptr is NULL:
            raise MemoryError("Failed to allocate Context")
        return Context.from_ptr(_ptr, owner=True)

    @property
    def style(self) -> Style:
        return Style.from_ptr(<t_style*>self.ptr.style)

    @property
    def style(self) -> Style:
        return Style.from_ptr(<t_style*>self.ptr.style)


Why is this triggering such errors as I followed the forward declaration the orginal header?

da-woods

unread,
Jul 3, 2025, 3:53:34 PMJul 3
to cython...@googlegroups.com

typedef struct t_context t_context;

I'm fairly sure this isn't valid C and forward declarations just don't work for typedef struct.  Given that, I wouldn't expect it to work in Cython either (although I don't know why you get any specific errors)

--

---
You received this message because you are subscribed to the Google Groups "cython-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cython-users...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/cython-users/3953b77d-ad39-468d-8344-40bd223facd0n%40googlegroups.com.

shakfu

unread,
Jul 4, 2025, 1:08:06 AMJul 4
to cython-users

D Woods wrote:

typedef struct t_context t_context;

I'm fairly sure this isn't valid C and forward declarations just don't work for typedef struct.  Given that, I wouldn't expect it to work in Cython either (although I don't know why you get any specific errors)


Strange, there were are no compiler errors or warnings when building the library to be wrapped (which is microui (1100 lines of c). FYI the offending line is here

I hate to do this, but I asked Claude if the offending line is valid c code and got the following:

--start
Yes, typedef struct t_context t_context; is valid C code. This is called a forward declaration or incomplete type declaration.

Here's what it does:

  1. Declares that t_context is a struct type
  2. Creates a typedef alias so you can use t_context instead of struct t_context
  3. Doesn't define the actual structure members yet

This is commonly used when:

  • You need to reference the type before fully defining it (like for self-referential structures)
  • You want to keep the full definition in a separate header file
  • You're creating opaque types where the internal structure is hidden

Example usage:

c
// Forward declaration typedef struct t_context t_context; // Now you can use t_context in function declarations t_context* create_context(void); void destroy_context(t_context* ctx); // Later, you'd provide the full definition struct t_context { int value; char* name; t_context* next; // Self-reference is now possible };

The compiler accepts this because it only needs to know that t_context exists as a struct type to handle pointers to it, even without knowing its size or members yet."

--end

I'm not claiming that what Claude is reporting is true, but it If this is valid c then I'll report this issue as a cython bug, 

S

 

da-woods

unread,
Jul 4, 2025, 2:52:41 AMJul 4
to cython...@googlegroups.com

Sorry - you're right. What you can't do is the forward declaration of a typedef for an unnamed struct e.g. `typedef struct t_context;` (where the full declaration would be `typedef struct { ... } t_context;`

Unfortunately I think it's this that you're trying to do in Cython.

Do you actually need the forward declaration in Cython? You usually don't I think.

--

---
You received this message because you are subscribed to the Google Groups "cython-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cython-users...@googlegroups.com.

Shakeeb Alireza

unread,
Jul 4, 2025, 5:51:19 AMJul 4
to cython...@googlegroups.com
da-woods wrote:

Sorry - you're right. What you can't do is the forward declaration of a typedef for an unnamed struct e.g. `typedef struct t_context;` (where the full declaration would be `typedef struct { ... } t_context;`

Unfortunately I think it's this that you're trying to do in Cython.

Well `typedef struct t_context t_context` translated to `ctypedef struct t_context: pass` is accepted by cython without error whereas `ctypedef struct t_context t_context: pass` is indeed a syntax error.

Do you actually need the forward declaration in Cython? You usually don't I think.

You don't need it, for sure. Everything works as it should if the "ctypedef struct t_context: pass"  line is removed. 

My mistake, in retrospect, was to translate (somewhat automatically) this c forward declaration to cython. Because this caused other unrelated things to break during compilation it was a real pain to debug and took me a while to discover the problem. I'm just highlighting it here in case anyone else has a similar issue.

S




 

da-woods

unread,
Jul 4, 2025, 4:20:42 PMJul 4
to cython...@googlegroups.com

I've had a bit more of a look at it.

I think the problem is that for Cython `ctypedef struct t_context` (no colon, no pass) is a forward declaration while `ctypedef struct t_context: pass` is a declaration of an empty struct. So  it conflicts with your later full declaration.

The error message "Cannot convert 'S *' to Python object" is pretty unhelpful I agree. That's definitely worth reporting as a bug and we might be able to improve it.

--

---
You received this message because you are subscribed to the Google Groups "cython-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cython-users...@googlegroups.com.

shakfu

unread,
Jul 5, 2025, 2:14:49 AMJul 5
to cython-users
D Woods wrote:

I've had a bit more of a look at it.

Thanks! 

I think the problem is that for Cython `ctypedef struct t_context` (no colon, no pass) is a forward declaration while `ctypedef struct t_context: pass` is a declaration of an empty struct. So  it conflicts with your later full declaration.

That's exactly right, and indeed `ctypedef struct t_contextt` does not cause any errors!

Note that the Visual Code cython extension treats the correct use as an error. 

The error message "Cannot convert 'S *' to Python object" is pretty unhelpful I agree. That's definitely worth reporting as a bug and we might be able to improve it.

I'll report this as a bug, and also highlight this issue with the visual studio Cython extension. Thanks very much for your help in clarifying this mystery!

S
 
Reply all
Reply to author
Forward
0 new messages