comtypes

repository·main·Indexed 43 days ago

https://github.com/enthought/comtypes

A lightweight, pure Python package for defining, calling, and implementing Component Object Model (COM) interfaces on Windows, built on top of the ctypes foreign function interface. It provides core abstractions like IUnknown, COMObject, and CoClass, as well as support for SAFEARRAY structures and COM hosting via LocalServer and InprocServer.

Tokens
26.4K
Snippets
96
Records
121
Agent score
56%

What's inside comtypes

  1. Overview of comtypes capabilities

    main

    The comtypes package is a pure Python COM (Component Object Model) package built on top of the ctypes foreign function interface.

    Unlike pywin32, which focuses on dispatch-based COM interfaces, comtypes allows you to both access and implement custom COM interfaces without requiring C++ wrappers.

    Key functional areas include:

    • Client functionality for consuming COM objects.
    • Server functionality for implementing COM objects.
    • COM Interface handling.
    • NPSupport.
    • Threading support.
  2. How the IUnknown metaclass generates COM methods

    main

    The IUnknown metaclass automatically generates Python methods based on the _methods_ description in an interface. The behavior depends on how the method was described in the IDL:

    • STDMETHOD: Only argument types and return types are known. The generated code performs basic type checking and returns the raw COM return value.
    • COMMETHOD: Provides rich metadata including argument names and direction (in, out, or in/out).
      • For out parameters, the metaclass manages containers.
      • If a method has exactly one out parameter, the generated Python method returns that value directly.
      • If a method has multiple out parameters, the generated Python method returns them as a tuple.
      • Note: When out parameters are present, the native HRESULT is not returned by the generated method; instead, the values are returned.

    To override a generated method while still accessing the underlying COM functionality, implement your custom method with the standard name (e.g., GetGUID) and call the automatically generated version using the _ prefix (e.g., self._GetGUID(...)).

    class IProvideClassInfo2(IProvideClassInfo):
        # Manually implementing GetGUID to provide a default value
        def GetGUID(self, dwGuidKind=1):
            # Accessing the raw functionality generated by the metaclass
            return self._GetGUID(dwGuidKind)
  3. Implement COM methods (Low-level vs High-level)

    main

    You can implement COM methods using two different strategies. comtypes determines which strategy to use by inspecting the second parameter (immediately after self) of your Python method:

    Low-level implementation

    Trigger: The second parameter is named this.

    • Arguments: (self, this, *args).
    • this parameter: Receives None or the integer address of the COM object. Usually ignored.
    • Parameters: [in] parameters are native Python objects; [out] or [out, retval] parameters are passed as ctypes pointers. You must write results into these pointers.
    • Return value: Must return a numerical HRESULT (e.g., 0 for S_OK or None).

    High-level implementation

    Trigger: The second parameter is missing or is NOT named this.

    • Arguments: (self, *args).
    • Parameters: [in] parameters are native Python objects.
    • Return value: If there is a single [out] or [out, retval] parameter, return the value directly. If there are multiple, return a tuple. If there are no [out] parameters, the return value is ignored.

    Choosing a strategy: Use low-level if you need to return specific HRESULT error codes. Use high-level for standard Pythonic code and ease of use.

    # Low-level example
    class MyObjectImpl(MyObject):
        def MyMethod(self, this, a, b, presult):
            presult[0] = a + b
            return 0  # Returns HRESULT
    
    # High-level example
    class MyObjectImpl(MyObject):
        def MyMethod(self, a, b):
            return a + b  # Returns value directly
  4. How to define COM interfaces manually

    main

    To define a COM interface in comtypes, create a Python class that derives from comtypes.IUnknown or one of its subclasses.

    Important Rules:

    • Do not instantiate interface classes directly. They are templates used to create pointer instances. To use methods, you must call them from a POINTER(InterfaceClass) instance.
    • Inheritance: Methods from the superclass (like IUnknown) are inherited automatically and should not be listed in the _methods_ attribute.
    • Circular References: If a method references the interface class itself, define the _methods_ attribute after the class definition.

    Required Class Attributes:

    • _iid_: A comtypes.GUID instance representing the interface identifier.
    • _methods_: A sequence describing the methods in VTable order.

    Optional Attributes:

    • _idlflags_: A sequence containing IDL flags.
    • _case_insensitive_: Set to True if the interface supports case-insensitive attribute access.
    from ctypes import *
    from comtypes import IUnknown, GUID, COMMETHOD
    
    class IMyInterface(IUnknown):
        _iid_ = GUID("{YOUR-GUID-HERE}")
        _methods_ = [
            COMMETHOD([], HRESULT, "MyMethod", (['in'], c_int, "arg1")),
        ]
  5. Convert data types for VARIANT parameters

    main

    While comtypes handles most conversions, some COM servers require specific VARIANT typecodes (e.g., an array of integers instead of a generic array of variants).

    To satisfy these requirements, pass a Python array.array object with the appropriate typecode. The mapping between array.array typecodes and comtypes.automation VARIANT typecodes is as follows:

    array.array codeVARIANT typeDescription
    bVT_I1signed char
    hVT_I2short
    iVT_INTlong
    lVT_I4long
    BVT_UI1unsigned char
    HVT_UI2unsigned short
    IVT_UINTunsigned long
    LVT_UI4unsigned long
    fVT_R4float
    dVT_R8double

    Note: NumPy arrays can also be used for this purpose.

    import array
    import comtypes.client
    
    # Example: Using array.array to pass specific types to AutoCAD
    app = comtypes.client.GetActiveObject("AutoCAD.Application")
    ms = app.ActiveDocument.ModelSpace
    
    # Passing a double array (VT_R8)
    pt = array.array('d', [0, 0, 0])
    point = ms.AddPoint(pt)
    
    # Passing a short array (VT_I2)
    point.SetXData(array.array("h", [1001, 1070]), ['Test_Application1', 600])
  6. Platform compatibility and requirements

    main

    Supported Platforms

    comtypes is designed exclusively for Windows. It cannot be used on Linux or macOS because the Microsoft Component Object Model (COM) is a Windows-specific technology.

    Python Version Compatibility

    • Python 3.9+: Required for current versions.
    • Python 3.13: Use version 1.4.8 or later.
    • Python 3.8: Supported by version 1.4.12 (the last version to support 3.8).
    • Python 3.7: Supported by version 1.4.6 (the last version to support 3.7).
    • Python 2.7 and 3.3–3.6: Supported by version 1.2.1 (the last version to support these).

    Important Note on Python 3.15

    Starting with Python 3.15, changes to how IntFlag (Flag) handles negative members may affect enumeration types generated by comtypes from COM type libraries. Users may need to take action to maintain literal evaluation.

  7. Access COM properties (named and optional arguments)

    main

    COM properties can be read-write, read-only, or write-only. They may require arguments.

    Properties with arguments

    To access properties that require arguments (named properties), use index notation.

    Properties with optional arguments

    Because Python syntax does not allow empty brackets [] for indexing, use one of the following patterns to access or set properties that have optional arguments:

    1. To GET a property:

      • Call the property as a function: obj.Property()
      • Use an empty slice: obj.Property[:]
      • Use an empty tuple: obj.Property[()]
    2. To SET a property:

      • Use an empty slice: obj.Property[:] = value
      • Use an empty tuple: obj.Property[()] = value
      • Direct assignment (if supported): obj.Property = value
    import comtypes.client
    from comtypes.gen.Excel import xlRangeValueDefault
    
    xl = comtypes.client.CreateObject('Excel.Application')
    xl.Workbooks.Add()
    
    # Accessing a property with arguments using index notation
    xl.Range["A1", "C1"].Value[xlRangeValueDefault] = (10, '20', 31.4)
    
    # Accessing an optional property using the function call pattern
    print(xl.Range["A1", "C1"].Value())
    
    # Accessing/Setting an optional property using empty slice/tuple
    print(xl.Range["A1", "C1"].Value[:])
    xl.Range["A1", "C1"].Value[()] = (3, 2, 1)
  8. Implement a simple COM server object

    main

    To implement a COM server in comtypes, you must follow a three-step process:

    1. Define the COM interface: Create an IDL (Interface Definition Language) file describing the coclass, the interface(s) it implements, and optional event interfaces. It is recommended to use dual interfaces and automation-compatible data types. Compile this IDL with the Microsoft MIDL compiler to generate a .tlb type library file.

    2. Implement the Python class: Generate a wrapper module from the .tlb file using GetModule, then create a subclass of the generated coclass. You must provide registration attributes like _reg_threading_, _reg_progid_, and _reg_clsctx_.

    3. Register and run: Use comtypes.server.register.UseCommandLine within your script's entry point and run the script with the /regserver flag to write the object to the Windows registry.

    import comtypes
    import comtypes.server.localserver
    from comtypes.client import GetModule
    
    # 1. Generate wrapper from TLB
    GetModule('path\\to\\mytypelib.tlb')
    from comtypes.gen.MyTypeLib import MyObject
    
    # 2. Implement the class
    class MyObjectImpl(MyObject):
        _reg_threading_ = "Both"
        _reg_progid_ = "MyTypeLib.MyObject.1"
        _reg_novers_progid_ = "MyTypeLib.MyObject"
        _reg_desc_ = "Simple COM server for testing"
        _reg_clsctx_ = comtypes.CLSCTX_INPROC_SERVER | comtypes.CLSCTX_LOCAL_SERVER
        _regcls_ = comtypes.server.localserver.REGCLS_MULTIPLEUSE
    
    if __name__ == "__main__":
        from comtypes.server.register import UseCommandLine
        UseCommandLine(MyObjectImpl)
  9. Ensure typelib wrappers are included when freezing with py2exe

    main

    When using tools like py2exe to freeze your script into an executable, you must ensure the generated comtypes.gen modules are included in the bundle. To do this, explicitly import the friendly module somewhere in your code.

    Recommended pattern for automatic generation and inclusion:

    import sys
    if not hasattr(sys, 'frozen'):
        from comtypes.client import GetModule
        GetModule('scrrun.dll')  # Replace with your specific DLL
    
    import comtypes.gen.Scripting  # Replace with your specific friendly module name
    import sys
    if not hasattr(sys, 'frozen'):
        from comtypes.client import GetModule
        GetModule('scrrun.dll')
    
    import comtypes.gen.Scripting
  10. Pass NumPy arrays as input arguments

    main

    You can pass NumPy arrays as arguments to COM methods that expect a VARIANT array. comtypes will convert the NumPy array into a SAFEARRAY based on its type using numpy.ctypeslib.

    For optimal performance, use NumPy types that allow for nearly direct conversion to SAFEARRAY types. Types not listed in the conversion table below (such as object arrays) will be converted on an item-by-item basis, which is slower.

    ### NumPy to VARIANT Type Conversions
    
    | NumPy type | VARIANT type |
    | :--- | :--- |
    | `int8` | VT_I1 |
    | `int16`, `short` | VT_I2 |
    | `int32`, `int`, `intc`, `int_` | VT_I4 |
    | `int64`, `long`, `longlong`, `intp` | VT_I8 |
    | `uint8`, `ubyte` | VT_UI1 |
    | `uint16`, `ushort` | VT_UI2 |
    | `uint32`, `uint`, `uintc` | VT_UI4 |
    | `uint64`, `ulonglong`, `uintp` | VT_UI8 |
    | `float32` | VT_R4 |
    | `float64`, `float_` | VT_R8 |
    | `datetime64` | VT_DATE |