Skip to content

Support VT_ERROR variant type and optional parameters #325

Open

Description

@bennyrowland

Currently comtypes does not support variants with typecode 0xa (VT_ERROR). This is particularly important for servers supporting optional arguments for their methods, as an optional argument is replaced with a VT_ERROR variant with a particular error code (DISP_E_PARAMNOTFOUND).

Supporting VT_ERROR itself seems like a simple fix, as internally it is just an HRESULT, i.e. a 32-bit integer (where different parts of the bit field represent different components). As far as I can tell, the only change necessary is to modify VARIANT.__get_value() to have a VT_ERROR branch which duplicates the 32bit int pathway. comtypes currently has some support for creating HRESULTs from the individual components but does not have an actual class representing them, a more advanced fix would perhaps be to create an HRESULT/SCODE class that would allow the subfields to be examined.

However, the most important change required is to modify the machinery used in calling the Python functions on the server class so that if an argument is supplied with DISP_E_PARAMNOTFOUND then this can be removed from the argument list actually supplied to the server. I am not completely sure how to do this so would welcome some support in implementing this, and any pitfalls that might be lurking out there for the unwary. @vasily-v-ryabov , @cfarrow do you have any thoughts?

Activity

  1. junkmd commented on Jun 13, 2023

    @junkmd
    Collaborator

    #372 is very close to your suggestion.

    In 1.2.0, repr(VARIANT.missing) returns "VARIANT.missing".

    def __repr__(self):
    if self.vt & VT_BYREF:
    return "VARIANT(vt=0x%x, byref(%r))" % (self.vt, self[0])
    elif self is type(self).null:
    return "VARIANT.null"
    elif self is type(self).empty:
    return "VARIANT.empty"
    elif self is type(self).missing:
    return "VARIANT.missing"
    return "VARIANT(vt=0x%x, %r)" % (self.vt, self.value)

    When implementing #486, I noticed that the VARIANT with vt==VT_ERROR and _.VT_I4==DISP_E_PARAMNOTFOUND is not only VARIANT.missing.

    Your suggestion is smarter than adding a workaround that will not cause errors when repring such VARIANT.

    But I'm not sure what type the .value property should return.

    I think that use cases implementing com-ffi and VARIANT in other languages would be helpful.

  2. junkmd commented on Jun 14, 2023

    @junkmd
    Collaborator

    I don't know whether this will be helpful for this issue, I will share how COM support in other languages has handled VARIANT.

    In PHP, There was an error that VARIANT with VT_ERROR could not be created.

    php/php-src#8750
    php/php-src@56804e3

  3. junkmd commented on Jun 14, 2023

    @junkmd
    Collaborator

    In ruby, "VARIANT with vt==VT_ERROR and _.VT_I4==DISP_E_PARAMNOTFOUND" is defined as a constant.

    https://github.com/ruby/ruby/blob/813a5f4fc46a24ca1695d23c159250b9e1080ac7/ext/win32ole/win32ole_variant.c#L727-L736

  4. bennyrowland commented on Jun 15, 2023

    @bennyrowland
    ContributorAuthor

    @junkmd I am not quite sure of the purpose of VARIANT.missing (that is a bit deeper into the COM internals than I have gone so far), but I note that it is defined to be simply an empty VARIANT rather than specifically an error type.

    My use case is in implementing a server with optional arguments where the COM layer is providing the DISP_E_PARAMNOTFOUND VT_ERROR and comtypes crashes on receiving it. The fix I proposed above will stop the crash from occurring but the server will receive the VARIANT object as the value which then has to be checked against the DISP_E_PARAMNOTFOUND to decide if it is a real value or not. I thought it would be nicer to get the interface layer to strip the value out of the call arguments altogether, if that were possible, but I couldn't figure out how to do it.

    At that point, you wouldn't really need to worry about the .value as users wouldn't see it. Defining it as a constant would probably be sensible, but would we then need to define an equality operator for VARIANT to be able to compare to the constant?

    This is not currently blocking me so is more of a "wouldn't it be nice" feature than something I am desperately pursuing. But if you can give me some hints (and some design guidance) then I am happy to dig back into this and see where we can get to.

  5. junkmd commented on Jun 16, 2023

    @junkmd
    Collaborator

    I think that VARIANT.missing is a class attribute used by comtypes to get default values for parameters, rather than a process in the COM layer.
    I will later write a summary of what I found when I improved _memberspec.py in #373 and #368.

  6. junkmd commented on Jun 17, 2023

    @junkmd
    Collaborator

    When COMMETHOD is called, _resolve_argspec is called to derive argtypes for creating function prototypes.
    In it, VARIANT.missing is used as the default value for optional arguments.
    I thought that this VARIANT instance could be a module-level constant, as defined in ruby's WIN32OLE module, but it has been set as a class attribute of VARIANT ever since this package development was started.

    VARIANT.missing = v = VARIANT()
    v.vt = VT_ERROR
    v._.VT_I4 = 0x80020004
    del v

    def COMMETHOD(idlflags, restype, methodname, *argspec):
    """Specifies a COM method slot with idlflags.
    XXX should explain the sematics of the arguments.
    """
    # collect all helpstring instances
    # We should suppress docstrings when Python is started with -OO
    # join them together(does this make sense?) and replace by None if empty.
    helptext = "".join(t for t in idlflags if isinstance(t, helpstring)) or None
    paramflags, argtypes = _resolve_argspec(argspec)
    if "propget" in idlflags:
    name = "_get_%s" % methodname
    elif "propput" in idlflags:
    name = "_set_%s" % methodname
    elif "propputref" in idlflags:
    name = "_setref_%s" % methodname
    else:
    name = methodname
    return _ComMemberSpec(
    restype, name, argtypes, paramflags, tuple(idlflags), helptext
    )

    def _resolve_argspec(
    items: Tuple[_ArgSpecElmType, ...]
    ) -> Tuple[Tuple[_ParamFlagType, ...], Tuple[Type[_CData], ...]]:
    """Unpacks and converts from argspec to paramflags and argtypes.
    - paramflags is a sequence of `(pflags: int, argname: str, | None[, defval: Any])`.
    - argtypes is a sequence of `type[_CData]`.
    """
    from comtypes.automation import VARIANT
    paramflags = []
    argtypes = []
    for item in items:
    idl, typ, argname, defval = _unpack_argspec(*item) # type: ignore
    pflags = _encode_idl(idl)
    if "optional" in idl:
    if defval is _NOTHING:
    if typ is VARIANT:
    defval = VARIANT.missing
    elif typ is ctypes.POINTER(VARIANT):
    defval = ctypes.pointer(VARIANT.missing)
    else:
    # msg = ("'optional' only allowed for VARIANT and VARIANT*, not for %s" % typ.__name__)
    # warnings.warn(msg, IDLWarning, stacklevel=2)
    defval = typ()
    if defval is _NOTHING:
    paramflags.append((pflags, argname))
    else:
    paramflags.append((pflags, argname, defval))
    argtypes.append(typ)
    return tuple(paramflags), tuple(argtypes)

    def add(self, m: _ComMemberSpec) -> None:
    proto = ctypes.WINFUNCTYPE(m.restype, *m.argtypes)

  7. junkmd commented on Apr 26, 2025

    @junkmd
    Collaborator

    @bennyrowland

    When I was working on Excel COM operations after a long break, I encountered a situation in my use case where VARIANT with vt=VT_ERROR should be supported.
    This can apply to cells that contain formulas resulting in errors, such as #DIV/0!.

    Image

    In VBA, it's clear from the procedure execution result that the return value of the .Value property for error cells is a VARIANT where VarType returns 10 and TypeName is Error.
    https://learn.microsoft.com/en-us/office/vba/excel/concepts/cells-and-ranges/cell-error-values
    https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/variant-data-type
    https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/cverr-function

    Option Explicit
    
    Sub foo()
    
        Dim foo As Variant
        Dim a1 As Range, b2 As Range
    
        Set a1 = Range("A1")
        a1.Formula = "=1/0"
        Debug.Print a1.Formula '=1/0
        Debug.Print a1.Text '#DIV/0!
        Debug.Print a1.Value 'エラー 2007
    
        Set b2 = Range("B2")
        b2.Value = a1.Value
        Debug.Print b2.Formula '#DIV/0!
        Debug.Print b2.Text '#DIV/0!
        Debug.Print b2.Value 'エラー 2007
    
        Debug.Print a1.Value = b2.Value 'True
        Debug.Print VarType(a1.Value) ' 10
        Debug.Print TypeName(a1.Value) 'Error
    
        Debug.Print CVErr(2007) = a1.Value 'True
    
    End Sub
    =1/0
    #DIV/0!
    エラー 2007
    #DIV/0!
    #DIV/0!
    エラー 2007
    True
     10
    Error
    True
    

    Image

    Attempting the equivalent operation with Python and comtypes fails because the VARIANT implementation does not support vt=VT_ERROR.

    import comtypes.client
    
    
    comtypes.client.GetModule(
        # Excel GUID
        ("{00020813-0000-0000-C000-000000000046}",)
    )
    import comtypes.gen.Excel as genxl
    
    
    app = comtypes.client.GetActiveObject(
        genxl.Application,
        interface=genxl._Application,
    )
    a1 = app.Range("A1")
    a1.Formula = "=1/0"
    print(a1.Formula)
    print(a1.Text)
    print(a1.Value())
    
    b2 = app.Range("B2")
    b2.Value[:] = a1.Value()
    print(b2.Formula)
    print(b2.Text)
    print(b2.Value())
    
    print(a1.Value() == b2.Value())
    print(a1.Value().vt)
    print(type(a1.Value()))
    
    # There is no function equivalent to CVErr in Python or comtypes.
    > py -3.12 sample.py
    =1/0
    #DIV/0!
    Traceback (most recent call last):
      File "myprj\comtypes\sample.py", line 13, in <module>
        print(a1.Value())
              ^^^^^^^^^^
      File "myprjj\comtypes\comtypes\_memberspec.py", line 629, in __call__
        return self.fget(self.instance, *args)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "myprj\comtypes\comtypes\_memberspec.py", line 564, in getfunc
        return obj.Invoke(memid, _invkind=DISPATCH_PROPERTYGET, *args, **kw)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "myprj\comtypes\comtypes\automation.py", line 927, in Invoke
        return result._get_value(dynamic=True)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "myprj\comtypes\comtypes\automation.py", line 521, in _get_value
        raise NotImplementedError(f"typecode {vt} = 0x{vt:x})")
    NotImplementedError: typecode 10 = 0xa)

    This error stops occurring if we modify comtypes/automation.py as shown in the diff below:

    --- a/comtypes/automation.py
    +++ b/comtypes/automation.py
    @@ -213,6 +213,8 @@ class tagVARIANT(Structure):
                 return "VARIANT.empty"
               elif self is type(self).missing:
                 return "VARIANT.missing"
    +         if isinstance(self.value, VARIANT):
    +               return f"VARIANT(vt=0x{self.vt:x})"
               return f"VARIANT(vt=0x{self.vt:x}, {self.value!r})"
    
           @classmethod
    @@ -517,6 +519,8 @@ class tagVARIANT(Structure):
               elif self.vt & VT_ARRAY:
                 typ = _vartype_to_ctype[self.vt & ~VT_ARRAY]
                 return cast(self._.pparray, _midlSAFEARRAY(typ)).unpack()
    +         elif self.vt == VT_ERROR:
    +               return self
               else:
                 raise NotImplementedError(f"typecode {vt} = 0x{vt:x})")
    > py -3.12 sample.py
    =1/0
    #DIV/0!
    VARIANT(vt=0xa)
    #DIV/0!
    #DIV/0!
    VARIANT(vt=0xa)
    False
    10
    <class 'comtypes.automation.tagVARIANT'>

    Presumably, adding conditional branches like them is the correct way to handle it.

    However, testing this requires Excel, which is currently skipped in the GHA CI pipeline due to licensing issues.

    If you could contribute a simple COM server (acting as a test double) that receives or returns VARIANTs with vt=VT_ERROR, I believe we could release this change with greater confidence.

    Any feedback is welcome.


    Note:
    My computer's Excel is the Japanese localized version.
    While some output characters may differ from the English version of Excel, the internal processing should be the same.
    The Japanese katakana "エラー" is "Error" in English.

  8. bennyrowland commented on Apr 27, 2025

    @bennyrowland
    ContributorAuthor

    @junkmd thanks for continuing your amazing commitment to improving comtypes. I think this is a simple fix and should not be controversial as it doesn’t break any existing behaviour, it just prevents some crashes.

    I am happy to work on contributing a server that deals with VT_ERROR, but it would probably have to be a non Python server as otherwise you end up with a circular testing mechanism as we would have to implement the support to test the support. I am currently interested in exploring COM servers in Rust and so making this test server would be a convenient way to do that. However, I don’t currently have the bandwidth to get that done soon due to some urgent deadlines. So this post is basically just to acknowledge yours, say I will work on it but it won’t be soon.

  9. junkmd commented on Apr 29, 2025

    @junkmd
    Collaborator

    @bennyrowland

    Thank you as well for your continued participation and long-standing contributions to this community.

    Regarding the test server, I believe that implementing a COM server in Rust should be possible, given the existence of the windows crate.

    Perhaps @forderud might also have insights into this in Rust, or even know how to generate VARIANTs with vt=VT_ERROR in languages other than Python.
    They have previously contributed to this project and published COM implementation examples in multiple languages.

    In any case, I warmly welcome you and other community members to deepen your COM knowledge and contribute to this project.

    For my part, handling error cells currently has a workaround by using Application.WorksheetFunction.IsError, so this is not an urgent issue for me personally.
    However, many other COM libraries may not have such workarounds, so supporting VT_ERROR will definitely be a benefit to the project.

    Please feel free to comment on this issue with any findings, questions, or suggestions you may have.

  10. linked a pull request that will close this issueSupport `VT_ERROR` variant type. #965on Oct 12, 2026
  11. junkmd commented on Oct 12, 2026

    @junkmd
    Collaborator

    @bennyrowland

    For now, I have added support for VARIANT with VT_ERROR. See #965.

    Considering how it works with Excel, the .value property returns the VARIANT itself instead of the error code.
    We can now also get scode, which holds the error code, but I haven't made it a public property yet.

    Your use case will probably be a good reference for what works best here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions