Extremely Late Binding

From IronPython Cookbook

There may be times when you need to invoke COM automation servers using late-bound calls only (IDispatch::Invoke) because either (a) you don't have a type library or interop assembly handy or (b) the COM object does not provide one at all. For example, Word has a legacy automation interface called Word Basic that is still available in Microsoft Office 2007 (which is the latest released version at the time of this writing). Unfortunately, IronPython 1.x doesn't work too well in such late-bound scenarios. IronPython 2.0, Alpha 6, may add support for this some time soon, but is still failing at the time of writing even with the -X:PreferComDispatch switch specified:

IronPython console: IronPython 2.0A6 (2.0.11102.00) on .NET 2.0.50727.1433
Copyright (c) Microsoft Corporation. All rights reserved.
>>> from System import Type, Activator
>>> wb = Activator.CreateInstance(Type.GetTypeFromProgID('Word.Basic'))
>>> wb.AppShow()
Traceback (most recent call last):
  File , line 0, in ##30
TypeError: Specified cast is not valid.

Contents

Introducing LateBinding

To get around this problem, one can take advantage of the late-binding support available in Visual Basic, which is usually installed and available as part of the .NET Framework. Although the Visual Basic language provides native support for invoking COM objects using late-bound calls, the heavy-lifting is implemented via the public type Microsoft.VisualBasic.CompilerServices.LateBinding . One could therefore leverage this in IronPython as well to talk to the WordBasic COM object, as shown here:

IronPython console: IronPython 2.0A6 (2.0.11102.00) on .NET 2.0.50727.1433
Copyright (c) Microsoft Corporation. All rights reserved.
>>> from System import Array, Type, Activator
>>> import clr
>>> clr.AddReference('Microsoft.VisualBasic')
>>> from Microsoft.VisualBasic.CompilerServices import LateBinding
>>> wb = Activator.CreateInstance(Type.GetTypeFromProgID('Word.Basic'))
>>> LateBinding.LateCall(wb, None, 'AppShow', None, None, None)
>>> LateBinding.LateCall(wb, None, 'FileNew', None, None, None)
>>> LateBinding.LateCall(wb, None, 'Insert', Array[object](('Hello from IronPython', )), None, None)
>>> LateBinding.LateCall(wb, None, 'FileClose', Array[object]((2, )), None, None)
>>> LateBinding.LateCall(wb, None, 'AppClose', None, None, None)

Making It Simple, Again

Using LateBinding certainly makes the code quite complicated and hard to read so a workaround could be to use a helper class that makes the calls very natural once more and following demo program shows just that:

import sys
from System import Array, Type, Activator

import clr
clr.AddReference('Microsoft.VisualBasic')

class ActiveXObject(object):
    def __init__(self, progid):
         self.target = Activator.CreateInstance(Type.GetTypeFromProgID(progid))
    def __call__(self, member, *args):
        """Makes a late-bound call on the COM object.
        
        For properties, you need to prefix the member with 'get' or 'set'.
        In other words, if the property on the COM object is called 'Visible'
        and you want to get its value, then pass in 'getVisible' for the
        member argument. Likewise, to set the property value, pass in 
        'setVisible' for the member argument. The prefix is case-sensitive.
        """
        from Microsoft.VisualBasic.CompilerServices import LateBinding
        args = Array[object](args)
        if len(member) > 3:
            modifier = member[0:3]
            if modifier == 'get':
                return LateBinding.LateGet(self.target, None, member[3:], args, None, None)
            elif modifier == 'set':
                return LateBinding.LateSet(self.target, None, member[3:], args, None)
        return LateBinding.LateCall(self.target, None, member, args, None, None)
    def __getattribute__(self, name):
        try:
            return super(ActiveXObject, self).__getattribute__(name)
        except AttributeError:
            return lambda *args: self(name, *args)

def main(args):
    wb = ActiveXObject('Word.Basic')
    wb.AppShow()
    wb.FileNew()
    wb.Insert('Greetings from Python ' + sys.version)
    wb.InsertPara()
    print 'Press ENTER to end.'
    sys.stdin.readline()
    wb.FileClose(2)
    wb.AppClose()

if __name__ == '__main__':
    main(sys.argv[1:])

Note how the ActiveXObject class now makes working with WordBasic as simple as natural Python calls:

wb = ActiveXObject('Word.Basic')
wb.AppShow()
wb.FileNew()
wb.Insert('Greetings from Python ' + sys.version)
wb.InsertPara()
...

About ActiveXObject

The ActiveXObject class achieves its magic by turning non-existing attribute lookups into callable functions that, when invoked, delegate the call to the target COM object via LateBinding.

Limitations and Improvments

The ActiveXObject class presented is by no means a comprehensive implementation. It is limited in many ways but could serve as a good basis for further improvements. Here are a few ideas:

  • Named arguments via Python kwargs
  • Setting and getting indexed properties via LateIndexGet and LateIndexSet
  • By-reference parameters as tuple-d return values

Caution About LateBinding

The documentation for LateBinding states, "This class supports the .NET Framework infrastructure and is not intended to be used directly from your code." What this means is that Microsoft reserves the right to change or remove the class in the future. Meanwhile, it serves as a reasonable workaround until support for pure late-bound COM calls is improved in IronPython.

It is also worth noting that Microsoft.VisualBasic.Interaction.CallByName provides pretty much the same functionality as LateBinding but it does not support named-arguments. However, Microsoft.VisualBasic.Interaction.CallByName is probably the more supported approach since it is publicly documented and dates back to classic VB (VB6 and previous versions). So depending on your needs, ActiveXObject in the above example could be adapted to work with one or the other.


Back to Contents.

TOOLBOX
LANGUAGES