From dc5e77b356e77aa46a5e13c2533372bfa483a9ce Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 3 Nov 2026 01:14:26 +0900 Subject: [PATCH 1/4] test: Add record `SAFEARRAY` tests and test-double COM server implementation. --- comtypes/test/test_record_safearray_bstr.py | 332 +++++++++++++++++++ comtypes/test/test_record_safearray_value.py | 327 ++++++++++++++++++ source/CppTestSrv/CoRecordSafearrayTest.cpp | 311 +++++++++++++++++ source/CppTestSrv/CoRecordSafearrayTest.h | 62 ++++ source/CppTestSrv/MAKEFILE | 7 +- source/CppTestSrv/SERVER.CPP | 7 + source/CppTestSrv/SERVER.IDL | 71 ++++ 7 files changed, 1116 insertions(+), 1 deletion(-) create mode 100644 comtypes/test/test_record_safearray_bstr.py create mode 100644 comtypes/test/test_record_safearray_value.py create mode 100644 source/CppTestSrv/CoRecordSafearrayTest.cpp create mode 100644 source/CppTestSrv/CoRecordSafearrayTest.h diff --git a/comtypes/test/test_record_safearray_bstr.py b/comtypes/test/test_record_safearray_bstr.py new file mode 100644 index 000000000..faac344b6 --- /dev/null +++ b/comtypes/test/test_record_safearray_bstr.py @@ -0,0 +1,332 @@ +"""Record SAFEARRAY Tests (BSTR Records) + +Tests record marshaling of BSTR records (structures owning heap-allocated +strings) between Python and a COM server. +""" + +import gc +import unittest +from _ctypes import COMError +from typing import TYPE_CHECKING, ClassVar, TypeVar +from typing import Union as _UnionT + +from comtypes import CLSCTX_LOCAL_SERVER, IUnknown, _safearray +from comtypes.client import CreateObject, GetModule +from comtypes.hresult import DISP_E_EXCEPTION, DISP_E_TYPEMISMATCH, E_INVALIDARG +from comtypes.safearray import _midlSAFEARRAY + +# _TCBase is unittest.TestCase during type checking (so mixin methods can use +# self.assertEqual etc.) but object at runtime. This prevents the mixin class +# itself from being collected as a test case by unittest. +if TYPE_CHECKING: + _TCBase = unittest.TestCase +else: + _TCBase = object + +ComtypesCppTestSrvLib_GUID = "{07D2AEE5-1DF8-4D2C-953A-554ADFD25F99}" + +try: + GetModule((ComtypesCppTestSrvLib_GUID, 1, 0, 0)) + import comtypes.gen.ComtypesCppTestSrvLib as ComtypesCppTestSrvLib + + IMPORT_FAILED = False +except (ImportError, OSError): + IMPORT_FAILED = True + + +def _create_record( + id: int, seq: int, text: str, checksum: int +) -> "ComtypesCppTestSrvLib.StructBstrRecordSafearrayTest": + return ComtypesCppTestSrvLib.StructBstrRecordSafearrayTest( + id=id, seq=seq, text=text, checksum=checksum + ) + + +_T_IUnknown = TypeVar("_T_IUnknown", bound=IUnknown) + + +def _create_server(interface: type[_T_IUnknown]) -> _T_IUnknown: + return CreateObject( + "Comtypes.RecordSafeArrayRoundTripTest", + clsctx=CLSCTX_LOCAL_SERVER, + interface=interface, + ) + + +def _get_bstr_record_sa_type(): + """Return the SAFEARRAY type for BSTR records""" + return _midlSAFEARRAY(ComtypesCppTestSrvLib.StructBstrRecordSafearrayTest) + + +class _BstrRecordScenariosMixin(_TCBase): + """Mixin providing test scenarios for BSTR records + (StructBstrRecordSafearrayTest). + """ + + interface: ClassVar[ + _UnionT[ + type["ComtypesCppTestSrvLib.IDualRecordSafearrayTest"], + type["ComtypesCppTestSrvLib.IDispRecordSafearrayTest"], + ] + ] + + def test_first_record_length_1_safearray_by_from_param(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=400, seq=0, text="RecSafeArray-400-0", checksum=40708), + ] + lp = _get_bstr_record_sa_type().from_param(records) + + result = ifc.FirstBstrRecord(lp) + self.assertEqual(result.id, 400) + self.assertEqual(result.seq, 0) + self.assertEqual(result.text, "RecSafeArray-400-0") + self.assertEqual(result.checksum, 40708) + + del records, lp + gc.collect() + + def test_first_record_length_1_safearray_by_create(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=400, seq=0, text="RecSafeArray-400-0", checksum=40708), + ] + lp = _get_bstr_record_sa_type().create(records) + self.addCleanup(_safearray.SafeArrayDestroy, lp) + + result = ifc.FirstBstrRecord(lp) + self.assertEqual(result.id, 400) + self.assertEqual(result.seq, 0) + self.assertEqual(result.text, "RecSafeArray-400-0") + self.assertEqual(result.checksum, 40708) + + del records + gc.collect() + + def test_first_record_length_3_safearray_by_from_param(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=401, seq=0, text="RecSafeArray-401-0", checksum=40808), + _create_record(id=402, seq=1, text="RecSafeArray-402-1", checksum=40918), + _create_record(id=403, seq=2, text="RecSafeArray-403-2", checksum=41028), + ] + lp = _get_bstr_record_sa_type().from_param(records) + + result = ifc.FirstBstrRecord(lp) + self.assertEqual(result.id, 401) + self.assertEqual(result.seq, 0) + self.assertEqual(result.text, "RecSafeArray-401-0") + self.assertEqual(result.checksum, 40808) + + del records, lp + gc.collect() + + def test_first_record_length_3_safearray_by_create(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=401, seq=0, text="RecSafeArray-401-0", checksum=40808), + _create_record(id=402, seq=1, text="RecSafeArray-402-1", checksum=40918), + _create_record(id=403, seq=2, text="RecSafeArray-403-2", checksum=41028), + ] + lp = _get_bstr_record_sa_type().create(records) + self.addCleanup(_safearray.SafeArrayDestroy, lp) + + result = ifc.FirstBstrRecord(lp) + self.assertEqual(result.id, 401) + self.assertEqual(result.seq, 0) + self.assertEqual(result.text, "RecSafeArray-401-0") + self.assertEqual(result.checksum, 40808) + + del records + gc.collect() + + def test_first_record_survives_input_release(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=402, seq=0, text="RecSafeArray-402-0", checksum=40908), + _create_record(id=403, seq=1, text="RecSafeArray-403-1", checksum=41018), + _create_record(id=404, seq=2, text="RecSafeArray-404-2", checksum=41128), + ] + lp = _get_bstr_record_sa_type().from_param(records) + + result = ifc.FirstBstrRecord(lp) + + del lp # Release the input array + gc.collect() + + # Result should still be valid after the input SAFEARRAY is released + self.assertEqual(result.id, 402) + self.assertEqual(result.seq, 0) + self.assertEqual(result.text, "RecSafeArray-402-0") + self.assertEqual(result.checksum, 40908) + + del records + gc.collect() + + +# ====================================================================== +# Vtable pathway tests (IDualRecordSafearrayTest) +# ====================================================================== + + +@unittest.skipIf(IMPORT_FAILED, "This depends on the out of process COM-server.") +class TestVtableBstrRecordSafeArray(_BstrRecordScenariosMixin, unittest.TestCase): + """IDispatch dual vtable tests, BSTR records.""" + + @classmethod + def setUpClass(cls): + cls.interface = ComtypesCppTestSrvLib.IDualRecordSafearrayTest + + def test_first_record_length_1_list(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=400, seq=0, text="RecSafeArray-400-0", checksum=40708), + ] + + result = ifc.FirstBstrRecord(records) + self.assertEqual(result.id, 400) + self.assertEqual(result.seq, 0) + self.assertEqual(result.text, "RecSafeArray-400-0") + self.assertEqual(result.checksum, 40708) + + del records + gc.collect() + + def test_first_record_length_3_list(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=401, seq=0, text="RecSafeArray-401-0", checksum=40808), + _create_record(id=402, seq=1, text="RecSafeArray-402-1", checksum=40918), + _create_record(id=403, seq=2, text="RecSafeArray-403-2", checksum=41028), + ] + + result = ifc.FirstBstrRecord(records) + self.assertEqual(result.id, 401) + self.assertEqual(result.seq, 0) + self.assertEqual(result.text, "RecSafeArray-401-0") + self.assertEqual(result.checksum, 40808) + + del records + gc.collect() + + def test_first_record_empty_safearray_by_from_param(self): + ifc = _create_server(self.interface) + records = [] + lp = _get_bstr_record_sa_type().from_param(records) + + with self.assertRaises(COMError) as cm: + ifc.FirstBstrRecord(lp) + self.assertEqual(cm.exception.hresult, E_INVALIDARG) + + del records, lp + gc.collect() + + def test_first_record_empty_list(self): + ifc = _create_server(self.interface) + records = [] + + with self.assertRaises(COMError) as cm: + ifc.FirstBstrRecord(records) + self.assertEqual(cm.exception.hresult, E_INVALIDARG) + + del records + gc.collect() + + def test_first_record_empty_safearray_by_create(self): + ifc = _create_server(self.interface) + records = [] + lp = _get_bstr_record_sa_type().create(records) + self.addCleanup(_safearray.SafeArrayDestroy, lp) + + with self.assertRaises(COMError) as cm: + ifc.FirstBstrRecord(lp) + self.assertEqual(cm.exception.hresult, E_INVALIDARG) + + del records + gc.collect() + + +# ====================================================================== +# Dispatch pathway tests (IDispRecordSafearrayTest) +# ====================================================================== + + +@unittest.skipIf(IMPORT_FAILED, "This depends on the out of process COM-server.") +class TestDispatchBstrRecordSafeArray(_BstrRecordScenariosMixin, unittest.TestCase): + """IDispatch dispinterface Invoke tests, BSTR records.""" + + @classmethod + def setUpClass(cls): + cls.interface = ComtypesCppTestSrvLib.IDispRecordSafearrayTest + + def test_first_record_length_1_list(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=400, seq=0, text="RecSafeArray-400-0", checksum=40708), + ] + + with self.assertRaises(COMError) as cm: + ifc.FirstBstrRecord(records) + self.assertEqual(cm.exception.hresult, DISP_E_TYPEMISMATCH) + + del records + gc.collect() + + def test_first_record_length_3_list(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=401, seq=0, text="RecSafeArray-401-0", checksum=40808), + _create_record(id=402, seq=1, text="RecSafeArray-402-1", checksum=40918), + _create_record(id=403, seq=2, text="RecSafeArray-403-2", checksum=41028), + ] + + with self.assertRaises(COMError) as cm: + ifc.FirstBstrRecord(records) + self.assertEqual(cm.exception.hresult, DISP_E_TYPEMISMATCH) + + del records + gc.collect() + + def test_first_record_empty_safearray_by_from_param(self): + ifc = _create_server(self.interface) + records = [] + lp = _get_bstr_record_sa_type().from_param(records) + + with self.assertRaises(COMError) as cm: + ifc.FirstBstrRecord(lp) + # wrapped in DISP_E_EXCEPTION with scode=E_INVALIDARG. + self.assertEqual(cm.exception.hresult, DISP_E_EXCEPTION) + self.assertEqual(cm.exception.details[4], E_INVALIDARG) + + del records, lp + gc.collect() + + def test_first_record_empty_safearray_list(self): + ifc = _create_server(self.interface) + records = [] + + with self.assertRaises(COMError) as cm: + ifc.FirstBstrRecord(records) + self.assertEqual(cm.exception.hresult, DISP_E_TYPEMISMATCH) + + del records + gc.collect() + + def test_first_record_empty_safearray_by_create(self): + ifc = _create_server(self.interface) + records = [] + lp = _get_bstr_record_sa_type().create(records) + self.addCleanup(_safearray.SafeArrayDestroy, lp) + + with self.assertRaises(COMError) as cm: + ifc.FirstBstrRecord(lp) + # wrapped in DISP_E_EXCEPTION with scode=E_INVALIDARG. + self.assertEqual(cm.exception.hresult, DISP_E_EXCEPTION) + self.assertEqual(cm.exception.details[4], E_INVALIDARG) + + del records + gc.collect() + + +if __name__ == "__main__": + unittest.main() diff --git a/comtypes/test/test_record_safearray_value.py b/comtypes/test/test_record_safearray_value.py new file mode 100644 index 000000000..973c21b47 --- /dev/null +++ b/comtypes/test/test_record_safearray_value.py @@ -0,0 +1,327 @@ +"""Record SAFEARRAY Tests (Value Records) + +Tests record marshaling of value records (integer-only structures) between +Python and a COM server. +""" + +import gc +import unittest +from _ctypes import COMError +from typing import TYPE_CHECKING, ClassVar, TypeVar +from typing import Union as _UnionT + +from comtypes import CLSCTX_LOCAL_SERVER, IUnknown, _safearray +from comtypes.client import CreateObject, GetModule +from comtypes.hresult import DISP_E_EXCEPTION, DISP_E_TYPEMISMATCH, E_INVALIDARG +from comtypes.safearray import _midlSAFEARRAY + +# _TCBase is unittest.TestCase during type checking (so mixin methods can use +# self.assertEqual etc.) but object at runtime. This prevents the mixin class +# itself from being collected as a test case by unittest. +if TYPE_CHECKING: + _TCBase = unittest.TestCase +else: + _TCBase = object + +ComtypesCppTestSrvLib_GUID = "{07D2AEE5-1DF8-4D2C-953A-554ADFD25F99}" + +try: + GetModule((ComtypesCppTestSrvLib_GUID, 1, 0, 0)) + import comtypes.gen.ComtypesCppTestSrvLib as ComtypesCppTestSrvLib + + IMPORT_FAILED = False +except (ImportError, OSError): + # Server type library not registered; tests depending on it will be skipped. + IMPORT_FAILED = True + + +def _create_record( + id: int, seq: int, checksum: int +) -> "ComtypesCppTestSrvLib.StructValueRecordSafearrayTest": + return ComtypesCppTestSrvLib.StructValueRecordSafearrayTest( + id=id, seq=seq, checksum=checksum + ) + + +_T_IUnknown = TypeVar("_T_IUnknown", bound=IUnknown) + + +def _create_server(interface: type[_T_IUnknown]) -> _T_IUnknown: + """Create an out-of-process COM server instance for the given interface. + + Uses CLSCTX_LOCAL_SERVER to launch CoRecordSafearrayTest as a separate + process, ensuring proper marshaling of SAFEARRAY across process boundaries. + """ + return CreateObject( + "Comtypes.RecordSafeArrayRoundTripTest", + clsctx=CLSCTX_LOCAL_SERVER, + interface=interface, + ) + + +def _get_value_record_sa_type(): + """Return the SAFEARRAY type for value records (the _midlSAFEARRAY specialization). + + This is a function (not a module-level constant) to defer evaluation until + the type library has been imported. This allows the module to be imported + even when the test server is not registered. + """ + return _midlSAFEARRAY(ComtypesCppTestSrvLib.StructValueRecordSafearrayTest) + + +class _ValueRecordScenariosMixin(_TCBase): + """Mixin providing test scenarios for value records + (StructValueRecordSafearrayTest). + """ + + interface: ClassVar[ + _UnionT[ + type["ComtypesCppTestSrvLib.IDualRecordSafearrayTest"], + type["ComtypesCppTestSrvLib.IDispRecordSafearrayTest"], + ] + ] + + def test_first_record_length_1_safearray_by_from_param(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=200, seq=0, checksum=20042), + ] + lp = _get_value_record_sa_type().from_param(records) + + result = ifc.FirstValueRecord(lp) + self.assertEqual(result.id, 200) + self.assertEqual(result.seq, 0) + self.assertEqual(result.checksum, 20042) + + del records, lp + gc.collect() + + def test_first_record_length_1_safearray_by_create(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=200, seq=0, checksum=20042), + ] + lp = _get_value_record_sa_type().create(records) + self.addCleanup(_safearray.SafeArrayDestroy, lp) + + result = ifc.FirstValueRecord(lp) + self.assertEqual(result.id, 200) + self.assertEqual(result.seq, 0) + self.assertEqual(result.checksum, 20042) + + del records + gc.collect() + + def test_first_record_length_3_safearray_by_from_param(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=201, seq=0, checksum=20142), + _create_record(id=202, seq=1, checksum=20252), + _create_record(id=203, seq=2, checksum=20362), + ] + lp = _get_value_record_sa_type().from_param(records) + + result = ifc.FirstValueRecord(lp) + self.assertEqual(result.id, 201) + self.assertEqual(result.seq, 0) + self.assertEqual(result.checksum, 20142) + + del records, lp + gc.collect() + + # Server returns the first record from a length-3 array (create LP). + def test_first_record_length_3_safearray_by_create(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=201, seq=0, checksum=20142), + _create_record(id=202, seq=1, checksum=20252), + _create_record(id=203, seq=2, checksum=20362), + ] + lp = _get_value_record_sa_type().create(records) + self.addCleanup(_safearray.SafeArrayDestroy, lp) + + result = ifc.FirstValueRecord(lp) + self.assertEqual(result.id, 201) + self.assertEqual(result.seq, 0) + self.assertEqual(result.checksum, 20142) + + del records + gc.collect() + + def test_first_record_survives_input_release(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=202, seq=0, checksum=20242), + _create_record(id=203, seq=1, checksum=20352), + _create_record(id=204, seq=2, checksum=20462), + ] + lp = _get_value_record_sa_type().from_param(records) + + result = ifc.FirstValueRecord(lp) + + # Release the input array + del lp + gc.collect() + + # Result should still be valid + self.assertEqual(result.id, 202) + self.assertEqual(result.seq, 0) + self.assertEqual(result.checksum, 20242) + + del records + gc.collect() + + +@unittest.skipIf(IMPORT_FAILED, "This depends on the out of process COM-server.") +class TestVtableValueRecordSafeArray(_ValueRecordScenariosMixin, unittest.TestCase): + """IDispatch dual vtable tests, value records.""" + + @classmethod + def setUpClass(cls): + cls.interface = ComtypesCppTestSrvLib.IDualRecordSafearrayTest + + def test_first_record_length_1_list(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=200, seq=0, checksum=20042), + ] + + result = ifc.FirstValueRecord(records) + self.assertEqual(result.id, 200) + self.assertEqual(result.seq, 0) + self.assertEqual(result.checksum, 20042) + + del records + gc.collect() + + def test_first_record_length_3_list(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=201, seq=0, checksum=20142), + _create_record(id=202, seq=1, checksum=20252), + _create_record(id=203, seq=2, checksum=20362), + ] + + result = ifc.FirstValueRecord(records) + self.assertEqual(result.id, 201) + self.assertEqual(result.seq, 0) + self.assertEqual(result.checksum, 20142) + + del records + gc.collect() + + def test_first_record_empty_safearray_by_from_param(self): + ifc = _create_server(self.interface) + records = [] + lp = _get_value_record_sa_type().from_param(records) + + with self.assertRaises(COMError) as cm: + ifc.FirstValueRecord(lp) + self.assertEqual(cm.exception.hresult, E_INVALIDARG) + + del records, lp + gc.collect() + + def test_first_record_empty_list(self): + ifc = _create_server(self.interface) + records = [] + + with self.assertRaises(COMError) as cm: + ifc.FirstValueRecord(records) + self.assertEqual(cm.exception.hresult, E_INVALIDARG) + + del records + gc.collect() + + def test_first_record_empty_safearray_by_create(self): + ifc = _create_server(self.interface) + records = [] + lp = _get_value_record_sa_type().create(records) + self.addCleanup(_safearray.SafeArrayDestroy, lp) + + with self.assertRaises(COMError) as cm: + ifc.FirstValueRecord(lp) + self.assertEqual(cm.exception.hresult, E_INVALIDARG) + + del records + gc.collect() + + +@unittest.skipIf(IMPORT_FAILED, "This depends on the out of process COM-server.") +class TestDispatchValueRecordSafeArray(_ValueRecordScenariosMixin, unittest.TestCase): + """IDispatch dispinterface Invoke tests, value records.""" + + @classmethod + def setUpClass(cls): + cls.interface = ComtypesCppTestSrvLib.IDispRecordSafearrayTest + + def test_first_record_length_1_list(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=200, seq=0, checksum=20042), + ] + + with self.assertRaises(COMError) as cm: + ifc.FirstValueRecord(records) + self.assertEqual(cm.exception.hresult, DISP_E_TYPEMISMATCH) + + del records + gc.collect() + + def test_first_record_length_3_list(self): + ifc = _create_server(self.interface) + records = [ + _create_record(id=201, seq=0, checksum=20142), + _create_record(id=202, seq=1, checksum=20252), + _create_record(id=203, seq=2, checksum=20362), + ] + with self.assertRaises(COMError) as cm: + ifc.FirstValueRecord(records) + self.assertEqual(cm.exception.hresult, DISP_E_TYPEMISMATCH) + + del records + gc.collect() + + def test_first_record_empty_safearray_by_from_param(self): + ifc = _create_server(self.interface) + records = [] + lp = _get_value_record_sa_type().from_param(records) + + with self.assertRaises(COMError) as cm: + ifc.FirstValueRecord(lp) + # wrapped in DISP_E_EXCEPTION with scode=E_INVALIDARG. + self.assertEqual(cm.exception.hresult, DISP_E_EXCEPTION) + self.assertEqual(cm.exception.details[4], E_INVALIDARG) + + del records, lp + gc.collect() + + def test_first_record_empty_list(self): + ifc = _create_server(self.interface) + records = [] + + with self.assertRaises(COMError) as cm: + ifc.FirstValueRecord(records) + self.assertEqual(cm.exception.hresult, DISP_E_TYPEMISMATCH) + + del records + gc.collect() + + def test_first_record_empty_safearray_by_create(self): + ifc = _create_server(self.interface) + records = [] + lp = _get_value_record_sa_type().create(records) + self.addCleanup(_safearray.SafeArrayDestroy, lp) + + with self.assertRaises(COMError) as cm: + ifc.FirstValueRecord(lp) + # wrapped in DISP_E_EXCEPTION with scode=E_INVALIDARG. + self.assertEqual(cm.exception.hresult, DISP_E_EXCEPTION) + self.assertEqual(cm.exception.details[4], E_INVALIDARG) + + del records + gc.collect() + + +if __name__ == "__main__": + unittest.main() diff --git a/source/CppTestSrv/CoRecordSafearrayTest.cpp b/source/CppTestSrv/CoRecordSafearrayTest.cpp new file mode 100644 index 000000000..1a573886d --- /dev/null +++ b/source/CppTestSrv/CoRecordSafearrayTest.cpp @@ -0,0 +1,311 @@ +/* + * CoRecordSafearrayTest.cpp - Component for record SAFEARRAY round-trip tests + * + * This component implements two test pathways: + * 1. IDispatch dual vtable (IDualRecordSafearrayTest) + * 2. IDispatch dispinterface Invoke (IDispRecordSafearrayTest) + */ + +#include +#include + +#include "Iface.h" +#include "Util.h" +#include "CUnknown.h" +#include "CFactory.h" +#include "CoRecordSafearrayTest.h" + +// Forward declaration for dispinterface IID (not in MIDL-generated header) +EXTERN_C const IID DIID_IDispRecordSafearrayTest; + +// Explicit GUIDs for record structs: MIDL does not emit __declspec(uuid(...)) for +// typedef'd structs, so __uuidof() cannot be used directly. +static const GUID GUID_StructValueRecordSafearrayTest = + { 0x6B298B7B, 0xCA0E, 0x431D, { 0x98, 0x8C, 0xFE, 0xD3, 0xCE, 0xB5, 0x97, 0x29 } }; +static const GUID GUID_StructBstrRecordSafearrayTest = + { 0x5F72F918, 0xEC40, 0x42C1, { 0xA0, 0x34, 0xF6, 0x60, 0x89, 0x02, 0xEC, 0xB3 } }; + +static inline void trace(const char* msg) + { Util::Trace("CoRecordSafearrayTest", msg, S_OK); } +static inline void trace(const char* msg, HRESULT hr) + { Util::Trace("CoRecordSafearrayTest", msg, hr); } + +// ======================================================================== +// CRecordSafearrayTest Implementation +// ======================================================================== + +CRecordSafearrayTest::CRecordSafearrayTest(IUnknown* pUnknownOuter) + : CUnknown(pUnknownOuter), m_pITypeInfo(NULL) +{ +} + +CRecordSafearrayTest::~CRecordSafearrayTest() +{ + if (m_pITypeInfo != NULL) + m_pITypeInfo->Release(); + trace("Destroy self."); +} + +HRESULT CRecordSafearrayTest::CreateInstance( + IUnknown* pUnknownOuter, CUnknown** ppNewComponent) +{ + if (pUnknownOuter != NULL) + return CLASS_E_NOAGGREGATION; + + *ppNewComponent = new CRecordSafearrayTest(pUnknownOuter); + return S_OK; +} + +HRESULT __stdcall CRecordSafearrayTest::NondelegatingQueryInterface( + const IID& iid, void** ppv) +{ + if (iid == IID_IDualRecordSafearrayTest) + { + return FinishQI(static_cast(this), ppv); + } + else if (iid == DIID_IDispRecordSafearrayTest) + { + trace("Queried for IDispRecordSafearrayTest."); + return FinishQI(static_cast(this), ppv); + } + else if (iid == IID_IDispatch) + { + trace("Queried for IDispatch."); + return FinishQI(static_cast(this), ppv); + } + return CUnknown::NondelegatingQueryInterface(iid, ppv); +} + +HRESULT CRecordSafearrayTest::Init() +{ + HRESULT hr; + + if (m_pITypeInfo == NULL) + { + ITypeLib* pITypeLib = NULL; + hr = ::LoadRegTypeLib(LIBID_ComtypesCppTestSrvLib, + 1, 0, 0x00, &pITypeLib); + if (FAILED(hr)) + { + trace("LoadRegTypeLib Failed.", hr); + return hr; + } + + hr = pITypeLib->GetTypeInfoOfGuid(IID_IDualRecordSafearrayTest, + &m_pITypeInfo); + pITypeLib->Release(); + if (FAILED(hr)) + { + trace("GetTypeInfoOfGuid failed.", hr); + return hr; + } + } + return S_OK; +} + +// ------------------------------------------------------------------------ +// IDispatch Implementation +// ------------------------------------------------------------------------ + +HRESULT __stdcall CRecordSafearrayTest::GetTypeInfoCount(UINT* pCountTypeInfo) +{ + trace("GetTypeInfoCount call succeeded."); + *pCountTypeInfo = 1; + return S_OK; +} + +HRESULT __stdcall CRecordSafearrayTest::GetTypeInfo( + UINT iTypeInfo, LCID, ITypeInfo** ppITypeInfo) +{ + *ppITypeInfo = NULL; + + if (iTypeInfo != 0) + { + trace("GetTypeInfo call failed -- bad iTypeInfo index."); + return DISP_E_BADINDEX; + } + + trace("GetTypeInfo call succeeded."); + m_pITypeInfo->AddRef(); + *ppITypeInfo = m_pITypeInfo; + return S_OK; +} + +HRESULT __stdcall CRecordSafearrayTest::GetIDsOfNames( + const IID& iid, OLECHAR** arrayNames, UINT countNames, LCID, DISPID* arrayDispIDs) +{ + if (iid != IID_NULL) + { + trace("GetIDsOfNames call failed -- bad IID."); + return DISP_E_UNKNOWNINTERFACE; + } + + trace("GetIDsOfNames call succeeded."); + HRESULT hr = m_pITypeInfo->GetIDsOfNames(arrayNames, countNames, arrayDispIDs); + return hr; +} + +HRESULT __stdcall CRecordSafearrayTest::Invoke( + DISPID dispidMember, const IID& iid, LCID, WORD wFlags, + DISPPARAMS* pDispParams, VARIANT* pvarResult, EXCEPINFO* pExcepInfo, UINT* pArgErr) +{ + if (iid != IID_NULL) + { + trace("Invoke call failed -- bad IID."); + return DISP_E_UNKNOWNINTERFACE; + } + + ::SetErrorInfo(0, NULL); + trace("Invoke call succeeded."); + HRESULT hr = m_pITypeInfo->Invoke( + static_cast(this), + dispidMember, wFlags, pDispParams, + pvarResult, pExcepInfo, pArgErr); + return hr; +} + +// ------------------------------------------------------------------------ +// IDualRecordSafearrayTest Implementation +// ------------------------------------------------------------------------ + +// Return the first value record from a SAFEARRAY +HRESULT __stdcall CRecordSafearrayTest::FirstValueRecord( + SAFEARRAY* records, StructValueRecordSafearrayTest* record) +{ + trace("FirstValueRecord called"); + if (record == NULL) + return E_POINTER; + + // Zero out the output record first (COM out parameter rule) + memset(record, 0, sizeof(StructValueRecordSafearrayTest)); + + if (records == NULL) + return E_POINTER; + + // Validate dimensions + UINT dims = ::SafeArrayGetDim(records); + if (dims != 1) + return DISP_E_TYPEMISMATCH; + + // Validate VARTYPE + VARTYPE vt; + HRESULT hr = ::SafeArrayGetVartype(records, &vt); + if (FAILED(hr) || vt != VT_RECORD) + return DISP_E_TYPEMISMATCH; + + // Get IRecordInfo + IRecordInfo* pRecordInfo = NULL; + hr = ::SafeArrayGetRecordInfo(records, &pRecordInfo); + if (FAILED(hr) || pRecordInfo == NULL) + return DISP_E_TYPEMISMATCH; + + // Validate IID + GUID actual_guid; + hr = pRecordInfo->GetGuid(&actual_guid); + if (FAILED(hr) || !IsEqualGUID(actual_guid, GUID_StructValueRecordSafearrayTest)) + { + pRecordInfo->Release(); + return DISP_E_TYPEMISMATCH; + } + + // Get bounds + LONG lbound, ubound; + hr = ::SafeArrayGetLBound(records, 1, &lbound); + if (FAILED(hr)) { pRecordInfo->Release(); return hr; } + hr = ::SafeArrayGetUBound(records, 1, &ubound); + if (FAILED(hr)) { pRecordInfo->Release(); return hr; } + + LONG count = ubound - lbound + 1; + if (count <= 0) + { + pRecordInfo->Release(); + return E_INVALIDARG; + } + + // Access data and copy first element using RecordCopy + void* pvData = NULL; + hr = ::SafeArrayAccessData(records, &pvData); + if (FAILED(hr)) + { + pRecordInfo->Release(); + return hr; + } + + // Use RecordCopy to properly copy the record (handles BSTR/pointers) + hr = pRecordInfo->RecordCopy(pvData, record); + ::SafeArrayUnaccessData(records); + pRecordInfo->Release(); + + return hr; +} + +// Return the first BSTR record from a SAFEARRAY +HRESULT __stdcall CRecordSafearrayTest::FirstBstrRecord( + SAFEARRAY* records, StructBstrRecordSafearrayTest* record) +{ + trace("FirstBstrRecord called"); + if (record == NULL) + return E_POINTER; + + // Zero out the output record first (COM out parameter rule) + memset(record, 0, sizeof(StructBstrRecordSafearrayTest)); + + if (records == NULL) + return E_POINTER; + + // Validate dimensions + UINT dims = ::SafeArrayGetDim(records); + if (dims != 1) + return DISP_E_TYPEMISMATCH; + + // Validate VARTYPE + VARTYPE vt; + HRESULT hr = ::SafeArrayGetVartype(records, &vt); + if (FAILED(hr) || vt != VT_RECORD) + return DISP_E_TYPEMISMATCH; + + // Get IRecordInfo + IRecordInfo* pRecordInfo = NULL; + hr = ::SafeArrayGetRecordInfo(records, &pRecordInfo); + if (FAILED(hr) || pRecordInfo == NULL) + return DISP_E_TYPEMISMATCH; + + // Validate IID + GUID actual_guid; + hr = pRecordInfo->GetGuid(&actual_guid); + if (FAILED(hr) || !IsEqualGUID(actual_guid, GUID_StructBstrRecordSafearrayTest)) + { + pRecordInfo->Release(); + return DISP_E_TYPEMISMATCH; + } + + // Get bounds + LONG lbound, ubound; + hr = ::SafeArrayGetLBound(records, 1, &lbound); + if (FAILED(hr)) { pRecordInfo->Release(); return hr; } + hr = ::SafeArrayGetUBound(records, 1, &ubound); + if (FAILED(hr)) { pRecordInfo->Release(); return hr; } + + LONG count = ubound - lbound + 1; + if (count <= 0) + { + pRecordInfo->Release(); + return E_INVALIDARG; + } + + // Access data and copy first element using RecordCopy + void* pvData = NULL; + hr = ::SafeArrayAccessData(records, &pvData); + if (FAILED(hr)) + { + pRecordInfo->Release(); + return hr; + } + + // Use RecordCopy to properly copy the record (handles BSTR/pointers) + hr = pRecordInfo->RecordCopy(pvData, record); + ::SafeArrayUnaccessData(records); + pRecordInfo->Release(); + + return hr; +} diff --git a/source/CppTestSrv/CoRecordSafearrayTest.h b/source/CppTestSrv/CoRecordSafearrayTest.h new file mode 100644 index 000000000..29c5bff3f --- /dev/null +++ b/source/CppTestSrv/CoRecordSafearrayTest.h @@ -0,0 +1,62 @@ +/* + * CoRecordSafearrayTest.h - Component for record SAFEARRAY round-trip tests + * + * This component implements two test pathways: + * 1. IDispatch dual vtable (IDualRecordSafearrayTest) + * 2. IDispatch dispinterface Invoke (IDispRecordSafearrayTest) + */ + +#ifndef __CoRecordSafearrayTest_h__ +#define __CoRecordSafearrayTest_h__ + +#include "Iface.h" +#include "CUnknown.h" + +// Forward declaration for dispinterface IID (not in MIDL-generated header) +EXTERN_C const IID DIID_IDispRecordSafearrayTest; + +/////////////////////////////////////////////////////////// +// +// Component for record SAFEARRAY round-trip tests +// +class CRecordSafearrayTest : public CUnknown, + public IDualRecordSafearrayTest +{ +public: + static HRESULT CreateInstance(IUnknown* pUnknownOuter, + CUnknown** ppNewComponent); + +private: + DECLARE_IUNKNOWN + virtual HRESULT __stdcall NondelegatingQueryInterface(const IID& iid, void** ppv); + + // Return the first record from a SAFEARRAY + virtual HRESULT __stdcall FirstValueRecord( + SAFEARRAY* records, + StructValueRecordSafearrayTest* record); + + // Return the first BSTR record from a SAFEARRAY + virtual HRESULT __stdcall FirstBstrRecord( + SAFEARRAY* records, + StructBstrRecordSafearrayTest* record); + + // IDispatch + virtual HRESULT __stdcall GetTypeInfoCount(UINT* pCountTypeInfo); + virtual HRESULT __stdcall GetTypeInfo(UINT iTypeInfo, LCID, ITypeInfo** ppITypeInfo); + virtual HRESULT __stdcall GetIDsOfNames(const IID& iid, OLECHAR** arrayNames, + UINT countNames, LCID, DISPID* arrayDispIDs); + virtual HRESULT __stdcall Invoke(DISPID dispidMember, const IID& iid, LCID, + WORD wFlags, DISPPARAMS* pDispParams, + VARIANT* pvarResult, EXCEPINFO* pExcepInfo, + UINT* pArgErr); + + // Initialization + virtual HRESULT Init(); + + CRecordSafearrayTest(IUnknown* pUnknownOuter); + ~CRecordSafearrayTest(); + + ITypeInfo* m_pITypeInfo; +}; + +#endif diff --git a/source/CppTestSrv/MAKEFILE b/source/CppTestSrv/MAKEFILE index 85c90d9af..29d510d97 100644 --- a/source/CppTestSrv/MAKEFILE +++ b/source/CppTestSrv/MAKEFILE @@ -41,7 +41,7 @@ guids.obj : guids.c # Component/server source files # -server.obj : server.cpp cunknown.h cfactory.h iface.h CoNumericSafearrayTest.h CoByteCompatibilityTest.h CoVariantErrorTest.h CoFixedPointNumberTest.h +server.obj : server.cpp cunknown.h cfactory.h iface.h CoNumericSafearrayTest.h CoByteCompatibilityTest.h CoVariantErrorTest.h CoFixedPointNumberTest.h CoRecordSafearrayTest.h cl $(CPP_FLAGS) server.cpp CoComtypesDispRecordParamTest.obj : CoComtypesDispRecordParamTest.cpp \ @@ -71,6 +71,10 @@ CoFixedPointNumberTest.obj : CoFixedPointNumberTest.cpp \ CoFixedPointNumberTest.h iface.h CUnknown.h cl $(CPP_FLAGS) CoFixedPointNumberTest.cpp +CoRecordSafearrayTest.obj : CoRecordSafearrayTest.cpp \ + CoRecordSafearrayTest.h iface.h CUnknown.h + cl $(CPP_FLAGS) CoRecordSafearrayTest.cpp + # # Helper classes # @@ -105,6 +109,7 @@ SERVER_OBJS = Server.obj \ CoStringSafearrayInsideVariantTest.obj \ CoVariantErrorTest.obj \ CoFixedPointNumberTest.obj \ + CoRecordSafearrayTest.obj \ Registry.obj \ Cfactory.obj \ Cunknown.obj \ diff --git a/source/CppTestSrv/SERVER.CPP b/source/CppTestSrv/SERVER.CPP index 54d1089d8..8e357fe7d 100644 --- a/source/CppTestSrv/SERVER.CPP +++ b/source/CppTestSrv/SERVER.CPP @@ -15,6 +15,7 @@ #include "CoStringSafearrayInsideVariantTest.h" #include "CoVariantErrorTest.h" #include "CoFixedPointNumberTest.h" +#include "CoRecordSafearrayTest.h" /////////////////////////////////////////////////////////// @@ -82,6 +83,12 @@ CFactoryData g_FactoryDataArray[] = L"Comtypes.FixedPointNumberTest.1", L"Comtypes.FixedPointNumberTest", &LIBID_ComtypesCppTestSrvLib, + NULL, 0}, + {&CLSID_CoRecordSafearrayTest, CRecordSafearrayTest::CreateInstance, + L"Record SAFEARRAY round-trip test component", + L"Comtypes.RecordSafeArrayRoundTripTest.1", + L"Comtypes.RecordSafeArrayRoundTripTest", + &LIBID_ComtypesCppTestSrvLib, NULL, 0} } ; int g_cFactoryDataEntries diff --git a/source/CppTestSrv/SERVER.IDL b/source/CppTestSrv/SERVER.IDL index 465cf3097..848a7db5d 100644 --- a/source/CppTestSrv/SERVER.IDL +++ b/source/CppTestSrv/SERVER.IDL @@ -149,6 +149,64 @@ interface IFixedPointNumberTest : IUnknown HRESULT InitDecVariantWith3dot14([in, out] VARIANT* value); } ; + +// Value-only record for basic SAFEARRAY round-trip tests +typedef [uuid(6B298B7B-CA0E-431D-988C-FED3CEB59729)] +struct StructValueRecordSafearrayTest { + long id; + long seq; + hyper checksum; +} StructValueRecordSafearrayTest; + + +// BSTR-containing record for deep copy validation +typedef [uuid(5F72F918-EC40-42C1-A034-F6608902ECB3)] +struct StructBstrRecordSafearrayTest { + long id; + long seq; + BSTR text; + hyper checksum; +} StructBstrRecordSafearrayTest; + + +// IDispatch dual interface for typed record SAFEARRAY +[ + odl, + uuid(000CB4A0-9492-4E7E-9D1F-E19512B0FA3A), + dual, + helpstring("Dual interface for typed record SAFEARRAY round-trip tests."), + nonextensible, + oleautomation +] +interface IDualRecordSafearrayTest : IDispatch +{ + // Return the first record from a SAFEARRAY + [id(1)] + HRESULT FirstValueRecord( + [in] SAFEARRAY(StructValueRecordSafearrayTest) records, + [out, retval] StructValueRecordSafearrayTest* record + ); + + // Return the first BSTR record from a SAFEARRAY + [id(2)] + HRESULT FirstBstrRecord( + [in] SAFEARRAY(StructBstrRecordSafearrayTest) records, + [out, retval] StructBstrRecordSafearrayTest* record + ); +}; + + +// Dispinterface alias for dual interface +[ + uuid(E5D25FDE-3FF5-42A4-BAD0-44BB502D007E), + helpstring("Dispinterface alias for dual typed record SAFEARRAY tests.") +] +dispinterface IDispRecordSafearrayTest +{ + interface IDualRecordSafearrayTest; +}; + + // // Component and type library descriptions // @@ -265,4 +323,17 @@ library ComtypesCppTestSrvLib { [default] interface IFixedPointNumberTest ; } ; + + + // CoRecordSafearrayTest + // Component that implements record SAFEARRAY round-trip tests. + [ + uuid(1B9EB37E-1E97-4866-8600-C1F1D3620FAE), + helpstring("Record SAFEARRAY round-trip test component.") + ] + coclass CoRecordSafearrayTest + { + [default] interface IDualRecordSafearrayTest ; + dispinterface IDispRecordSafearrayTest ; + } ; } ; From c7f1b7607c7891ed35bc459cef010f5a898004e1 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 3 Nov 2026 01:14:26 +0900 Subject: [PATCH 2/4] test: Add wrap record tests for BSTR and value records in `SAFEARRAY`. --- comtypes/test/test_record_safearray_bstr.py | 25 ++++ comtypes/test/test_record_safearray_value.py | 24 ++++ source/CppTestSrv/CoRecordSafearrayTest.cpp | 134 +++++++++++++++++++ source/CppTestSrv/CoRecordSafearrayTest.h | 10 ++ source/CppTestSrv/SERVER.IDL | 18 ++- 5 files changed, 209 insertions(+), 2 deletions(-) diff --git a/comtypes/test/test_record_safearray_bstr.py b/comtypes/test/test_record_safearray_bstr.py index faac344b6..cdba1a734 100644 --- a/comtypes/test/test_record_safearray_bstr.py +++ b/comtypes/test/test_record_safearray_bstr.py @@ -177,6 +177,20 @@ class TestVtableBstrRecordSafeArray(_BstrRecordScenariosMixin, unittest.TestCase def setUpClass(cls): cls.interface = ComtypesCppTestSrvLib.IDualRecordSafearrayTest + def test_wrap_record(self): + ifc = _create_server(self.interface) + record = _create_record(id=500, seq=0, text="WrapBstr-500-0", checksum=50708) + + result = ifc.WrapBstrRecord(record) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].id, 500) + self.assertEqual(result[0].seq, 0) + self.assertEqual(str(result[0].text), "WrapBstr-500-0") + self.assertEqual(result[0].checksum, 50708) + + del record + gc.collect() + def test_first_record_length_1_list(self): ifc = _create_server(self.interface) records = [ @@ -259,6 +273,17 @@ class TestDispatchBstrRecordSafeArray(_BstrRecordScenariosMixin, unittest.TestCa def setUpClass(cls): cls.interface = ComtypesCppTestSrvLib.IDispRecordSafearrayTest + def test_wrap_record(self): + ifc = _create_server(self.interface) + record = _create_record(id=500, seq=0, text="WrapBstr-500-0", checksum=50708) + + with self.assertRaises(KeyError) as cm: + ifc.WrapBstrRecord(record) + self.assertEqual(str(cm.exception), "36") + + del record + gc.collect() + def test_first_record_length_1_list(self): ifc = _create_server(self.interface) records = [ diff --git a/comtypes/test/test_record_safearray_value.py b/comtypes/test/test_record_safearray_value.py index 973c21b47..86e7c9b3e 100644 --- a/comtypes/test/test_record_safearray_value.py +++ b/comtypes/test/test_record_safearray_value.py @@ -180,6 +180,19 @@ class TestVtableValueRecordSafeArray(_ValueRecordScenariosMixin, unittest.TestCa def setUpClass(cls): cls.interface = ComtypesCppTestSrvLib.IDualRecordSafearrayTest + def test_wrap_record(self): + ifc = _create_server(self.interface) + record = _create_record(id=300, seq=0, checksum=30042) + + result = ifc.WrapValueRecord(record) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].id, 300) + self.assertEqual(result[0].seq, 0) + self.assertEqual(result[0].checksum, 30042) + + del record + gc.collect() + def test_first_record_length_1_list(self): ifc = _create_server(self.interface) records = [ @@ -255,6 +268,17 @@ class TestDispatchValueRecordSafeArray(_ValueRecordScenariosMixin, unittest.Test def setUpClass(cls): cls.interface = ComtypesCppTestSrvLib.IDispRecordSafearrayTest + def test_wrap_record(self): + ifc = _create_server(self.interface) + record = _create_record(id=300, seq=0, checksum=30042) + + with self.assertRaises(KeyError) as cm: + ifc.WrapValueRecord(record) + self.assertEqual(str(cm.exception), "36") + + del record + gc.collect() + def test_first_record_length_1_list(self): ifc = _create_server(self.interface) records = [ diff --git a/source/CppTestSrv/CoRecordSafearrayTest.cpp b/source/CppTestSrv/CoRecordSafearrayTest.cpp index 1a573886d..fa42b42d2 100644 --- a/source/CppTestSrv/CoRecordSafearrayTest.cpp +++ b/source/CppTestSrv/CoRecordSafearrayTest.cpp @@ -168,6 +168,73 @@ HRESULT __stdcall CRecordSafearrayTest::Invoke( // IDualRecordSafearrayTest Implementation // ------------------------------------------------------------------------ +// Wrap a single value record into a SAFEARRAY of length 1 +HRESULT __stdcall CRecordSafearrayTest::WrapValueRecord( + StructValueRecordSafearrayTest* record, SAFEARRAY** records) +{ + trace("WrapValueRecord called"); + if (records == NULL) + return E_POINTER; + *records = NULL; + + if (record == NULL) + return E_POINTER; + + // Get IRecordInfo from typelib + IRecordInfo* pRecordInfo = NULL; + HRESULT hr = ::GetRecordInfoFromGuids( + LIBID_ComtypesCppTestSrvLib, 1, 0, 0, GUID_StructValueRecordSafearrayTest, &pRecordInfo); + if (FAILED(hr)) + { + trace("GetRecordInfoFromGuids failed", hr); + return hr; + } + + // Create SAFEARRAY with 1 element + SAFEARRAY* psa = ::SafeArrayCreateVectorEx(VT_RECORD, 0, 1, pRecordInfo); + if (psa == NULL) + { + pRecordInfo->Release(); + return E_OUTOFMEMORY; + } + + // Access data and copy the input record using RecordCopy + void* pvData = NULL; + hr = ::SafeArrayAccessData(psa, &pvData); + if (FAILED(hr)) + { + ::SafeArrayDestroy(psa); + pRecordInfo->Release(); + return hr; + } + + // Initialize destination element before RecordCopy (S5/N5) + // RecordCopy expects destination to be initialized; otherwise it may + // try to free garbage BSTR/pointer values. + hr = pRecordInfo->RecordInit(pvData); + if (FAILED(hr)) + { + ::SafeArrayUnaccessData(psa); + ::SafeArrayDestroy(psa); + pRecordInfo->Release(); + return hr; + } + + // Use RecordCopy to properly copy the record (handles BSTR/pointers) + hr = pRecordInfo->RecordCopy(record, pvData); + ::SafeArrayUnaccessData(psa); + pRecordInfo->Release(); + + if (FAILED(hr)) + { + ::SafeArrayDestroy(psa); + return hr; + } + + *records = psa; + return S_OK; +} + // Return the first value record from a SAFEARRAY HRESULT __stdcall CRecordSafearrayTest::FirstValueRecord( SAFEARRAY* records, StructValueRecordSafearrayTest* record) @@ -239,6 +306,73 @@ HRESULT __stdcall CRecordSafearrayTest::FirstValueRecord( return hr; } +// Wrap a single BSTR record into a SAFEARRAY of length 1 +HRESULT __stdcall CRecordSafearrayTest::WrapBstrRecord( + StructBstrRecordSafearrayTest* record, SAFEARRAY** records) +{ + trace("WrapBstrRecord called"); + if (records == NULL) + return E_POINTER; + *records = NULL; + + if (record == NULL) + return E_POINTER; + + // Get IRecordInfo from typelib + IRecordInfo* pRecordInfo = NULL; + HRESULT hr = ::GetRecordInfoFromGuids( + LIBID_ComtypesCppTestSrvLib, 1, 0, 0, GUID_StructBstrRecordSafearrayTest, &pRecordInfo); + if (FAILED(hr)) + { + trace("GetRecordInfoFromGuids failed", hr); + return hr; + } + + // Create SAFEARRAY with 1 element + SAFEARRAY* psa = ::SafeArrayCreateVectorEx(VT_RECORD, 0, 1, pRecordInfo); + if (psa == NULL) + { + pRecordInfo->Release(); + return E_OUTOFMEMORY; + } + + // Access data and copy the input record using RecordCopy + void* pvData = NULL; + hr = ::SafeArrayAccessData(psa, &pvData); + if (FAILED(hr)) + { + ::SafeArrayDestroy(psa); + pRecordInfo->Release(); + return hr; + } + + // Initialize destination element before RecordCopy (S5/N5) + // RecordCopy expects destination to be initialized; otherwise it may + // try to free garbage BSTR/pointer values. + hr = pRecordInfo->RecordInit(pvData); + if (FAILED(hr)) + { + ::SafeArrayUnaccessData(psa); + ::SafeArrayDestroy(psa); + pRecordInfo->Release(); + return hr; + } + + // Use RecordCopy to properly copy the record (handles BSTR/pointers) + hr = pRecordInfo->RecordCopy(record, pvData); + ::SafeArrayUnaccessData(psa); + pRecordInfo->Release(); + + if (FAILED(hr)) + { + ::SafeArrayDestroy(psa); + return hr; + } + + *records = psa; + return S_OK; +} + // Return the first BSTR record from a SAFEARRAY HRESULT __stdcall CRecordSafearrayTest::FirstBstrRecord( SAFEARRAY* records, StructBstrRecordSafearrayTest* record) diff --git a/source/CppTestSrv/CoRecordSafearrayTest.h b/source/CppTestSrv/CoRecordSafearrayTest.h index 29c5bff3f..1cacae0e8 100644 --- a/source/CppTestSrv/CoRecordSafearrayTest.h +++ b/source/CppTestSrv/CoRecordSafearrayTest.h @@ -30,11 +30,21 @@ class CRecordSafearrayTest : public CUnknown, DECLARE_IUNKNOWN virtual HRESULT __stdcall NondelegatingQueryInterface(const IID& iid, void** ppv); + // Wrap a single record into a SAFEARRAY of length 1 + virtual HRESULT __stdcall WrapValueRecord( + StructValueRecordSafearrayTest* record, + SAFEARRAY** records); + // Return the first record from a SAFEARRAY virtual HRESULT __stdcall FirstValueRecord( SAFEARRAY* records, StructValueRecordSafearrayTest* record); + // Wrap a single BSTR record into a SAFEARRAY of length 1 + virtual HRESULT __stdcall WrapBstrRecord( + StructBstrRecordSafearrayTest* record, + SAFEARRAY** records); + // Return the first BSTR record from a SAFEARRAY virtual HRESULT __stdcall FirstBstrRecord( SAFEARRAY* records, diff --git a/source/CppTestSrv/SERVER.IDL b/source/CppTestSrv/SERVER.IDL index 848a7db5d..ef251ee15 100644 --- a/source/CppTestSrv/SERVER.IDL +++ b/source/CppTestSrv/SERVER.IDL @@ -180,15 +180,29 @@ struct StructBstrRecordSafearrayTest { ] interface IDualRecordSafearrayTest : IDispatch { - // Return the first record from a SAFEARRAY + // Wrap a single record into a SAFEARRAY of length 1 [id(1)] + HRESULT WrapValueRecord( + [in] StructValueRecordSafearrayTest* record, + [out, retval] SAFEARRAY(StructValueRecordSafearrayTest)* records + ); + + // Return the first record from a SAFEARRAY + [id(2)] HRESULT FirstValueRecord( [in] SAFEARRAY(StructValueRecordSafearrayTest) records, [out, retval] StructValueRecordSafearrayTest* record ); + // Wrap a single BSTR record into a SAFEARRAY of length 1 + [id(3)] + HRESULT WrapBstrRecord( + [in] StructBstrRecordSafearrayTest* record, + [out, retval] SAFEARRAY(StructBstrRecordSafearrayTest)* records + ); + // Return the first BSTR record from a SAFEARRAY - [id(2)] + [id(4)] HRESULT FirstBstrRecord( [in] SAFEARRAY(StructBstrRecordSafearrayTest) records, [out, retval] StructBstrRecordSafearrayTest* record From ea46ef543807ca680f94513f4cb02410e38ecfa0 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 3 Nov 2026 01:14:26 +0900 Subject: [PATCH 3/4] test: Add reverse records tests for BSTR and value records in `SAFEARRAY`. --- comtypes/test/test_record_safearray_bstr.py | 37 ++++++ comtypes/test/test_record_safearray_value.py | 37 ++++++ source/CppTestSrv/CoRecordSafearrayTest.cpp | 118 +++++++++++++++++++ source/CppTestSrv/CoRecordSafearrayTest.h | 10 ++ source/CppTestSrv/SERVER.IDL | 16 ++- 5 files changed, 216 insertions(+), 2 deletions(-) diff --git a/comtypes/test/test_record_safearray_bstr.py b/comtypes/test/test_record_safearray_bstr.py index cdba1a734..24c2bd63b 100644 --- a/comtypes/test/test_record_safearray_bstr.py +++ b/comtypes/test/test_record_safearray_bstr.py @@ -259,6 +259,26 @@ def test_first_record_empty_safearray_by_create(self): del records gc.collect() + def test_reverse_records_length_3(self): + ifc = _create_server(self.interface) + + records = [ + _create_record(id=401, seq=0, text="RecSafeArray-401-0", checksum=40808), + _create_record(id=402, seq=1, text="RecSafeArray-402-1", checksum=40918), + _create_record(id=403, seq=2, text="RecSafeArray-403-2", checksum=41028), + ] + lp = _get_bstr_record_sa_type().from_param(records) + + result = ifc.ReverseBstrRecords(lp) + self.assertEqual(len(result), 3) + self.assertEqual( + [(r.id, r.seq, r.text, r.checksum) for r in result], + [(s.id, s.seq, s.text, s.checksum) for s in reversed(records)], + ) + + del records + gc.collect() + # ====================================================================== # Dispatch pathway tests (IDispRecordSafearrayTest) @@ -352,6 +372,23 @@ def test_first_record_empty_safearray_by_create(self): del records gc.collect() + def test_reverse_records_length_3(self): + ifc = _create_server(self.interface) + + records = [ + _create_record(id=401, seq=0, text="RecSafeArray-401-0", checksum=40808), + _create_record(id=402, seq=1, text="RecSafeArray-402-1", checksum=40918), + _create_record(id=403, seq=2, text="RecSafeArray-403-2", checksum=41028), + ] + lp = _get_bstr_record_sa_type().from_param(records) + + with self.assertRaises(KeyError) as cm: + ifc.ReverseBstrRecords(lp) + self.assertEqual(str(cm.exception), "36") + + del records + gc.collect() + if __name__ == "__main__": unittest.main() diff --git a/comtypes/test/test_record_safearray_value.py b/comtypes/test/test_record_safearray_value.py index 86e7c9b3e..5c41f9e8a 100644 --- a/comtypes/test/test_record_safearray_value.py +++ b/comtypes/test/test_record_safearray_value.py @@ -259,6 +259,26 @@ def test_first_record_empty_safearray_by_create(self): del records gc.collect() + def test_reverse_records_length_3(self): + ifc = _create_server(self.interface) + + records = [ + _create_record(id=201, seq=0, checksum=20142), + _create_record(id=202, seq=1, checksum=20252), + _create_record(id=203, seq=2, checksum=20362), + ] + lp = _get_value_record_sa_type().from_param(records) + + result = ifc.ReverseValueRecords(lp) + self.assertEqual(len(result), 3) + self.assertEqual( + [(r.id, r.seq, r.checksum) for r in result], + [(s.id, s.seq, s.checksum) for s in reversed(records)], + ) + + del records + gc.collect() + @unittest.skipIf(IMPORT_FAILED, "This depends on the out of process COM-server.") class TestDispatchValueRecordSafeArray(_ValueRecordScenariosMixin, unittest.TestCase): @@ -346,6 +366,23 @@ def test_first_record_empty_safearray_by_create(self): del records gc.collect() + def test_reverse_records_length_3(self): + ifc = _create_server(self.interface) + + records = [ + _create_record(id=201, seq=0, checksum=20142), + _create_record(id=202, seq=1, checksum=20252), + _create_record(id=203, seq=2, checksum=20362), + ] + lp = _get_value_record_sa_type().from_param(records) + + with self.assertRaises(KeyError) as cm: + ifc.ReverseValueRecords(lp) + self.assertEqual(str(cm.exception), "36") + + del records + gc.collect() + if __name__ == "__main__": unittest.main() diff --git a/source/CppTestSrv/CoRecordSafearrayTest.cpp b/source/CppTestSrv/CoRecordSafearrayTest.cpp index fa42b42d2..f1186c684 100644 --- a/source/CppTestSrv/CoRecordSafearrayTest.cpp +++ b/source/CppTestSrv/CoRecordSafearrayTest.cpp @@ -30,6 +30,108 @@ static inline void trace(const char* msg) static inline void trace(const char* msg, HRESULT hr) { Util::Trace("CoRecordSafearrayTest", msg, hr); } +// ======================================================================== +// Helper Functions +// ======================================================================== + +// Return a reversed deep copy of a record SAFEARRAY. +static HRESULT ReverseRecords( + SAFEARRAY* records, const GUID& recGuid, SAFEARRAY** reversed) +{ + if (reversed == NULL) + return E_POINTER; + *reversed = NULL; + if (records == NULL) + return E_POINTER; + + if (::SafeArrayGetDim(records) != 1) + return DISP_E_TYPEMISMATCH; + + VARTYPE vt; + HRESULT hr = ::SafeArrayGetVartype(records, &vt); + if (FAILED(hr) || vt != VT_RECORD) + return DISP_E_TYPEMISMATCH; + + IRecordInfo* pRecordInfo = NULL; + hr = ::SafeArrayGetRecordInfo(records, &pRecordInfo); + if (FAILED(hr) || pRecordInfo == NULL) + return DISP_E_TYPEMISMATCH; + + GUID actual_guid; + hr = pRecordInfo->GetGuid(&actual_guid); + if (FAILED(hr) || !IsEqualGUID(actual_guid, recGuid)) + { + pRecordInfo->Release(); + return DISP_E_TYPEMISMATCH; + } + + LONG lbound, ubound; + hr = ::SafeArrayGetLBound(records, 1, &lbound); + if (FAILED(hr)) { pRecordInfo->Release(); return hr; } + hr = ::SafeArrayGetUBound(records, 1, &ubound); + if (FAILED(hr)) { pRecordInfo->Release(); return hr; } + + LONG count = ubound - lbound + 1; + if (count <= 0) + { + pRecordInfo->Release(); + return E_INVALIDARG; + } + + ULONG size = 0; + hr = pRecordInfo->GetSize(&size); + if (FAILED(hr)) { pRecordInfo->Release(); return hr; } + + SAFEARRAY* psa = ::SafeArrayCreateVectorEx(VT_RECORD, 0, count, pRecordInfo); + if (psa == NULL) + { + pRecordInfo->Release(); + return E_OUTOFMEMORY; + } + + void* pvSrc = NULL; + void* pvDst = NULL; + hr = ::SafeArrayAccessData(records, &pvSrc); + if (FAILED(hr)) + { + ::SafeArrayDestroy(psa); + pRecordInfo->Release(); + return hr; + } + hr = ::SafeArrayAccessData(psa, &pvDst); + if (FAILED(hr)) + { + ::SafeArrayUnaccessData(records); + ::SafeArrayDestroy(psa); + pRecordInfo->Release(); + return hr; + } + + BYTE* src = static_cast(pvSrc); + BYTE* dst = static_cast(pvDst); + for (LONG i = 0; i < count && SUCCEEDED(hr); ++i) + { + BYTE* dstElem = dst + (size_t)i * size; + BYTE* srcElem = src + (size_t)(count - 1 - i) * size; + hr = pRecordInfo->RecordInit(dstElem); + if (SUCCEEDED(hr)) + hr = pRecordInfo->RecordCopy(srcElem, dstElem); + } + + ::SafeArrayUnaccessData(psa); + ::SafeArrayUnaccessData(records); + pRecordInfo->Release(); + + if (FAILED(hr)) + { + ::SafeArrayDestroy(psa); + return hr; + } + + *reversed = psa; + return S_OK; +} + // ======================================================================== // CRecordSafearrayTest Implementation // ======================================================================== @@ -306,6 +408,14 @@ HRESULT __stdcall CRecordSafearrayTest::FirstValueRecord( return hr; } +// Return a reversed deep copy of a record SAFEARRAY. +HRESULT __stdcall CRecordSafearrayTest::ReverseValueRecords( + SAFEARRAY* records, SAFEARRAY** reversed) +{ + trace("ReverseValueRecords called"); + return ReverseRecords(records, GUID_StructValueRecordSafearrayTest, reversed); +} + // Wrap a single BSTR record into a SAFEARRAY of length 1 HRESULT __stdcall CRecordSafearrayTest::WrapBstrRecord( StructBstrRecordSafearrayTest* record, SAFEARRAY** records) @@ -443,3 +553,11 @@ HRESULT __stdcall CRecordSafearrayTest::FirstBstrRecord( return hr; } + +// Return a reversed deep copy of a record SAFEARRAY. +HRESULT __stdcall CRecordSafearrayTest::ReverseBstrRecords( + SAFEARRAY* records, SAFEARRAY** reversed) +{ + trace("ReverseBstrRecords called"); + return ReverseRecords(records, GUID_StructBstrRecordSafearrayTest, reversed); +} diff --git a/source/CppTestSrv/CoRecordSafearrayTest.h b/source/CppTestSrv/CoRecordSafearrayTest.h index 1cacae0e8..e8dcf510a 100644 --- a/source/CppTestSrv/CoRecordSafearrayTest.h +++ b/source/CppTestSrv/CoRecordSafearrayTest.h @@ -40,6 +40,11 @@ class CRecordSafearrayTest : public CUnknown, SAFEARRAY* records, StructValueRecordSafearrayTest* record); + // Return a reversed deep copy of a record SAFEARRAY. + virtual HRESULT __stdcall ReverseValueRecords( + SAFEARRAY* records, + SAFEARRAY** reversed); + // Wrap a single BSTR record into a SAFEARRAY of length 1 virtual HRESULT __stdcall WrapBstrRecord( StructBstrRecordSafearrayTest* record, @@ -50,6 +55,11 @@ class CRecordSafearrayTest : public CUnknown, SAFEARRAY* records, StructBstrRecordSafearrayTest* record); + // Return a reversed deep copy of a record SAFEARRAY. + virtual HRESULT __stdcall ReverseBstrRecords( + SAFEARRAY* records, + SAFEARRAY** reversed); + // IDispatch virtual HRESULT __stdcall GetTypeInfoCount(UINT* pCountTypeInfo); virtual HRESULT __stdcall GetTypeInfo(UINT iTypeInfo, LCID, ITypeInfo** ppITypeInfo); diff --git a/source/CppTestSrv/SERVER.IDL b/source/CppTestSrv/SERVER.IDL index ef251ee15..9c9635fa8 100644 --- a/source/CppTestSrv/SERVER.IDL +++ b/source/CppTestSrv/SERVER.IDL @@ -194,19 +194,31 @@ interface IDualRecordSafearrayTest : IDispatch [out, retval] StructValueRecordSafearrayTest* record ); - // Wrap a single BSTR record into a SAFEARRAY of length 1 [id(3)] + HRESULT ReverseValueRecords( + [in] SAFEARRAY(StructValueRecordSafearrayTest) records, + [out, retval] SAFEARRAY(StructValueRecordSafearrayTest)* reversed + ); + + // Wrap a single BSTR record into a SAFEARRAY of length 1 + [id(4)] HRESULT WrapBstrRecord( [in] StructBstrRecordSafearrayTest* record, [out, retval] SAFEARRAY(StructBstrRecordSafearrayTest)* records ); // Return the first BSTR record from a SAFEARRAY - [id(4)] + [id(5)] HRESULT FirstBstrRecord( [in] SAFEARRAY(StructBstrRecordSafearrayTest) records, [out, retval] StructBstrRecordSafearrayTest* record ); + + [id(6)] + HRESULT ReverseBstrRecords( + [in] SAFEARRAY(StructBstrRecordSafearrayTest) records, + [out, retval] SAFEARRAY(StructBstrRecordSafearrayTest)* reversed + ); }; From 38bfe8df8eaf64b52ed363036762708f1af68b6b Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 3 Nov 2026 01:14:26 +0900 Subject: [PATCH 4/4] test: Mark dispatch record `SAFEARRAY` failures as characterization of #63. Some of the dispatch-path tests that use `assertRaises` pin down the current behavior, which is a known limitation rather than the intended contract. Unlike the cases where the server legitimately rejects the input (e.g. empty arrays returning `E_INVALIDARG`), those should become normal-path tests once #63 is fixed. The `CHARACTERIZATION(#63)` message is reported only when the call no longer raises, which is exactly when someone has fixed the issue, so the failure itself tells them what to do next. --- comtypes/test/test_record_safearray_bstr.py | 28 +++++++++++++++++--- comtypes/test/test_record_safearray_value.py | 28 +++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/comtypes/test/test_record_safearray_bstr.py b/comtypes/test/test_record_safearray_bstr.py index 24c2bd63b..cc4448b3f 100644 --- a/comtypes/test/test_record_safearray_bstr.py +++ b/comtypes/test/test_record_safearray_bstr.py @@ -297,7 +297,12 @@ def test_wrap_record(self): ifc = _create_server(self.interface) record = _create_record(id=500, seq=0, text="WrapBstr-500-0", checksum=50708) - with self.assertRaises(KeyError) as cm: + with self.assertRaises( + KeyError, + msg="CHARACTERIZATION(#63): " + "If https://github.com/enthought/comtypes/issues/63 is fixed, " + "this failure is expected; change it to a normal-path test.", + ) as cm: ifc.WrapBstrRecord(record) self.assertEqual(str(cm.exception), "36") @@ -310,7 +315,12 @@ def test_first_record_length_1_list(self): _create_record(id=400, seq=0, text="RecSafeArray-400-0", checksum=40708), ] - with self.assertRaises(COMError) as cm: + with self.assertRaises( + COMError, + msg="CHARACTERIZATION(#63): " + "If https://github.com/enthought/comtypes/issues/63 is fixed, " + "this failure is expected; change it to a normal-path test.", + ) as cm: ifc.FirstBstrRecord(records) self.assertEqual(cm.exception.hresult, DISP_E_TYPEMISMATCH) @@ -325,7 +335,12 @@ def test_first_record_length_3_list(self): _create_record(id=403, seq=2, text="RecSafeArray-403-2", checksum=41028), ] - with self.assertRaises(COMError) as cm: + with self.assertRaises( + COMError, + msg="CHARACTERIZATION(#63): " + "If https://github.com/enthought/comtypes/issues/63 is fixed, " + "this failure is expected; change it to a normal-path test.", + ) as cm: ifc.FirstBstrRecord(records) self.assertEqual(cm.exception.hresult, DISP_E_TYPEMISMATCH) @@ -382,7 +397,12 @@ def test_reverse_records_length_3(self): ] lp = _get_bstr_record_sa_type().from_param(records) - with self.assertRaises(KeyError) as cm: + with self.assertRaises( + KeyError, + msg="CHARACTERIZATION(#63): " + "If https://github.com/enthought/comtypes/issues/63 is fixed, " + "this failure is expected; change it to a normal-path test.", + ) as cm: ifc.ReverseBstrRecords(lp) self.assertEqual(str(cm.exception), "36") diff --git a/comtypes/test/test_record_safearray_value.py b/comtypes/test/test_record_safearray_value.py index 5c41f9e8a..74259322b 100644 --- a/comtypes/test/test_record_safearray_value.py +++ b/comtypes/test/test_record_safearray_value.py @@ -292,7 +292,12 @@ def test_wrap_record(self): ifc = _create_server(self.interface) record = _create_record(id=300, seq=0, checksum=30042) - with self.assertRaises(KeyError) as cm: + with self.assertRaises( + KeyError, + msg="CHARACTERIZATION(#63): " + "If https://github.com/enthought/comtypes/issues/63 is fixed, " + "this failure is expected; change it to a normal-path test.", + ) as cm: ifc.WrapValueRecord(record) self.assertEqual(str(cm.exception), "36") @@ -305,7 +310,12 @@ def test_first_record_length_1_list(self): _create_record(id=200, seq=0, checksum=20042), ] - with self.assertRaises(COMError) as cm: + with self.assertRaises( + COMError, + msg="CHARACTERIZATION(#63): " + "If https://github.com/enthought/comtypes/issues/63 is fixed, " + "this failure is expected; change it to a normal-path test.", + ) as cm: ifc.FirstValueRecord(records) self.assertEqual(cm.exception.hresult, DISP_E_TYPEMISMATCH) @@ -319,7 +329,12 @@ def test_first_record_length_3_list(self): _create_record(id=202, seq=1, checksum=20252), _create_record(id=203, seq=2, checksum=20362), ] - with self.assertRaises(COMError) as cm: + with self.assertRaises( + COMError, + msg="CHARACTERIZATION(#63): " + "If https://github.com/enthought/comtypes/issues/63 is fixed, " + "this failure is expected; change it to a normal-path test.", + ) as cm: ifc.FirstValueRecord(records) self.assertEqual(cm.exception.hresult, DISP_E_TYPEMISMATCH) @@ -376,7 +391,12 @@ def test_reverse_records_length_3(self): ] lp = _get_value_record_sa_type().from_param(records) - with self.assertRaises(KeyError) as cm: + with self.assertRaises( + KeyError, + msg="CHARACTERIZATION(#63): " + "If https://github.com/enthought/comtypes/issues/63 is fixed, " + "this failure is expected; change it to a normal-path test.", + ) as cm: ifc.ReverseValueRecords(lp) self.assertEqual(str(cm.exception), "36")