@@ -261,11 +261,109 @@ def repl(m):
261261 for name in invalid_ctypes :
262262 data = re .sub (rf"\bctypes\.{ name } \b" , name , data )
263263
264+ data = self .disambiguate_anonymous_field_names (data )
265+
264266 with open (self .output_file , "w" ) as f :
265267 f .write (data )
266268
267269 self .log (f"Saved final output to { self .output_file } " )
268270
271+ def disambiguate_anonymous_field_names (self , data ):
272+ """Make clang2py's generic '_N' field names globally unique per struct/union.
273+
274+ clang2py names both (a) unnamed anonymous struct/union members that go
275+ into `_anonymous_`, and (b) other unnamed/reserved members, using the
276+ same per-struct sequential scheme ('_0', '_1', ...). ctypes flattens
277+ anonymous members by promoting their field names onto the parent class,
278+ so if a struct has an anonymous member (say '_0') whose type itself has
279+ a field also named '_1', and that same struct has ANOTHER anonymous
280+ member named '_1', the promoted name collides with the parent's own
281+ field key. ctypes then fails with a cryptic
282+ "type object 'c_ulong' has no attribute '_fields_'" (or similar) when
283+ `_fields_` is assigned. Prefixing every generic '_N' key with its
284+ owning struct/union name keeps these names unique across the whole
285+ file, so promoted names can never collide with a sibling's own key.
286+ """
287+ self .log ("Disambiguating generic anonymous-field names..." )
288+
289+ def sanitize (name ):
290+ return re .sub (r"[^0-9A-Za-z_]" , "_" , name )
291+
292+ lines = data .split ("\n " )
293+ class_re = re .compile (r"^class\s+(\w+)\(" )
294+ stmt_fields_re = re .compile (r"^(\w+)\._fields_\s*=\s*\[\s*$" )
295+ inline_fields_re = re .compile (r"^\s+_fields_\s*=\s*\[\s*$" )
296+ anon_stmt_re = re .compile (r"^(\w+)\._anonymous_\s*=\s*\((.*)\)\s*$" )
297+ anon_inline_re = re .compile (r"^(\s+)_anonymous_\s*=\s*\((.*)\)\s*$" )
298+ key_re = re .compile (r"(\('_)([0-9]+)(',)" )
299+
300+ last_class = None
301+ out = []
302+ i = 0
303+ n = len (lines )
304+ renamed_count = 0
305+ while i < n :
306+ line = lines [i ]
307+
308+ m_class = class_re .match (line )
309+ if m_class :
310+ last_class = m_class .group (1 )
311+ out .append (line )
312+ i += 1
313+ continue
314+
315+ m_anon = anon_stmt_re .match (line )
316+ if m_anon :
317+ cls , body = m_anon .group (1 ), m_anon .group (2 )
318+ prefix = sanitize (cls )
319+ new_body , cnt = re .subn (
320+ r"'_([0-9]+)'" , lambda mm : f"'_{ prefix } _{ mm .group (1 )} '" , body
321+ )
322+ renamed_count += cnt
323+ out .append (f"{ cls } ._anonymous_ = ({ new_body } )" )
324+ i += 1
325+ continue
326+
327+ m_anon_inline = anon_inline_re .match (line )
328+ if m_anon_inline and last_class :
329+ indent , body = m_anon_inline .group (1 ), m_anon_inline .group (2 )
330+ prefix = sanitize (last_class )
331+ new_body , cnt = re .subn (
332+ r"'_([0-9]+)'" , lambda mm : f"'_{ prefix } _{ mm .group (1 )} '" , body
333+ )
334+ renamed_count += cnt
335+ out .append (f"{ indent } _anonymous_ = ({ new_body } )" )
336+ i += 1
337+ continue
338+
339+ m_stmt = stmt_fields_re .match (line )
340+ m_inline = inline_fields_re .match (line ) if not m_stmt else None
341+ if m_stmt or m_inline :
342+ cls = m_stmt .group (1 ) if m_stmt else last_class
343+ prefix = sanitize (cls ) if cls else None
344+ out .append (line )
345+ i += 1
346+ while i < n and lines [i ].strip () != "]" :
347+ fl = lines [i ]
348+ if prefix :
349+ fl , cnt = key_re .subn (
350+ lambda mm : f"{ mm .group (1 )} { prefix } _{ mm .group (2 )} { mm .group (3 )} " ,
351+ fl ,
352+ )
353+ renamed_count += cnt
354+ out .append (fl )
355+ i += 1
356+ if i < n :
357+ out .append (lines [i ]) # closing ']'
358+ i += 1
359+ continue
360+
361+ out .append (line )
362+ i += 1
363+
364+ self .log (f"Renamed { renamed_count } generic field keys" )
365+ return "\n " .join (out )
366+
269367 def cleanup (self ):
270368 """Remove temporary files if not keeping them."""
271369 if not self .keep_intermediate and self .temp_dir != "." :
0 commit comments