Skip to content

Follow up on #104: Addresses #63 by unpacking VT_ARRAYs of VT_RECORDs - #981

Merged
junkmd merged 2 commits into
mainfrom
up1_followup_104
Nov 5, 2026
Merged

junkmd merged 2 commits into
mainfrom
up1_followup_104

Conversation

@junkmd

@junkmd junkmd commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator

Purpose

This PR is a follow-up to #104, which has been inactive for about 10 years.

The first goal is to merge the original changes into the current main branch while keeping the original implementation as much as possible.

I want to respect the original contributor's work and intent.

Background

  • Addresses #63 by unpacking VT_ARRAYs of VT_RECORDs #104 was created about 10 years ago but was never merged.
  • Since then, main has changed. It now has new code and tests for SAFEARRAY and RECORD.
  • Because of these changes, the original PR can no longer be merged without conflict resolution.
  • However, the feature in the original PR is still useful.

Scope of This PR

  • Merge the current main branch into the original PR branch.
  • Resolve conflicts while keeping the original implementation.
  • Add an explicit byref import. The original code used it through a wildcard import.
  • Compare the original PR with this follow-up PR to check that the original changes are still present.

Comparison with the Original PR

git range-diff shows that the original commit and the commit in this follow-up PR have the same patch.

The git diff results also show that the main changes from the original PR are still present:

  • Unpacking SAFEARRAY values of VT_RECORD.
  • Copying record structures to keep their internal BSTR addresses valid.

During conflict resolution, I kept the original implementation where it conflicted with the changes in main.

> git range-diff 5bd432a..0681ffe c34d320..87d3129                                      
1:  0681ffe7 = 1:  0681ffe7 Addresses #63 by unpacking VT_ARRAYs of VT_RECORDs
> git show --remerge-diff 87d3129 --format="commit %H%nMerge: %p%nDate:   %cd%n%n%B"       
commit 87d3129bc41230d8f91c8c10db31833902fb9330
Merge: 0681ffe7 c34d3205
Date:   Tue Nov 3 11:06:35 2026 +0900

Merge branch 'main' into up1

Resolve conflicts and fix missing `byref` import (previously supported via wildcard import).


diff --git a/comtypes/_safearray.py b/comtypes/_safearray.py
remerge CONFLICT (content): Merge conflict in comtypes/_safearray.py
index 8ff896ed..01c3195c 100644
--- a/comtypes/_safearray.py
+++ b/comtypes/_safearray.py
@@ -1,6 +1,6 @@
 """SAFEARRAY api functions, data types, and constants."""
 
-from ctypes import POINTER, Structure, WinDLL, c_uint, c_ushort, c_void_p
+from ctypes import POINTER, Structure, WinDLL, byref, c_uint, c_ushort, c_void_p
 from ctypes.wintypes import DWORD, LONG, UINT, USHORT
 
 from comtypes import GUID, HRESULT
@@ -77,7 +77,6 @@ def SafeArrayGetVartype(pa):
     _SafeArrayGetVartype(pa, result)
     return result.value
 
-<<<<<<< 0681ffe7 (Addresses #63 by unpacking VT_ARRAYs of VT_RECORDs)
 def SafeArrayGetRecordInfo(pa):
     # Defined here to avoid a circular import
     from comtypes.typeinfo import IRecordInfo
@@ -89,8 +88,6 @@ def SafeArrayGetRecordInfo(pa):
     result = POINTER(IRecordInfo)()
     _SafeArrayGetRecordInfo(pa, byref(result))
     return result.value
-=======
->>>>>>> c34d3205 (Add record `SAFEARRAY` tests and a test-double COM server. (#980))
 
 SafeArrayGetElement = _oleaut32.SafeArrayGetElement
 SafeArrayGetElement.restype = HRESULT
diff --git a/comtypes/safearray.py b/comtypes/safearray.py
remerge CONFLICT (content): Merge conflict in comtypes/safearray.py
index 6eb23480..f99ce9d1 100644
--- a/comtypes/safearray.py
+++ b/comtypes/safearray.py
@@ -354,17 +354,12 @@ def _make_safearray_type(itemtype):
                         return ptr[:num_elements]
 
                     def keep_safearray(v):
-<<<<<<< 0681ffe7 (Addresses #63 by unpacking VT_ARRAYs of VT_RECORDs)
                         # Simply keeping a reference to self does not keep the
                         # internal addresses of BSTR safe and strings will be
                         # overwritten. A copy of the bytes solves the problem
                         v1 = v.__class__()
                         memmove(byref(v1), byref(v), sizeof(v))
                         return v1
-=======
-                        v.__keepref = self
-                        return v
->>>>>>> c34d3205 (Add record `SAFEARRAY` tests and a test-double COM server. (#980))
 
                     return [keep_safearray(x) for x in ptr[:num_elements]]
             finally:
> git diff 5bd432a 0681ffe
diff --git a/comtypes/_safearray.py b/comtypes/_safearray.py
index 76e41b9d..668d0731 100644
--- a/comtypes/_safearray.py
+++ b/comtypes/_safearray.py
@@ -69,6 +69,18 @@ def SafeArrayGetVartype(pa):
     _SafeArrayGetVartype(pa, result)
     return result.value
 
+def SafeArrayGetRecordInfo(pa):
+    # Defined here to avoid a circular import
+    from comtypes.typeinfo import IRecordInfo
+
+    _SafeArrayGetRecordInfo = _oleaut32.SafeArrayGetRecordInfo
+    _SafeArrayGetRecordInfo.restype = HRESULT
+    _SafeArrayGetRecordInfo.argtypes = [POINTER(SAFEARRAY), POINTER(POINTER(IRecordInfo))]
+
+    result = POINTER(IRecordInfo)()
+    _SafeArrayGetRecordInfo(pa, byref(result))
+    return result.value
+
 SafeArrayGetElement = _oleaut32.SafeArrayGetElement
 SafeArrayGetElement.restype = HRESULT
 SafeArrayGetElement.argtypes = [POINTER(SAFEARRAY), POINTER(LONG), c_void_p]
diff --git a/comtypes/automation.py b/comtypes/automation.py
index 7b709826..32ed8c57 100644
--- a/comtypes/automation.py
+++ b/comtypes/automation.py
@@ -454,8 +454,31 @@ class tagVARIANT(Structure):
 
             return value
         elif self.vt & VT_ARRAY:
-            typ = _vartype_to_ctype[self.vt & ~VT_ARRAY]
-            return cast(self._.pparray, _midlSAFEARRAY(typ)).unpack()
+            try:
+                typ = _vartype_to_ctype[self.vt & ~VT_ARRAY]
+                return cast(self._.pparray, _midlSAFEARRAY(typ)).unpack()
+            except KeyError:
+                pass
+
+            if self.vt & VT_RECORD:
+                from comtypes.client import GetModule
+                from comtypes.typeinfo import IRecordInfo
+
+                # Get generic safearray and its IRecordInfo
+                ri0 = cast(self._.pparray, POINTER(_safearray.SAFEARRAY))
+                ri1 = _safearray.SafeArrayGetRecordInfo(ri0)
+                # QUESTION: Do we need to add a ref ?
+                ri2 = ri1.QueryInterface(IRecordInfo)
+
+                # Now containing typelib and through it the right class type
+                mod = GetModule(ri2.GetTypeInfo().GetContainingTypeLib()[0])
+                ricls = getattr(mod, ri2.GetName())
+
+                # Cast the array to the right safearray(type), unpack -> return
+                return cast(self._.pparray, _midlSAFEARRAY(ricls)).unpack()
+
+            # Else we cannot unpack the array
+            raise NotImplementedError("typecode %d = 0x%x)" % (vt, vt))
         else:
             raise NotImplementedError("typecode %d = 0x%x)" % (vt, vt))
 
diff --git a/comtypes/safearray.py b/comtypes/safearray.py
index bef10137..7881c2fe 100644
--- a/comtypes/safearray.py
+++ b/comtypes/safearray.py
@@ -315,8 +315,13 @@ def _make_safearray_type(itemtype):
                         return ptr[:num_elements]
 
                     def keep_safearray(v):
-                        v.__keepref = self
-                        return v
+                        # Simply keeping a reference to self does not keep the
+                        # internal addresses of BSTR safe and strings will be
+                        # overwritten. A copy of the bytes solves the problem
+                        v1 = v.__class__()
+                        memmove(byref(v1), byref(v), sizeof(v))
+                        return v1
+
                     return [keep_safearray(x) for x in ptr[:num_elements]]
             finally:
                 _safearray.SafeArrayUnaccessData(self)
> git diff c34d320 87d3129
diff --git a/comtypes/_safearray.py b/comtypes/_safearray.py
index b34e3e12..01c3195c 100644
--- a/comtypes/_safearray.py
+++ b/comtypes/_safearray.py
@@ -1,6 +1,6 @@
 """SAFEARRAY api functions, data types, and constants."""
 
-from ctypes import POINTER, Structure, WinDLL, c_uint, c_ushort, c_void_p
+from ctypes import POINTER, Structure, WinDLL, byref, c_uint, c_ushort, c_void_p
 from ctypes.wintypes import DWORD, LONG, UINT, USHORT
 
 from comtypes import GUID, HRESULT
@@ -77,6 +77,17 @@ def SafeArrayGetVartype(pa):
     _SafeArrayGetVartype(pa, result)
     return result.value
 
+def SafeArrayGetRecordInfo(pa):
+    # Defined here to avoid a circular import
+    from comtypes.typeinfo import IRecordInfo
+
+    _SafeArrayGetRecordInfo = _oleaut32.SafeArrayGetRecordInfo
+    _SafeArrayGetRecordInfo.restype = HRESULT
+    _SafeArrayGetRecordInfo.argtypes = [POINTER(SAFEARRAY), POINTER(POINTER(IRecordInfo))]
+
+    result = POINTER(IRecordInfo)()
+    _SafeArrayGetRecordInfo(pa, byref(result))
+    return result.value
 
 SafeArrayGetElement = _oleaut32.SafeArrayGetElement
 SafeArrayGetElement.restype = HRESULT
diff --git a/comtypes/automation.py b/comtypes/automation.py
index aaadadc2..da851a1f 100644
--- a/comtypes/automation.py
+++ b/comtypes/automation.py
@@ -691,8 +691,31 @@ class tagVARIANT(Structure):
 
             return value
         elif self.vt & VT_ARRAY:
-            typ = _vartype_to_ctype[self.vt & ~VT_ARRAY]
-            return cast(self._.pparray, _midlSAFEARRAY(typ)).unpack()
+            try:
+                typ = _vartype_to_ctype[self.vt & ~VT_ARRAY]
+                return cast(self._.pparray, _midlSAFEARRAY(typ)).unpack()
+            except KeyError:
+                pass
+
+            if self.vt & VT_RECORD:
+                from comtypes.client import GetModule
+                from comtypes.typeinfo import IRecordInfo
+
+                # Get generic safearray and its IRecordInfo
+                ri0 = cast(self._.pparray, POINTER(_safearray.SAFEARRAY))
+                ri1 = _safearray.SafeArrayGetRecordInfo(ri0)
+                # QUESTION: Do we need to add a ref ?
+                ri2 = ri1.QueryInterface(IRecordInfo)
+
+                # Now containing typelib and through it the right class type
+                mod = GetModule(ri2.GetTypeInfo().GetContainingTypeLib()[0])
+                ricls = getattr(mod, ri2.GetName())
+
+                # Cast the array to the right safearray(type), unpack -> return
+                return cast(self._.pparray, _midlSAFEARRAY(ricls)).unpack()
+
+            # Else we cannot unpack the array
+            raise NotImplementedError("typecode %d = 0x%x)" % (vt, vt))
         else:
             raise NotImplementedError(f"typecode {vt} = 0x{vt:x})")
 
diff --git a/comtypes/safearray.py b/comtypes/safearray.py
index 3db798bd..f99ce9d1 100644
--- a/comtypes/safearray.py
+++ b/comtypes/safearray.py
@@ -354,8 +354,12 @@ def _make_safearray_type(itemtype):
                         return ptr[:num_elements]
 
                     def keep_safearray(v):
-                        v.__keepref = self
-                        return v
+                        # Simply keeping a reference to self does not keep the
+                        # internal addresses of BSTR safe and strings will be
+                        # overwritten. A copy of the bytes solves the problem
+                        v1 = v.__class__()
+                        memmove(byref(v1), byref(v), sizeof(v))
+                        return v1
 
                     return [keep_safearray(x) for x in ptr[:num_elements]]
             finally:

Out of Scope

Some formatting and runtime errors are still present after merging the current main branch.

I plan to fix these errors in a separate follow-up PR.

This PR only aims to merge the original implementation with the current main branch. We do not want to make further design changes here.

Approach

I want to keep the original contributor's work as much as possible.
I will make further changes in separate PRs. This will make it easier to review the original work and the new fixes separately.

mementum and others added 2 commits April 9, 2016 11:21
Resolve conflicts and fix missing `byref` import (previously supported via wildcard import).
@junkmd
junkmd added this pull request to stack #983 November 4, 2026 00:20
@junkmd
junkmd marked this pull request as ready for review November 5, 2026 22:59
@junkmd
junkmd merged commit fa6b25d into main Nov 5, 2026
47 of 76 checks passed
@junkmd
junkmd deleted the up1_followup_104 branch November 5, 2026 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants