Skip to content

fix: generate signed byte types for VT_I1 - #962

Draft
haoxuanjng-lang wants to merge 7 commits into
enthought:mainfrom
haoxuanjng-lang:fix/typelib-signed-int8
Draft

haoxuanjng-lang wants to merge 7 commits into
enthought:mainfrom
haoxuanjng-lang:fix/typelib-signed-int8

Conversation

@haoxuanjng-lang

@haoxuanjng-lang haoxuanjng-lang commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Generating VT_I1 as c_char changes native signed values such as -7 to 249 in tuples and produces byte-string arrays or reshape errors with NumPy. This PR proposes signed char / c_byte for VT_I1; explicit VT_LPSTR remains a narrow string. The scope and compatibility decision are tracked in #969. Context: #956 and #935; the intermittent UIA exception in the latter is outside this change.

Draft: the backward-compatibility and transition plan remains pending. Regenerated plain character-pointer interfaces reject old bytes arguments and writable character buffers. Tests assert those specific ArgumentError cases; there are no expectedFailure decorators, and passing tests do not imply backward compatibility is restored.

The native CoByteCompatibilityTest component supplies real out-of-process calls. test_compatibility_tlbparser_VT_I1_assignment.py compares saved legacy and proposed COMMETHOD definitions and the freshly generated interface. Cases are grouped by interface method and successful/error path. Distinct legacy factories expose automatic STRING output versus explicit character storage, with the buffer handling inside the factory. Proposed helpers cover automatic, pointer, reference and array output. LPSTR helpers free the native allocation in finally. Separate 1-D/2-D SAFEARRAY tests check signed values, shape, dtype and copy behavior, and a migration page describes changes to scalar, pointer and record fields.

Real-library investigation in #969 covers regenerated SAPI confidence fields, PortableDevice generated union/count-array fields, mscorlib SByte/ToSByte and the MSVidCtl Error declaration. It establishes generated-type and local-record behavior, not end-to-end speech/device/CLR/event compatibility. Record sizes/offsets match on the tested x64 installation; application compatibility and the warning/transition policy remain open.

Observed local validation on Windows x64, Python 3.13.5, NumPy 2.1.3, pywin32 308:

  • Old mapping with freshly regenerated fixture: generated definitions match LEGACY_COMMETHODS; the proposed-definition assertion fails. The original native SAFEARRAY regression has 3 failures and 1 error. Before this test reorganization, all 12 handwritten comparison tests and both direct legacy generated calls also passed with the old mapping.
  • Proposed mapping focused suites: 75 run / 66 passed / 9 skipped.
  • Full python -m unittest discover -v -s comtypes/test -t comtypes/test: 481 run / 457 passed / 24 skipped. No expected failures.
  • Ruff 0.6.9 full CI lint and format scope passes in an isolated worktree (151 files); changed files match the validated copies. git diff --check passes. Migration RST parses without warnings/errors.
  • Temporary HKCU registration is removed after each native validation stage; the final focused run restores freshly generated proposed fixture wrappers.

Based on upstream main 91408c25247bec90b09db0bbc7717f47438cb06f. This revision changes the Python tests, a mapping comment and a documentation link/field-scope clarification; production behavior and native source/binary are unchanged. The existing native build is reused. Sphinx, optional-dependency-free testing and end-to-end Microsoft application scenarios were not rerun. Upstream CI is checked against the published head after push, separately from these local results.

AI assistance: OpenAI Codex helped investigate, implement and validate this contribution.

Copilot AI balanced review requested due to automatic review settings October 9, 2026 06:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov-commenter

codecov-commenter commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.99497% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.47%. Comparing base (91408c2) to head (c4d222c).

Files with missing lines Patch % Lines
...t/test_compatibility_tlbparser_VT_I1_assignment.py 98.98% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #962      +/-   ##
==========================================
+ Coverage   89.34%   89.47%   +0.13%     
==========================================
  Files         142      143       +1     
  Lines       13856    14049     +193     
==========================================
+ Hits        12380    12571     +191     
- Misses       1476     1478       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@junkmd junkmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your continued contributions!

This is clearly an inconsistency, and it is normally not the correct behavior for software. However, I could not find any earlier bug reports about this specific behavior. This makes me wonder whether users have come to expect the current behavior over time, or whether no one has ever reached this code path.

So, let's move forward step by step, checking both the impact of this change and backward compatibility.

As before, let's start with the test structure.

Comment thread comtypes/test/test_tlbparser.py Outdated
Comment thread comtypes/tools/tlbparser.py
@haoxuanjng-lang
haoxuanjng-lang marked this pull request as draft October 10, 2026 10:01
junkmd

This comment was marked as resolved.

@junkmd

This comment was marked as resolved.

@haoxuanjng-lang

Copy link
Copy Markdown
Contributor Author

Added the reverse-direction probe and expanded the real native-call matrix. The separate component/interface/test module are now named ByteCompatibilityTest, and I merged current main (939341c), resolving the three native fixture conflicts while retaining your new string-SAFEARRAY-in-VARIANT component.

The output probe writes one character through a plain char*, with caller-owned storage. I used this shape because a plain character pointer is not a NUL-terminated string contract; an allocated char** prototype could not safely assume a terminator through the universal marshaler. The final tests avoid that unbounded read.

Observed with freshly generated wrappers against latest-main production versus the proposed mapping:

Call Old mapping Proposed mapping
ReadCharPointer(b"h") 104 ArgumentError
Same call with pointer, byref, or array of c_byte(104) ArgumentError 104
Generated raw GetCharacterBytes(104, create_string_buffer(2)), then buffer .value b"h" ArgumentError
Same raw output call with numeric byte pointer/reference/array ArgumentError stored 104
High-level GetCharacterBytes(104) TypeError allocating implicit STRING output integer 104
High-level outputs for 128 / 255 same allocation TypeError integers -128 / -1

This establishes the opposite direction's accepted-buffer change independently of SAFEARRAYs. Explicit LPSTR and unsigned scalar tables still pass both versions. I retained the two legacy input/output assertions as expected failures and added documentation for affected generated types, saved/handwritten interfaces, replacement arguments, signed outputs and explicit string handling.

Full local unittest: 474 run / 448 passed / 24 skipped / 2 expected compatibility failures. Focused suites: 70 / 59 / 9 / 2; without NumPy/pywin32: 51 / 41 / 8 / 2. Native build, full Ruff CI scope and strict Sphinx HTML build pass. Existing full docs doctest cannot get past its optional Excel type-library setup locally (WinError -2147319779); this is recorded as unvalidated, not passing. New-head upstream CI is pending.

The mapping itself is unchanged in this follow-up and the PR remains draft. These results and the documentation describe the compatibility cost; they do not resolve it or decide whether a more detailed mapping should be used.

junkmd

This comment was marked as resolved.

@junkmd junkmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is logically consistent, regardless of compatibility, and the tests have become clearer with each review. However, I could not explain why it still felt difficult to understand.

I have just realized that the tests do not yet give us a clear record of this compatibility change.

At the moment, the tests only cover the new mapping. It was hard to compare how the old mapping worked, and what happens when we use the old calling style with the new mapping.

I came up with the fixtures to make this easier to understand.

This mapping seems to have survived unchanged for about 20 years, at least as far back as our version history goes. Changing something that old feels A LITTLE scary!

Please bear with me a little longer, at least until I can understand this change well enough myself.

Thank you!

Comment thread comtypes/test/test_compatibility_tlbparser_VT_I1_assignment.py
@junkmd

This comment was marked as outdated.

@junkmd junkmd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it has become much easier to understand.
I'll just mention one thing that caught my attention when I briefly glanced at the code.

Comment on lines +367 to +375
@unittest.expectedFailure
def test_char_pointer_bytes_input(self):
self.assertEqual(self.server.ReadCharPointer(b"h"), 104)

@unittest.expectedFailure
def test_character_bytes_output_buffer(self):
result = create_string_buffer(2)
self.server._IByteCompatibilityTest__com_GetCharacterBytes(104, result)
self.assertEqual(result.value, b"h")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since expectedFailure hides the reason for the failure, it might be better to use assertRaises instead.

If an assertion fails, please use assertNotEqual for the expected value in the legacy mapping and assertEqual for the expected value in the new mapping, and add a clarifying comment.

Comment thread comtypes/test/test_compatibility_tlbparser_VT_I1_assignment.py
Comment on lines +258 to +271
def test_legacy_int8_implicit_output(self):
# STRING cannot allocate a scalar output. Keep that legacy behavior
# visible rather than silently adapting it to the proposed result type.
for value in (b"\x80", b"\xf9", b"\xff", b"\0", b"\x01", b"\x7f"):
with self.subTest(mapping="legacy", value=value):
with self.assertRaises(TypeError):
self.legacy.EchoInt8(value)

def test_legacy_int8_explicit_output(self):
for value in (b"\x80", b"\xf9", b"\xff", b"\0", b"\x01", b"\x7f"):
with self.subTest(mapping="legacy", value=value):
result = create_string_buffer(2)
self.legacy._IByteCompatibilityTest__com_EchoInt8(value, result)
self.assertEqual(result.raw[:1], value)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performing buffer creation and result slicing—which explicit_output does—inside the tests might be reducing readability.

Please split the create_legacy_itf factory into two versions: one that overrides the EchoInt8 method to encapsulate buffer creation and result slicing, and one that does not. And please give each of those factories a good, distinguishable name. Then, please place them into the happy path and unhappy path subtests.

Please move comments like # STRING cannot allocate a scalar output. to the section where the method is overridden and encapsulated within the factory.

Comment thread comtypes/test/test_compatibility_tlbparser_VT_I1_assignment.py
@junkmd

junkmd commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

I opened #969 to record decisions about this change and provide a place for future support.

When referring to this mapping in code comments or documentation, please link to this issue.

@junkmd

junkmd commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

To understand real-world use cases of VT_I1, I first added a branch that emits a warning when parsing the libraries used in our tests.

You can review the implementation in a409eea.

The warnings alone are shown below.

path\to\comtypes\comtypes\tools\tlbparser.py:689: UserWarning: VT_I1 is used in '_IMSVidCtlEvents' method 'Error' parameter 'CancelDisplay', <libid='{B0EDF154-910A-11D2-B632-00C04F79498E}', lcid=0, major=1, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:680: UserWarning: VT_I1 is used in 'INumericSafearrayTest' method 'GetInt8Array' parameter 'result', <libid='{07D2AEE5-1DF8-4D2C-953A-554ADFD25F99}', lcid=0, major=1, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:680: UserWarning: VT_I1 is used in 'IByteCompatibilityTest' method 'EchoInt8' parameter 'value', <libid='{07D2AEE5-1DF8-4D2C-953A-554ADFD25F99}', lcid=0, major=1, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:680: UserWarning: VT_I1 is used in 'IByteCompatibilityTest' method 'EchoInt8' parameter 'result', <libid='{07D2AEE5-1DF8-4D2C-953A-554ADFD25F99}', lcid=0, major=1, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:680: UserWarning: VT_I1 is used in 'IByteCompatibilityTest' method 'ReadCharPointer' parameter 'value', <libid='{07D2AEE5-1DF8-4D2C-953A-554ADFD25F99}', lcid=0, major=1, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:680: UserWarning: VT_I1 is used in 'IByteCompatibilityTest' method 'GetCharacterBytes' parameter 'result', <libid='{07D2AEE5-1DF8-4D2C-953A-554ADFD25F99}', lcid=0, major=1, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:696: UserWarning: VT_I1 is used in '__MIDL___MIDL_itf_PortableDeviceApi_0001_0000_0001' field 'cVal', <libid='{1F001332-1A57-4934-BE31-AFFC99F4EE0A}', lcid=0, major=1, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:696: UserWarning: VT_I1 is used in '__MIDL_IOleAutomationTypes_0004' field 'cVal', <libid='{1F001332-1A57-4934-BE31-AFFC99F4EE0A}', lcid=0, major=1, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:696: UserWarning: VT_I1 is used in '__MIDL_IOleAutomationTypes_0004' field 'pcVal', <libid='{1F001332-1A57-4934-BE31-AFFC99F4EE0A}', lcid=0, major=1, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:676: UserWarning: VT_I1 is used in 'tagCAC' field 'pElems', <libid='{1F001332-1A57-4934-BE31-AFFC99F4EE0A}', lcid=0, major=1, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:696: UserWarning: VT_I1 is used in '__MIDL___MIDL_itf_PortableDeviceApi_0001_0000_0001' field 'pcVal', <libid='{1F001332-1A57-4934-BE31-AFFC99F4EE0A}', lcid=0, major=1, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:676: UserWarning: VT_I1 is used in 'SPPHRASERULE' field 'Confidence', <libid='{C866CA3A-32F7-11D2-9602-00C04F8EE628}', lcid=0, major=5, minor=4>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:676: UserWarning: VT_I1 is used in 'SPPHRASEPROPERTY' field 'Confidence', <libid='{C866CA3A-32F7-11D2-9602-00C04F8EE628}', lcid=0, major=5, minor=4>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:676: UserWarning: VT_I1 is used in 'SPPHRASEELEMENT' field 'RequiredConfidence', <libid='{C866CA3A-32F7-11D2-9602-00C04F8EE628}', lcid=0, major=5, minor=4>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:676: UserWarning: VT_I1 is used in 'SPPHRASEELEMENT' field 'ActualConfidence', <libid='{C866CA3A-32F7-11D2-9602-00C04F8EE628}', lcid=0, major=5, minor=4>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:706: UserWarning: VT_I1 is used in 'IConvertible' method 'ToSByte' parameter 'pRetVal', <libid='{BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}', lcid=0, major=2, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:676: UserWarning: VT_I1 is used in 'SByte' field 'm_value', <libid='{BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}', lcid=0, major=2, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.
path\to\comtypes\comtypes\tools\tlbparser.py:706: UserWarning: VT_I1 is used in 'IFormatterConverter' method 'ToSByte' parameter 'pRetVal', <libid='{BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}', lcid=0, major=2, minor=0>. The generated types are planned to change: from c_char to c_byte for VT_I1, and from STRING to POINTER(c_byte) for pointers to VT_I1. See https://github.com/enthought/comtypes/issues/969 for details.

Please investigate the use cases of these interfaces and structures to determine whether the impact of changing the mapping is small, or whether switching to the new mapping would fix bugs.

@haoxuanjng-lang

Copy link
Copy Markdown
Contributor Author

Pushed the requested test organization and explicit compatibility assertions:

  • Renamed the module to test_compatibility_tlbparser_VT_I1_assignment.py and divided it into TestEchoInt8, TestEchoUint8, TestEchoLpStr, TestReadCharPointer and TestGetCharacterBytes, separating successful calls from TypeError and ArgumentError paths where those occur.
  • Split the old interface factory into create_legacy_automatic_int8_output and create_legacy_buffered_int8_output. The latter owns the raw call, buffer and slicing; the explanation of the automatic STRING limitation is next to that override.
  • Removed both expectedFailure decorators. Tests now assert the exact incompatible generated-interface calls raise ArgumentError, and compare literal numeric/bytes results explicitly. Passing tests do not mean those old calling conventions are preserved.
  • Added links to Fix the tlbparser ctypes mapping for VT_I1. #969 in the mapping comment, test module and migration page. Investigated the four real Microsoft libraries mentioned above and will put the detailed findings and warning tradeoffs in Fix the tlbparser ctypes mapping for VT_I1. #969.

Observed Windows x64 / Python 3.13.5 / NumPy 2.1.3 / pywin32 308 results: focused 75 run, 66 passed, 9 skipped; full unittest discovery 481 run, 457 passed, 24 skipped. No expected failures remain. Old-mapping regeneration matches the legacy definitions; the proposed-definition assertion fails and the original native SAFEARRAY tests still fail (3 failures, 1 error). Ruff 0.6.9 passes the complete CI lint/format scope in an isolated worktree (151 files); the migration RST parses without warnings. Temporary HKCU registrations were cleaned up and the last focused run regenerated the proposed fixture wrappers.

The native binary is unchanged and was reused; I did not rerun its build, the optional-dependency-free environment or the Sphinx documentation build. Production behavior is unchanged from the previous PR revision. Draft status remains while the compatibility/transition plan is discussed; upstream CI for this new head is tracked separately.

AI assistance: OpenAI Codex.

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.

4 participants