diff --git a/comtypes/test/test_compatibility_tlbparser_VT_I1_assignment.py b/comtypes/test/test_compatibility_tlbparser_VT_I1_assignment.py new file mode 100644 index 000000000..134cce847 --- /dev/null +++ b/comtypes/test/test_compatibility_tlbparser_VT_I1_assignment.py @@ -0,0 +1,445 @@ +"""Compare the VT_I1 mappings across the native COM boundary. + +Mapping and compatibility discussion: https://github.com/enthought/comtypes/issues/969 +""" + +import unittest +from ctypes import ( + HRESULT, + POINTER, + ArgumentError, + byref, + c_byte, + c_char, + c_char_p, + c_ubyte, + c_void_p, + cast, + create_string_buffer, + pointer, +) +from typing import Any, Protocol + +from comtypes import CLSCTX_LOCAL_SERVER, COMMETHOD, GUID, IUnknown +from comtypes.client import CreateObject, GetModule +from comtypes.malloc import _CoTaskMemFree + +try: + GetModule(("{07D2AEE5-1DF8-4D2C-953A-554ADFD25F99}", 1, 0, 0)) + from comtypes.gen import ComtypesCppTestSrvLib + + IMPORT_SERVER_FAILED = False +except (ImportError, OSError): + IMPORT_SERVER_FAILED = True + + +STRING = c_char_p + +IID_IByteCompatibilityTest = GUID("{047E34C1-33A8-4DAA-A1E5-B941756A8319}") +PROPOSED_COMMETHODS = [ + COMMETHOD( + [], + HRESULT, + "EchoInt8", + (["in"], c_byte, "value"), + (["out", "retval"], POINTER(c_byte), "result"), + ), + COMMETHOD( + [], + HRESULT, + "EchoUint8", + (["in"], c_ubyte, "value"), + (["out", "retval"], POINTER(c_ubyte), "result"), + ), + COMMETHOD( + [], + HRESULT, + "EchoLpStr", + (["in"], STRING, "value"), + (["out", "retval"], POINTER(STRING), "result"), + ), + COMMETHOD( + [], + HRESULT, + "ReadCharPointer", + (["in"], POINTER(c_byte), "value"), + (["out", "retval"], POINTER(c_ubyte), "result"), + ), + COMMETHOD( + [], + HRESULT, + "GetCharacterBytes", + (["in"], c_ubyte, "value"), + (["out", "retval"], POINTER(c_byte), "result"), + ), +] +LEGACY_COMMETHODS = [ + COMMETHOD( + [], + HRESULT, + "EchoInt8", + (["in"], c_char, "value"), + (["out", "retval"], STRING, "result"), + ), + COMMETHOD( + [], + HRESULT, + "EchoUint8", + (["in"], c_ubyte, "value"), + (["out", "retval"], POINTER(c_ubyte), "result"), + ), + COMMETHOD( + [], + HRESULT, + "EchoLpStr", + (["in"], STRING, "value"), + (["out", "retval"], POINTER(STRING), "result"), + ), + COMMETHOD( + [], + HRESULT, + "ReadCharPointer", + (["in"], STRING, "value"), + (["out", "retval"], POINTER(c_ubyte), "result"), + ), + COMMETHOD( + [], + HRESULT, + "GetCharacterBytes", + (["in"], c_ubyte, "value"), + (["out", "retval"], STRING, "result"), + ), +] + +# The generated definitions are checked in TestGeneratedMapping below. + + +class _PByteCompatibilityTest(Protocol): + def EchoInt8(self, value: Any) -> Any: ... + def EchoUint8(self, value: Any) -> Any: ... + def EchoLpStr(self, value: Any) -> Any: ... + def ReadCharPointer(self, value: Any) -> Any: ... + def GetCharacterBytes(self, value: Any) -> Any: ... + + +def create_legacy_automatic_int8_output() -> type[_PByteCompatibilityTest]: + class IByteCompatibilityTest(IUnknown): + _case_insensitive_ = True + _iid_ = IID_IByteCompatibilityTest + _idlflags_ = ["oleautomation"] + _methods_ = LEGACY_COMMETHODS + + def EchoLpStr(self, value): + result = c_char_p() + try: + self.__com_EchoLpStr(value, byref(result)) + return result.value + finally: + _CoTaskMemFree(cast(result, c_void_p)) + + def GetCharacterBytes(self, value): + result = create_string_buffer(2) + self.__com_GetCharacterBytes(value, result) + return result.value + + return IByteCompatibilityTest + + +def create_legacy_buffered_int8_output() -> type[_PByteCompatibilityTest]: + class IByteCompatibilityTest(create_legacy_automatic_int8_output()): + def EchoInt8(self, value): + # STRING cannot allocate a scalar output. Supply character storage + # and retain one byte, including a possible NUL value. + result = create_string_buffer(2) + self._IByteCompatibilityTest__com_EchoInt8(value, result) + return result.raw[:1] + + return IByteCompatibilityTest + + +def create_proposed_itf() -> type[_PByteCompatibilityTest]: + class IByteCompatibilityTest(IUnknown): + _case_insensitive_ = True + _iid_ = IID_IByteCompatibilityTest + _idlflags_ = ["oleautomation"] + _methods_ = PROPOSED_COMMETHODS + + def EchoLpStr(self, value): + result = c_char_p() + try: + self.__com_EchoLpStr(value, byref(result)) + return result.value + finally: + _CoTaskMemFree(cast(result, c_void_p)) + + return IByteCompatibilityTest + + +def create_proposed_char_ptr_output_buf() -> type[_PByteCompatibilityTest]: + class IByteCompatibilityTest(IUnknown): + _case_insensitive_ = True + _iid_ = IID_IByteCompatibilityTest + _idlflags_ = ["oleautomation"] + _methods_ = PROPOSED_COMMETHODS + + def EchoLpStr(self, value): + result = c_char_p() + try: + self.__com_EchoLpStr(value, byref(result)) + return result.value + finally: + _CoTaskMemFree(cast(result, c_void_p)) + + def GetCharacterBytes(self, value): + result = pointer(c_byte()) + self.__com_GetCharacterBytes(value, result) + return result.contents.value + + return IByteCompatibilityTest + + +def create_proposed_char_ref_output_buf() -> type[_PByteCompatibilityTest]: + class IByteCompatibilityTest(IUnknown): + _case_insensitive_ = True + _iid_ = IID_IByteCompatibilityTest + _idlflags_ = ["oleautomation"] + _methods_ = PROPOSED_COMMETHODS + + def EchoLpStr(self, value): + result = c_char_p() + try: + self.__com_EchoLpStr(value, byref(result)) + return result.value + finally: + _CoTaskMemFree(cast(result, c_void_p)) + + def GetCharacterBytes(self, value): + result = c_byte() + self.__com_GetCharacterBytes(value, byref(result)) + return result.value + + return IByteCompatibilityTest + + +def create_proposed_arr_output_buf() -> type[_PByteCompatibilityTest]: + class IByteCompatibilityTest(IUnknown): + _case_insensitive_ = True + _iid_ = IID_IByteCompatibilityTest + _idlflags_ = ["oleautomation"] + _methods_ = PROPOSED_COMMETHODS + + def EchoLpStr(self, value): + result = c_char_p() + try: + self.__com_EchoLpStr(value, byref(result)) + return result.value + finally: + _CoTaskMemFree(cast(result, c_void_p)) + + def GetCharacterBytes(self, value): + result = (c_byte * 1)() + self.__com_GetCharacterBytes(value, result) + return result[0] + + return IByteCompatibilityTest + + +def create_server(interface: type[_PByteCompatibilityTest]) -> _PByteCompatibilityTest: + return CreateObject( + ComtypesCppTestSrvLib.CoByteCompatibilityTest, + clsctx=CLSCTX_LOCAL_SERVER, + interface=interface, + ) + + +@unittest.skipIf(IMPORT_SERVER_FAILED, "Requires the C++ out-of-process COM server.") +class ByteCompatibilityTest(unittest.TestCase): + def setUp(self): + self.legacy_automatic = create_server(create_legacy_automatic_int8_output()) + self.legacy_buffered = create_server(create_legacy_buffered_int8_output()) + self.proposed = { + "automatic": create_server(create_proposed_itf()), + "pointer": create_server(create_proposed_char_ptr_output_buf()), + "reference": create_server(create_proposed_char_ref_output_buf()), + "array": create_server(create_proposed_arr_output_buf()), + "generated": create_server(ComtypesCppTestSrvLib.IByteCompatibilityTest), + } + + +class TestGeneratedMapping(ByteCompatibilityTest): + def test_generated_mapping(self): + self.assertEqual( + ComtypesCppTestSrvLib.IByteCompatibilityTest._methods_, PROPOSED_COMMETHODS + ) + # The legacy mapping describes different Python argument/result types. + self.assertNotEqual( + ComtypesCppTestSrvLib.IByteCompatibilityTest._methods_, LEGACY_COMMETHODS + ) + + +class TestEchoInt8(ByteCompatibilityTest): + def test_takes_and_returns_as_intended(self): + for value, expected in ( + (b"\x80", b"\x80"), + (b"\xf9", b"\xf9"), + (b"\xff", b"\xff"), + (b"\0", b"\0"), + (b"\x01", b"\x01"), + (b"\x7f", b"\x7f"), + ): + with self.subTest(mapping="legacy_buffered", value=value): + self.assertEqual(self.legacy_buffered.EchoInt8(value), expected) + for name, server in self.proposed.items(): + for value, expected in ( + (-128, -128), + (-7, -7), + (-1, -1), + (0, 0), + (1, 1), + (127, 127), + ): + with self.subTest(mapping=name, value=value): + self.assertEqual(server.EchoInt8(value), expected) + + def test_raises_type_error(self): + for value in (b"\x80", b"\xf9", b"\xff", b"\0", b"\x01", b"\x7f"): + with self.subTest(mapping="legacy_automatic", value=value): + with self.assertRaises(TypeError): + self.legacy_automatic.EchoInt8(value) + + def test_raises_argument_error(self): + for name, server in self.proposed.items(): + for value in (b"\x80", b"h", b"\xff"): + with self.subTest(mapping=name, value=value): + with self.assertRaises(ArgumentError): + server.EchoInt8(value) + + +class TestEchoUint8(ByteCompatibilityTest): + def test_takes_and_returns_as_intended(self): + for name, server in [ + ("legacy_automatic", self.legacy_automatic), + ("legacy_buffered", self.legacy_buffered), + *self.proposed.items(), + ]: + for value, expected in ((0, 0), (1, 1), (42, 42), (128, 128), (255, 255)): + with self.subTest(mapping=name, value=value): + self.assertEqual(server.EchoUint8(value), expected) + + def test_raises_argument_error(self): + for name, server in [ + ("legacy_automatic", self.legacy_automatic), + ("legacy_buffered", self.legacy_buffered), + *self.proposed.items(), + ]: + with self.subTest(mapping=name): + with self.assertRaises(ArgumentError): + server.EchoUint8(object()) + + +class TestEchoLpStr(ByteCompatibilityTest): + def test_takes_and_returns_as_intended(self): + # Use helpers that free the native CoTaskMem allocation after copying it. + for name, server in [ + ("legacy_automatic", self.legacy_automatic), + ("legacy_buffered", self.legacy_buffered), + *[(n, s) for n, s in self.proposed.items() if n != "generated"], + ]: + for value, expected in ( + (b"", b""), + (b"hello", b"hello"), + (b"\x80\xff", b"\x80\xff"), + ): + with self.subTest(mapping=name, value=value): + self.assertEqual(server.EchoLpStr(value), expected) + + def test_raises_argument_error(self): + for name, server in [ + ("legacy_automatic", self.legacy_automatic), + ("legacy_buffered", self.legacy_buffered), + *self.proposed.items(), + ]: + with self.subTest(mapping=name): + with self.assertRaises(ArgumentError): + server.EchoLpStr(object()) + + +class TestReadCharPointer(ByteCompatibilityTest): + def test_takes_and_returns_as_intended(self): + for name, server in [ + ("legacy_automatic", self.legacy_automatic), + ("legacy_buffered", self.legacy_buffered), + ]: + for value, expected in ( + (b"\0", 0), + (b"h", 104), + (b"\x80", 128), + (b"\xff", 255), + ): + with self.subTest(mapping=name, value=value): + self.assertEqual(server.ReadCharPointer(value), expected) + for name, server in self.proposed.items(): + for value, expected in ((0, 0), (104, 104), (-128, 128), (-1, 255)): + for argument in ( + pointer(c_byte(value)), + byref(c_byte(value)), + (c_byte * 1)(value), + ): + with self.subTest(mapping=name, value=value, argument=argument): + self.assertEqual(server.ReadCharPointer(argument), expected) + + def test_raises_argument_error(self): + for name, server in [ + ("legacy_automatic", self.legacy_automatic), + ("legacy_buffered", self.legacy_buffered), + ]: + for argument in ( + pointer(c_byte(104)), + byref(c_byte(104)), + (c_byte * 1)(104), + ): + with self.subTest(mapping=name, argument=argument): + with self.assertRaises(ArgumentError): + server.ReadCharPointer(argument) + # This includes the generated interface: bytes input compatibility is + # changed, even though signed-byte pointers now work as intended. + for name, server in self.proposed.items(): + for value in (b"\0", b"h", b"\x80", b"\xff"): + with self.subTest(mapping=name, value=value): + with self.assertRaises(ArgumentError): + server.ReadCharPointer(value) + + +class TestGetCharacterBytes(ByteCompatibilityTest): + def test_takes_and_returns_as_intended(self): + for name, server in [ + ("legacy_automatic", self.legacy_automatic), + ("legacy_buffered", self.legacy_buffered), + ]: + for value, expected in ( + (0, b""), + (1, b"\x01"), + (104, b"h"), + (128, b"\x80"), + (255, b"\xff"), + ): + with self.subTest(mapping=name, value=value): + self.assertEqual(server.GetCharacterBytes(value), expected) + for name, server in self.proposed.items(): + for value, expected in ((0, 0), (1, 1), (104, 104), (128, -128), (255, -1)): + with self.subTest(mapping=name, value=value): + self.assertEqual(server.GetCharacterBytes(value), expected) + # The new signed numeric result is not the old bytes value. + self.assertNotEqual(server.GetCharacterBytes(value), bytes([value])) + + def test_raises_argument_error(self): + for name, server in self.proposed.items(): + with self.subTest(mapping=name): + result = create_string_buffer(2) + # Legacy character storage is incompatible with POINTER(c_byte). + with self.assertRaises(ArgumentError): + server._IByteCompatibilityTest__com_GetCharacterBytes(104, result) + + +if __name__ == "__main__": + unittest.main() diff --git a/comtypes/test/test_cpp_safearray.py b/comtypes/test/test_cpp_safearray.py index 1c8e77d2f..f39d5b575 100644 --- a/comtypes/test/test_cpp_safearray.py +++ b/comtypes/test/test_cpp_safearray.py @@ -35,6 +35,7 @@ def setUp(self): ) # Fixed expected values come from the C++ fixture, not Python input. self.cases_1d = [ + (self.server.GetInt8Array, "int8", (-7, 0, 42, -7, 0, 42)), (self.server.GetUint8Array, "uint8", (0, 42, 255, 0, 42, 255)), (self.server.GetInt16Array, "int16", (-7, 0, 42, -7, 0, 42)), (self.server.GetUint16Array, "uint16", (0, 42, 65535, 0, 42, 65535)), @@ -63,6 +64,7 @@ def setUp(self): ] # SAFEARRAY storage is column-major; the fixture has two rows. self.cases_2d = [ + (self.server.GetInt8Array, "int8", ((-7, 42, 0), (0, -7, 42))), (self.server.GetUint8Array, "uint8", ((0, 255, 42), (42, 0, 255))), (self.server.GetInt16Array, "int16", ((-7, 42, 0), (0, -7, 42))), (self.server.GetUint16Array, "uint16", ((0, 65535, 42), (42, 0, 65535))), @@ -140,18 +142,6 @@ def test_ndarray_values_2d(self): numpy.testing.assert_array_equal(result, expected) self.assertEqual(method(2, 6), expected) - def test_int8_codegen_1d(self): - # VT_I1 is currently parsed as c_char (see #935), whose pointer slice - # returns bytes. Check the real COM/code-generator path and raw bytes; - # signed int8/NumPy dtype assertions await the separate c_char/c_byte fix. - result = self.server.GetInt8Array(1, 6) - self.assertEqual(result, (249, 0, 42, 249, 0, 42)) - - def test_int8_codegen_2d(self): - # As above, 249 is the raw byte for -7, not a signed dtype assertion. - result = self.server.GetInt8Array(2, 6) - self.assertEqual(result, ((249, 42, 0), (0, 249, 42))) - if __name__ == "__main__": unittest.main() diff --git a/comtypes/tools/tlbparser.py b/comtypes/tools/tlbparser.py index cad940d7f..85a3ece20 100644 --- a/comtypes/tools/tlbparser.py +++ b/comtypes/tools/tlbparser.py @@ -21,6 +21,7 @@ def PTR(typ): # basic C data types, with size and alignment in bits char_type = typedesc.FundamentalType("char", 8, 8) +schar_type = typedesc.FundamentalType("signed char", 8, 8) uchar_type = typedesc.FundamentalType("unsigned char", 8, 8) wchar_t_type = typedesc.FundamentalType("wchar_t", 16, 16) short_type = typedesc.FundamentalType("short int", 16, 16) @@ -81,7 +82,8 @@ def midlSAFEARRAY(typ): automation.VT_VARIANT: VARIANT_type, # 12 automation.VT_UNKNOWN: PTR(IUNKNOWN_type), # 13 automation.VT_DECIMAL: DECIMAL_type, # 14 - automation.VT_I1: char_type, # 16 + # Signed-byte mapping and compatibility: https://github.com/enthought/comtypes/issues/969 + automation.VT_I1: schar_type, # 16 automation.VT_UI1: uchar_type, # 17 automation.VT_UI2: ushort_type, # 18 automation.VT_UI4: ulong_type, # 19 diff --git a/docs/source/byte_compatibility.rst b/docs/source/byte_compatibility.rst new file mode 100644 index 000000000..ff7cd9717 --- /dev/null +++ b/docs/source/byte_compatibility.rst @@ -0,0 +1,79 @@ +Signed 8-bit values from type libraries +======================================= + +This page describes the compatibility impact of the proposed ``VT_I1`` +type-library mapping change. The change is under review; the examples below +describe regenerated modules using that mapping. +The decision and compatibility discussion is tracked in +`issue #969 `_. + +Generated types +--------------- + +``GetModule`` now represents ``VT_I1`` with ``ctypes.c_byte`` (a signed 8-bit +integer) instead of ``ctypes.c_char``. This affects scalar values, pointers, +structure and union fields, and SAFEARRAY elements described by ``VT_I1`` +in a type library. For example, +an IDL ``small`` scalar and a plain ``char*`` pointer use this mapping. +Explicit string types such as ``LPSTR`` still generate ``STRING`` +(``ctypes.c_char_p``); ``VT_UI1`` still generates ``ctypes.c_ubyte``. + +The change applies when modules are generated again. Existing generated +source files and interfaces defined directly in Python retain their declared +types. Mixing old generated modules with new ones can therefore produce +different Python argument and return types for the same type library. + +Arguments +--------- + +Pass a Python integer in the range -128 to 127, or a ``ctypes.c_byte`` value, +to a signed 8-bit scalar argument. The generated retval for a signed scalar +is also numeric: an ``EchoInt8(-7)`` call returns the integer ``-7``. +Single-character ``bytes`` and ``ctypes.c_char`` arguments do not match the +new scalar type. + +A plain ``char*`` argument previously generated as ``STRING`` now generates +as ``POINTER(c_byte)``. Existing calls that pass ``b"h"`` to such an argument +raise ``ctypes.ArgumentError``. Use a pointer, a reference, or an array of +``c_byte`` values instead, according to the native method's buffer contract:: + + from ctypes import byref, c_byte, pointer + + value = c_byte(ord("h")) + server.ReadCharPointer(pointer(value)) + server.ReadCharPointer(byref(value)) + server.ReadCharPointer((c_byte * 1)(ord("h"))) + +These examples refer to a method that reads exactly one character. For +methods that read more data, provide the required buffer size and lifetime. +The pointer type alone does not describe a string's length or termination. +Explicit ``LPSTR`` arguments continue to accept ``bytes`` as before. + +Return values +------------- + +A plain ``char*`` output also changes from ``STRING`` to ``POINTER(c_byte)``. +For a method returning one character through that parameter, a generated +high-level call now returns an integer. The value 104 returns as 104, and +255 returns as -1. Under the old mapping the automatic output allocation +could instead fail with ``TypeError: c_char_p 'out' parameter must be passed +as default value``. + +Calls to the generated raw COM method previously accepted a writable +``ctypes.create_string_buffer`` for that output. The new pointer type rejects +that character array. Supply ``byref(c_byte())`` or a ``c_byte`` array for a +single-character output, and read the resulting signed number. To recover +the corresponding one-byte ``bytes`` value, use ``bytes([value & 0xff])``. + +For larger returned buffers, read the result using the native method's +documented length. ``ctypes.string_at(result, length)`` makes a ``bytes`` copy +from a pointer of known length. Use NUL-terminated string conversion only +when the native contract guarantees termination. A plain character pointer +does not guarantee a terminator. Free or release native allocations as +specified by that contract after copying the data. + +Explicit ``LPSTR`` output types retain their existing string conversion. +Changing interface declarations by hand should likewise follow the actual +native contract. Type libraries describe both numeric ``small`` and plain +character data using ``VT_I1``, so this mapping change cannot by itself +preserve every existing implicit string conversion. diff --git a/docs/source/index.rst b/docs/source/index.rst index b7c54777e..ff403eb71 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -30,6 +30,7 @@ Functionalities npsupport threading type_checking + byte_compatibility Links diff --git a/source/CppTestSrv/CoByteCompatibilityTest.cpp b/source/CppTestSrv/CoByteCompatibilityTest.cpp new file mode 100644 index 000000000..3f92b4c64 --- /dev/null +++ b/source/CppTestSrv/CoByteCompatibilityTest.cpp @@ -0,0 +1,84 @@ +#include +#include +#include "CoByteCompatibilityTest.h" + +HRESULT CByteCompatibilityTest::CreateInstance(IUnknown* pUnknownOuter, CUnknown** ppNewComponent) +{ + if (pUnknownOuter != NULL) + { + return CLASS_E_NOAGGREGATION ; + } + *ppNewComponent = new CByteCompatibilityTest(pUnknownOuter) ; + return S_OK ; +} + +HRESULT __stdcall CByteCompatibilityTest::NondelegatingQueryInterface(const IID& iid, void** ppv) +{ + if (iid == IID_IByteCompatibilityTest) + { + return FinishQI(static_cast(this), ppv) ; + } + return CUnknown::NondelegatingQueryInterface(iid, ppv) ; +} + +HRESULT __stdcall CByteCompatibilityTest::EchoInt8(char value, char* result) +{ + if (result == NULL) + { + return E_POINTER ; + } + *result = value ; + return S_OK ; +} + +HRESULT __stdcall CByteCompatibilityTest::EchoUint8(byte value, byte* result) +{ + if (result == NULL) + { + return E_POINTER ; + } + *result = value ; + return S_OK ; +} + +HRESULT __stdcall CByteCompatibilityTest::EchoLpStr(LPSTR value, LPSTR* result) +{ + if (result == NULL) + { + return E_POINTER ; + } + *result = NULL ; + if (value == NULL) + { + return E_INVALIDARG ; + } + size_t size = strlen(value) + 1 ; + *result = static_cast(CoTaskMemAlloc(size)) ; + if (*result == NULL) + { + return E_OUTOFMEMORY ; + } + memcpy(*result, value, size) ; + return S_OK ; +} + +HRESULT __stdcall CByteCompatibilityTest::ReadCharPointer(unsigned char* value, byte* result) +{ + if (value == NULL || result == NULL) + { + return E_POINTER ; + } + // A raw char* marshals one character, not a NUL-terminated string. + *result = static_cast(*value) ; + return S_OK ; +} + +HRESULT __stdcall CByteCompatibilityTest::GetCharacterBytes(byte value, unsigned char* result) +{ + if (result == NULL) + { + return E_POINTER ; + } + *result = value ; + return S_OK ; +} diff --git a/source/CppTestSrv/CoByteCompatibilityTest.h b/source/CppTestSrv/CoByteCompatibilityTest.h new file mode 100644 index 000000000..412dbad96 --- /dev/null +++ b/source/CppTestSrv/CoByteCompatibilityTest.h @@ -0,0 +1,20 @@ +// A separate COM component for byte and narrow-string compatibility. +#include "CUnknown.h" +#include "Iface.h" + +class CByteCompatibilityTest : public CUnknown, public IByteCompatibilityTest +{ +public: + static HRESULT CreateInstance(IUnknown* pUnknownOuter, CUnknown** ppNewComponent) ; + +private: + DECLARE_IUNKNOWN + virtual HRESULT __stdcall NondelegatingQueryInterface(const IID& iid, void** ppv) ; + virtual HRESULT __stdcall EchoInt8(char value, char* result) ; + virtual HRESULT __stdcall EchoUint8(byte value, byte* result) ; + virtual HRESULT __stdcall EchoLpStr(LPSTR value, LPSTR* result) ; + virtual HRESULT __stdcall ReadCharPointer(unsigned char* value, byte* result) ; + virtual HRESULT __stdcall GetCharacterBytes(byte value, unsigned char* result) ; + + CByteCompatibilityTest(IUnknown* pUnknownOuter) : CUnknown(pUnknownOuter) {} ; +} ; diff --git a/source/CppTestSrv/CoNumericSafearrayTest.h b/source/CppTestSrv/CoNumericSafearrayTest.h index 821a811bf..7224e01e3 100644 --- a/source/CppTestSrv/CoNumericSafearrayTest.h +++ b/source/CppTestSrv/CoNumericSafearrayTest.h @@ -12,8 +12,6 @@ class CNumericSafearrayTest : public CUnknown, public INumericSafearrayTest virtual HRESULT __stdcall NondelegatingQueryInterface(const IID& iid, void** ppv) ; // Interface INumericSafearrayTest - // VT_I1 has COM/code-generator smoke coverage; NumPy dtype coverage awaits - // the separate c_char/c_byte issue (#935). virtual HRESULT __stdcall GetInt8Array(short dimensions, long count, SAFEARRAY** result) ; virtual HRESULT __stdcall GetUint8Array(short dimensions, long count, SAFEARRAY** result) ; virtual HRESULT __stdcall GetInt16Array(short dimensions, long count, SAFEARRAY** result) ; diff --git a/source/CppTestSrv/MAKEFILE b/source/CppTestSrv/MAKEFILE index 13d66b1c3..d42893b46 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 CoVariantErrorTest.h +server.obj : server.cpp cunknown.h cfactory.h iface.h CoNumericSafearrayTest.h CoByteCompatibilityTest.h CoVariantErrorTest.h cl $(CPP_FLAGS) server.cpp CoComtypesDispRecordParamTest.obj : CoComtypesDispRecordParamTest.cpp \ @@ -56,6 +56,9 @@ CoNumericSafearrayTest.obj : CoNumericSafearrayTest.cpp \ CoNumericSafearrayTest.h iface.h CUnknown.h cl $(CPP_FLAGS) CoNumericSafearrayTest.cpp +CoByteCompatibilityTest.obj : CoByteCompatibilityTest.cpp \ + CoByteCompatibilityTest.h iface.h CUnknown.h + cl $(CPP_FLAGS) CoByteCompatibilityTest.cpp CoStringSafearrayInsideVariantTest.obj : CoStringSafearrayInsideVariantTest.cpp \ CoStringSafearrayInsideVariantTest.h iface.h registry.h CUnknown.h cl $(CPP_FLAGS) CoStringSafearrayInsideVariantTest.cpp @@ -94,6 +97,7 @@ SERVER_OBJS = Server.obj \ CoComtypesDispRecordParamTest.obj \ CoComtypesDispSafearrayParamTest.obj \ CoNumericSafearrayTest.obj \ + CoByteCompatibilityTest.obj \ CoStringSafearrayInsideVariantTest.obj \ CoVariantErrorTest.obj \ Registry.obj \ diff --git a/source/CppTestSrv/SERVER.CPP b/source/CppTestSrv/SERVER.CPP index 90b307958..433b27231 100644 --- a/source/CppTestSrv/SERVER.CPP +++ b/source/CppTestSrv/SERVER.CPP @@ -11,6 +11,7 @@ #include "CoComtypesDispRecordParamTest.h" #include "CoComtypesDispSafearrayParamTest.h" #include "CoNumericSafearrayTest.h" +#include "CoByteCompatibilityTest.h" #include "CoStringSafearrayInsideVariantTest.h" #include "CoVariantErrorTest.h" @@ -57,6 +58,12 @@ CFactoryData g_FactoryDataArray[] = L"Comtypes.NumericSafearrayTest", &LIBID_ComtypesCppTestSrvLib, NULL, 0}, + {&CLSID_CoByteCompatibilityTest, CByteCompatibilityTest::CreateInstance, + L"Comtypes component for byte and narrow-string compatibility tests", + L"Comtypes.ByteCompatibilityTest.1", + L"Comtypes.ByteCompatibilityTest", + &LIBID_ComtypesCppTestSrvLib, + NULL, 0}, {&CLSID_CoStringSafearrayInsideVariantTest, CStringSafearrayInsideVariantTest::CreateInstance, L"String SAFEARRAY inside VARIANT Tester", L"Comtypes.CoStringSafearrayInsideVariantTest.1", diff --git a/source/CppTestSrv/SERVER.IDL b/source/CppTestSrv/SERVER.IDL index 30f26ee54..c261ed3c7 100644 --- a/source/CppTestSrv/SERVER.IDL +++ b/source/CppTestSrv/SERVER.IDL @@ -88,6 +88,22 @@ interface INumericSafearrayTest : IUnknown HRESULT GetFloat64Array([in] short dimensions, [in] long count, [out, retval] SAFEARRAY(double)* result); } ; +// A separate interface for scalar input/output and string compatibility. +[ + object, + uuid(047E34C1-33A8-4DAA-A1E5-B941756A8319), + oleautomation +] +interface IByteCompatibilityTest : IUnknown +{ + HRESULT EchoInt8([in] small value, [out, retval] small* result); + HRESULT EchoUint8([in] byte value, [out, retval] byte* result); + HRESULT EchoLpStr([in] LPSTR value, [out, retval] LPSTR* result); + HRESULT ReadCharPointer([in] char* value, [out, retval] byte* result); + // Return one character through a plain char pointer. + HRESULT GetCharacterBytes([in] byte value, [out, retval] char* result); +} ; + // Interface IDispSafearrayParamTest [ uuid(4097A6D0-A111-40E2-BD0B-177B775A9496) @@ -165,6 +181,15 @@ library ComtypesCppTestSrvLib [default] interface INumericSafearrayTest ; } ; + [ + uuid(E07D71E8-C8A5-4B76-9A88-688DB10CF712), + helpstring("Comtypes component for byte and narrow-string compatibility tests.") + ] + coclass CoByteCompatibilityTest + { + [default] interface IByteCompatibilityTest ; + } ; + // Interface IStringSafearrayInsideVariantTest [ odl,