Tutorial: How to Let a Messenger Plus! Script-Callable DLL Handle Plus! Scripting Objects

- Mnjul (last update: 2011-05-27)

Messenger Plus! Scripts enable its users and developers to create their own functionalities for Windows Live Messeger. The scripting engine allows scripts to call arbitrary Win32 DLLs (through the Interop object), and to allocate raw memory (through the DataBloc object), making scripts capable of virtually anything. However, there are still occasions where making your own DLLs is inevitable; the most important one is when you need to have your own asynchronous callbacks. Currently, Plus! only supports synchronous callbacks for scripts. Asynchronous callbacks such as Window Procedures, which are crucial in subclassing various windows, are not supported, and have to be implemented with external DLLs. Now, what if you want to call the scripting engine objects (like MsgPlus, Messenger or ChatWnd instances) inside the DLL function's C++ codes? If you've ever wondered the question, then this tutorial is for you. In this tutorial, I'll illustrate how to let your own DLLs talk with Messenger Plus! scripting objects, step by step.

Pre-Requisites

In this tutorial, I assume that you're an adequate C++ programmer, and know how to create a native Win32 DLL with Microsoft Visual Studio. I also assume you're already experienced in Plus! scripting. I use Microsoft Visual Studio 2010 when writing this tutorial, but things should be pretty much the same with other versions of Visual Studio (down to 2005, I presume; for even earlier versions I can't guarantee).

Importing The Messenger Plus! Type Library

The first step is to let your DLL know what Plus! scripting objects are and where to locate them. For this, you have to import the Messenger Plus! Type library. Add the following line (depending on your Plus! version) to your code:

// For Plus! 4
#import "C:\Program Files (x86)\Messenger Plus! Live\MsgPlusLive.dll" tlbid(1)

// For Plus! 5
#import "C:\Program Files (x86)\Yuna Software\Messenger Plus!\MsgPlusLive.dll" tlbid(1)

Note that you may need to change the directory to match the directory where you installed Plus!. For example, take out " (x86)" if you're not using 64-bit Windows. Your DLL built against the Type Library on one Plus! version will typically work on another Plus! version, as long as version-specific features are taken care of. For example, as of build 706, Plus! 5 doesn't support changing personal messages through scripts with Messenger.MyPersonalMessage. Your DLL built against Plus! 4's Type Library may behave unexpectedly on Plus! 5 if it modifies the user's personal message in that way.

After you add the above #import line, you need to build your project first, so that msgpluslive.tlh, the Typelib Generated C++ Header, and msgpluslive.tli, the Typelib Generated C++ Inline File, will appear in the output directory (Debug or Release, depending on your build configuration). After you do this, Visual Studio's IntelliSense on Plus! scripting objects will kick in to work.

The type library you've just imported creates a namespace MPLiveScriptingLib, in which various Plus! scripting objects and constants are declared and defined. The following table outlines these objects and constants along with their JScript equivalents.

JScriptRaw InterfaceCOM Smart Interface Pointer
Object Classes
ChatWndsMPLiveScriptingLib::_IMPChatWnds*MPLiveScriptingLib::_IMPChatWndsPtr
ChatWndMPLiveScriptingLib::_IMPChatWnd*MPLiveScriptingLib::_IMPChatWndPtr
ContactsMPLiveScriptingLib::_IMPContacts*MPLiveScriptingLib::_IMPContactsPtr
ContactMPLiveScriptingLib::_IMPContact*MPLiveScriptingLib::_IMPContactPtr
EmoticonsMPLiveScriptingLib::_IMPEmoticons*MPLiveScriptingLib::_IMPEmoticonsPtr
EmoticonMPLiveScriptingLib::_IMPEmoticon*MPLiveScriptingLib::_IMPEmoticonPtr
PlusWndMPLiveScriptingLib::_IMPPlusWnd*MPLiveScriptingLib::_IMPPlusWndPtr
DataBlocMPLiveScriptingLib::_IMPDataBloc*MPLiveScriptingLib::_IMPDataBlocPtr
Utility Classes
DebugMPLiveScriptingLib::_IMPDebug*MPLiveScriptingLib::_IMPDebugPtr
MessengerMPLiveScriptingLib::_IMPMessenger*MPLiveScriptingLib::_IMPMessengerPtr
MsgPlusMPLiveScriptingLib::_IMPMsgPlus*MPLiveScriptingLib::_IMPMsgPlusPtr
InteropMPLiveScriptingLib::_IMPInterop*MPLiveScriptingLib::_IMPInteropPtr
Constants
STATUS_INVISIBLE, STATUS_ONLINE, ...MPLiveScriptingLib::STATUS_INVISIBLE, MPLiveScriptingLib::STATUS_ONLINE, ...
WNDOPT_NORMAL, WNDOPT_INVISIBLE, ...MPLiveScriptingLib::WNDOPT_NORMAL, MPLiveScriptingLib::WNDOPT_INVISIBLE, ...
(not all constant enumerations are outlined here, but I think you get the idea)

You might notice that for each object, two kinds of interfacs are available: one with the _IM prefix, like _IMPDebug, and another one without the prefix, like MPDebug. The difference of two is that the former use raw functions to interface with Plus!, while the latter uses _com_dispatch_method(). The two should function the same, while I prefer to use the former. Also, we want COM smart interface pointers to manage (e.g. automatically release when applicable) the pointers, so they will be the case from now on. Notice that from here on, I assume you're using the MPLiveScriptingLib namespace (by adding using namespace MPLiveScriptingLib;), so instead of saying MPLiveScriptingLib::_IMPMsgPlusPtr, I'll just say _IMPMsgPlusPtr.

Acquiring Scripting Objects From Scripts

Simplying declaring the interface pointers will get you nowhere. You need to have them point to the actual objects. For this, you need to pass those objects from scripts to the DLL. For example, if your DLL needs to use MsgPlus and Messenger utility objects, it can acquire them in one function it exports, say, acquire(). Then, your script can use Interop.Call() to pass the objects to the DLL:

Interop.Call("myowndll.dll", "acquire", MsgPlus, Messenger);

In the DLL codes, scripting objects are passed as IDispatch* pointers. So, the declaration of the above acquire() will look like:

extern "C" __declspec(dllexport) int __stdcall acquire(IDispatch* dispMsgPlus, IDispatch* dispMessenger);

You will need to convert these IDispatch* pointers to their corresponding _IMPxxxxxxPtr pointers. The following code will do the job, taking _IMPMsgPlusPtr as example:

_IMPMsgPlusPtr pMsgPlus;     // Note: I assume using namespace MPLiveScriptingLib
_IMPMsgPlusPtr::Interface* rawMsgPlus;
dispMsgPlus->QueryInterface(&rawMsgPlus);
pMsgPlus.Attach(rawMsgPlus);

The code above will get you a fully functional _IMPMsgPlusPtr smart interface pointer named pMsgPlus. I will detail how to use the interface pointers in the next section.

Of course, duplicating codes to acquire each interface pointer should be avoided. We can write a template function:

template<typename IPtr>
HRESULT acquireInterfacePtr(IDispatch* pDispatch, IPtr& ptr){
    IPtr::Interface* raw;
    HRESULT hr;
    if(SUCCEEDED(hr = pDispatch->QueryInterface(&raw))){
        ptr.Attach(raw);
    }
    return hr;
}

The above template function can be reused to acquire different types of interface pointers, like the following codes:

_IMPMsgPlus pMsgPlus;
_IMPMessenger pMessenger;

extern "C" __declspec(dllexport) int __stdcall acquire(IDispatch* dispMsgPlus, IDispatch* dispMessenger){
    acquireInterfacePtr(dispMsgPlus, pMsgPlus);
    acquireInterfacePtr(dispMessenger, pMessenger);
    // the interface pointers are usable now
}

The above template function also takes care of the return value of QueryInterface(), so in case anything fails (can be checked with the return value of acquireInterfacePtr()), you will know it.

Manipulating The Scripting Object Interface Pointers

After your interface pointers do point to the actual objects, you can use them in your C++ code just like you do in JScript, with some minor twists. First, you use the -> "member by pointer" operator to access the members of the acquired interface pointers (the . "member" operator is used to manipulate the smart pointer, and we are not interested in it).

Properties And Method Names

If you ever type pMessenger-> and let IntelliSense come up with the AutoComplete list, you'd find that there are three available options for one single property, and two available options for one single method.

Variable Types

Unlike JScript, C++ is a strong-typed language, requiring that all function parameters and return values have their types explicitly declared. The following table outlines the types, as used in the scripting documentation, and their corresponding ones used in the wrapped functions in the type library.

Type in Scripting Documentation Type in Type Library Description
bool VARIANT_BOOL Can be VARIANT_TRUE or VARIANT_FALSE.
string _bstr_t _bstr_t is a wrapper of BSTR strings. The constructors of it support converting from char* and wchar_t*. It can also be cast into char* and wchar_t*. They're all done implicitly, so it shouldn't be a problem.
object IDispatchPtr When objects are returned by methods or properties, you need to convert the pointer to the correct interface pointers, using the methods outlined in the section above.
(Just use IDispatchPtr like IDispatch*)
Example: Messenger.CurrentChats, ChatWnd::Contacts, and many others.
number int or long (integers)
float (floating-points, i.e. "fractions")
enum enum of the corresponding enumeration

Incidentally, some methods that are documented as "no return value" in the scripting documentation actually return a HRESULT. The value is returned by Plus! and I don't know it means.

Optional Method Parameters

Optional parameters are always declared as a constant reference to _variant_t regardless of their actual types, and can also be optional in your C++ codes. Don't let this queerish type scare you! The type, essentially a class (thanks to the automatic wrapping), can be implicitly cast from a variety of other types without any trouble, including the ones used in scripts: integers, booleans and strings. So when you need to pass an optional parameter, just think it as though it was defined with its real type. For example, to use the PlaySound() with our earlier pMsgPlus interface pointer with MaxPlayTime specified as 3.5 seconds, the code will do the trick: pMsgPlus->PlaySound(L"sound.wav", 3500);, despite that the second parameter is delcared as const _variant_t &. Nicely done, right?

Enumerators

What has been left so far is the use of Enumerators to enumerator contacts from Contacts, chat windows from ChatWnds, and emoticons from Emoticons. The following codes will illustrate how to enumerate contacts from a chat window.

_IMPChatWndPtr pChatWnd;

// (here, do something to make pChatWnd actually points to a chat window object)

IDispatchPtr dispContacts = pChatWnd->GetContacts();
_IMPContactsPtr pContacts;

acquireInterfacePtr(dispContacts, pContacts);

IEnumVARIANT *pEnumVariant;
IUnknownPtr pUnknown = pContacts->Get_NewEnum();
pUnknown->QueryInterface(&pEnumVariant);

VARIANT var;
VariantInit(&var);

while(pEnumVariant->Next(1, &var, NULL) == S_OK){
    IDispatch* pDispatch = V_DISPATCH(&var);

    _IMPContactPtr pContact;
    acquireInterfacePtr(pDispatch, pContact);

    // now you've got the contact in pContact!

    VariantClear(&var);
}

Example

The example codes pack the source codes of a DLL that does two things:

  1. It enumerates and outputs to the debug window (the first) ten contacts of the user's contacts when signed-in.
  2. It receives a ChatWnd object when a chat window is created, and then subclasses the chat window, and posts a message to the Plus! script debug window whenever the chat window is resized (by capturing WM_SIZE in the subclass Window Procedure).

The source codes of the corresponding Plus! script are also packed. Note that the DLL part is a Visual Studio 2010 solution. The solution file itself probably can't be opened by prior versions of Visual Studio, but since I didn't do anything special with the solution and the project properties (aside from changing "Multi-Thread (Debug) DLL" to "Multi-Thread (Debug)", and assigning a Module-Definition File), you should be able to reconstruct the solution and the project with prior versions of Visual Studio without major troubles.

Advanced Topics

Calling Back JScript Functions Inside DLL (special thanks to Eljay about this section)

Sometimes it would be desirable to call JScript functions inside our DLL. To let the DLL know what function to call, you need to pass the function object to the DLL. A function object is passed with its name directly, without parentheses, and not in the form of a string. So, to let your DLL call a function named callbackFunc with three arguments, the JScript codes will look like:

function callbackFunc(stringArg, fractionArg, integerArg){
    // do something here
}

// somewhere else
Interop.Call("myowndll.dll", "pleaseCallback", callbackFunc);

And the C++ codes inside the DLL will look like this:

extern "C" __declspec(dllexport) int __stdcall pleaseCallback(IDispatch* dispFunc){
    OLECHAR* methodName = L"call";
    DISPID idDisp;
    dispFunc->GetIDsOfNames(IID_NULL, &methodName, 1, LOCALE_USER_DEFAULT, &idDisp);

    DISPPARAMS params;

    params.cArgs = 4;    // yes, four, instead of three; explanation below
    params.cNamedArgs = 0;

    params.rgvarg = new VARIANTARG[4];
    // assign the parameters in the reverse order
    params.rgvarg[3].vt = VT_NULL;
    params.rgvarg[2].vt = VT_INT;
    params.rgvarg[2].intVal = 123;
    params.rgvarg[1].vt = VT_R8;
    params.rgvarg[1].dblVal = 0.456;
    params.rgvarg[0].vt = VT_BSTR;
    params.rgvarg[0].bstrVal = SysAllocString(L"Some String");
    dispFunc->Invoke(idDisp, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &params, NULL, NULL, NULL);

    SysFreeString(params.rgvarg[0].bstrVal);
    delete [] params.rgvarg;
}

You might have noticed that there is one confusing issue regarding the parameters: even though the callbackFunc() function has only three arguments, in the C++ codes we allocate four arguments. This is because we need to specify what the usually-implicit this object is inside the JScript function. In the codes above I use null for the this object, but there are indeed times you actually need a this object. Take the following JScript codes as example:

function ExampleClass(){
    this.someVar = "blah";
    this.testFunc = function(stringArg, fractionArg, integerArg){
        Debug.Trace(this.someVar);
    }
}

// somewhere else
var exampleObject = new ExampleClass();
exampleObject.someVar = "blahblah!";

For your DLL to correctly call back exampleObject's testFunc(), you need to designate this to be exampleObject. For this you need to pass exampleObject from JScript to the DLL, and relay it in your C++ codes:

// JScript code
Interop.Call("myowndll.dll", "pleaseCallback", exampleObject.testFunc, exampleObject); // use exampleObject.testFunc, not ExampleClass.testfunc


// C++ code
extern "C" __declspec(dllexport) int __stdcall pleaseCallback(IDispatch* dispFunc, IDispatch* dispThis){
    // similar to the above C++ codes, except that...
    params.rgvarg[3].vt = VT_DISPATCH;
    params.rgvarg[3].pdispVal = dispThis;
}

Please note that when you pass the function to pleaseCallback(), you use exampleObject.testFunc, instead of ExampleClass.testFunc. Also please note the extra argument in pleaseCallback()'s declaration for this.

In all, please also note that the parameters are assigned in the reverse order, with the right-most argument as the 0-th parameter, and this being the last parameter. Also, you can check the return value of GetIDsOfNames() and Invoke() to make sure things are going well. As for parameter types, I have demonstrated integer and fraction numbers along with strings in the codes above. If you want to use boolean values, just use VT_BOOL for the vt member, and assign VARIANT_TRUE or VARIANT_FALSE to the boolVal member. As for arrays, I haven't tried but I think you have to fiddle around with SAFEARRAYs. (I would honestly appreciate it if anyone could come up with some working array-passing codes, and share them with us.)

Issues with Multi-Threading

As per the scripting documentation, if you create new threads in your DLL, be sure to manipulate the scripting objects with the main thread. To quote the documentation, functions and properties will fail or may create unexpected bugs if called from a different thread.

Contact Me

For anything related to this tutorial, replying this thread on the official Messenger Plus! Community Forums is the easiest and the fatest way.