Three ways to implement VBScript (VB6, VBA) callback from C++/ATL class

Suppose you have an automation object that needs to implement a callback into caller Visual Basic environment, which can be Scripting Host, Visual Basic for Applications, ASP etc. With all the late binding in VB scripting and so much different C++ code – how to put everything together? There are great choices, let us have three.

On all three samples we add numbers on the way:

  • 300 is the initial argument of the VB caller
  • 20 more in the VB callback
  • 1 adds the C++ COM class

The three .VBS files will output 321 is everything goes well. The C++ method name is OuterDo, while the supposed callback method is InnerDo.

There are two basic problems on the way:

  1. We have to somehow pass the callback to COM class
  2. We have to define function in a way that all they are compatible one with another

Callback in VB Class Function

Straightforwardly, we can pass VB class to have a method called back. While it might be a good idea with VBA where it might be possible to add a reference to type library and have the named callback interface available in scripting environment (VB’s Implements keyword), this does not work directly in VB Scripting Host. To work this around in .VBS sample we don’t reference IFirstSite interface, and the automation object First will access the method by its hardcoded name.

The source code has commented parts which can be used to connect the parts more strictly using IFirstSite interface.

Class FirstSite 
  'Implements IFirstSite
  Public Function IFirstSite_InnerDo(ByVal A)
    IFirstSite_InnerDo = 20 + A
  End Function
End Class

Dim First
Set First = WScript.CreateObject("AlaxInfo.VbsCallback.First")
Result = First.OuterDo(300, new FirstSite)
WScript.Echo Result

C++ implementation accepts the call using IDL syntax:

interface IFirst : IDispatch
{
    //[id(1)] HRESULT OuterDo([in] LONG nA, [in] IFirstSite* pSite, [out, retval] LONG* pnB);
    [id(1)] HRESULT OuterDo([in] LONG nA, [in] IDispatch* pSite, [out, retval] LONG* pnB);
};

In C++ we receive a COM interface of the class, and now we are to locate the callback method by its name using IDispatchEx interface. Once we succeed with this, we invoke a dispatch interface call.

Callback in VB Function

An alternate option is to pass separate function IDispatch interface and have it called from C++. compared to using strictly defined interface IFirstSite this might look like an inferior way, however considering the workarounds we have to put in method above to stay compatible with Scripting Host, this method might look even a bit simpler.

Function InnerDo(ByVal A)
    InnerDo = 20 + A
End Function

Dim Second
Set Second = WScript.CreateObject("AlaxInfo.VbsCallback.Second")
Result = Second.OuterDo(300, GetRef("InnerDo"))
WScript.Echo Result

On the caller side, the key to success is GetRef method that creates a IDispatch-enabled object from a separate function. C++ will use IDispatch::Invoke on DISPID of zero in order for the call to reach the implementation.

Callback through Connection Points

Connection points are standard and well known mechanism to deliver outgoing calls from an automation object, however they are subject to certain constraints:

  • late binding is taking place and we have to use IDispatch::Invoke to deliver calls, luckily Visual Studio is capable of generating proxy classes for that (no IFirstSite-like strictly defined and callable interfaces!)
  • connection points assume that there might be several parties connected to the points/events, and the interface should nicely supports this (no return values!)

Most of the environments have support for connection points on caller side, so this methods is nicely applicable.

Sub Third_InnerDo(ByRef C)
  C = C + 20
End Sub

Dim Third
Set Third = WScript.CreateObject("AlaxInfo.VbsCallback.Third", "Third_")
Result = Third.OuterDo(300)
WScript.Echo Result

In C++ there is a proxy class to deliver the event, so implementation is as simple as this:

// IThird
    STDMETHOD(OuterDo)(LONG nA, LONG* pnB) throw()
    {
        ATLASSERT(pnB);
        CComVariant vB(nA + 1);
        ATLVERIFY(SUCCEEDED(Fire_InnerDo(&vB)));
        ATLASSERT(vB.vt == VT_I4);
        *pnB = vB.lVal;
        return S_OK;
    }

Note that [out] parameters need to be VARIANTs or otherwise the returned values might get lost on the way.

So we are ready for a test run:

D:\Projects\Alax.Info\Repository-Public\Utilities\VbsCallback\Scripts>cscript First.vbs
Microsoft (R) Windows Script Host Version 5.8
Copyright (C) Microsoft Corporation. All rights reserved.

321

D:\Projects\Alax.Info\Repository-Public\Utilities\VbsCallback\Scripts>cscript Second.vbs
Microsoft (R) Windows Script Host Version 5.8
Copyright (C) Microsoft Corporation. All rights reserved.

321

D:\Projects\Alax.Info\Repository-Public\Utilities\VbsCallback\Scripts>cscript Third.vbs
Microsoft (R) Windows Script Host Version 5.8
Copyright (C) Microsoft Corporation. All rights reserved.

321

Good news, all three methods work well!

Visual C++ .NET 2010 source code [Trac, Subversion] is available from SVN. .VBS scripts are included.

Leave a Reply