Categories

Writing a Skype Plugin in C++

NOTE: this article was published long ago and is probably way out of date with respect to how Skype plugins are created now. You may find it interesting for historical purposes only!

Introduction

A while ago, I became interested in looking at the Skype API, and found that it has a reasonably solid plugin architecture. Support for a Java-based API is coming along, although it seems to lag the C++ and C# implementations slightly. One of the reasons for this is (somewhat bizarre, in my opinion) API implementation chosen by Skype themselves – text-based API “commands” (such as ALTER CALL, GET PROFILE, etc) are translated into Win32 window messages, which are dispatched and picked up by the Skype window’s own event loop – a fairly ugly and dated form of IPC, which is made a bit more palatable by their COM wrapper implementation (also used by C#). The Linux version uses a different API – again, not ideal. However, the API does support quite comprehensive automation and callback facilities.

Writing a Plugin

In order to be able to get started writing a Skype plugin, you will need at a minimum:

I also downloaded the excellent Boost library for some C++ extensions (especially relating to date and time handling). I further installed WTL for some useful COM and ATL helpers.

Getting Started

The first thing you will need to do is register the COM server. After you have extracted the files from the Skype4COM distribution, go to the directory where the files have been extracted and run regsvr32 skype4com.dll. A message box should appear confirming successful registration. For convenience, I installed the COM DLL in $WINDIR/system32 for convenience, as it then is always available on the default system path for dynamic loading.

We are writing an application that makes use of COM, and my favourite way to write COM apps has always been to create a standard Win32 project and then let ATL/WTL take care of the COM boilerplate.I originally read about this technique in Beginning ATL 3 COM Programming years ago, of which I can see a used copy currently on Amazon for sale for 83 cents!!!

If you install the WTL AppWizard as per the installation instructions, you can begin a project using the project template below:

WTL AppWizard

You will then need to add any necessary includes and import the Skype COM type library using the import directive in Visual C++. Here is a snippet from my stdafx.h header file:

#include <atlbase.h>
#include <atlapp.h>

extern CAppModule _Module;

#include <atlwin.h>
#include <atlcom.h>
#include <atlhost.h>
#include <atlctrls.h>
#include <atlctrlw.h>
#include <atlframe.h>
#include <atldlgs.h>

#import "c:\\windows\\system32\\skype4com.dll" named_guids

using namespace SKYPE4COMLib;

This will allow Visual C++ to generate smart pointer wrappers for the COM interfaces.

The main source .cpp file (generated by the WTL AppWizard) contains the program’s main message handler loop:

#include "stdafx.h"
#include "resource.h"
#include "aboutdlg.h"
#include "MainDlg.h"

CAppModule _Module;

int Run(LPTSTR /*lpstrCmdLine*/ = NULL, int nCmdShow = SW_SHOWDEFAULT)
{
        CMessageLoop theLoop;
        _Module.AddMessageLoop(&theLoop);

        CMainDlg dlgMain;

        if(dlgMain.Create(NULL) == NULL)
        {
                ATLTRACE(_T("Main dialog creation failed!\n"));
                return 0;
        }

        dlgMain.ShowWindow(nCmdShow);

        int nRet = theLoop.Run();

        _Module.RemoveMessageLoop();
        return nRet;
}

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpstrCmdLine, int nCmdShow)
{
        HRESULT hRes = ::CoInitialize(NULL);
        // If you are running on NT 4.0 or higher you can use the following call instead to 
        // make the EXE free threaded. This means that calls come in on a random RPC thread.
        //     HRESULT hRes = ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
        ATLASSERT(SUCCEEDED(hRes));

        // this resolves ATL window thunking problem when Microsoft Layer for Unicode (MSLU) is used
        ::DefWindowProc(NULL, 0, 0, 0L);

        AtlInitCommonControls(ICC_BAR_CLASSES); // add flags to support other controls

        hRes = _Module.Init(NULL, hInstance);
        ATLASSERT(SUCCEEDED(hRes));

        int nRet = Run(lpstrCmdLine, nCmdShow);

        _Module.Term();
        ::CoUninitialize();

        return nRet;
}

Using the Skype Interfaces

Our main class is CMainDlg. We can now declare an instance of the Skype COM class in our class, using one of the ATL smart pointers that Visual C++ has generated for us (see the ATL documentation for more details on what it generates):

class CMainDlg : public CDialogImpl<CMainDlg>, public CUpdateUI<CMainDlg>,
        ...
{
private:
        ...
public:
        enum { IDD = IDD_MAINDLG };
        ISkypePtr ptr;

We can now use this interface pointer to connect to the running Skype instance and send it commands:

HRESULT hr;
hr = ptr.CreateInstance(__uuidof(Skype));
if (FAILED(hr)) {
        MessageBox("Failed to create Skype instance!\n", "Error");
        exit(-1);
}
...
hr = ptr->Attach(SKYPE_PROTOCOL_VERSION, true);
if (SUCCEEDED(hr)) {
        OutputDebugString("Connected to Skype\n");
}

Event Callbacks

If we have connected OK, we can also register for event callbacks. The most fundamental set of callbacks are the callbacks related to change in call status. Unfortunately, COM event handling can be tricky to get right, although ATL/WTL can alleviate some of the pain. Basically, the main class CMainDlg needs to extend the IDispEventImpl class, passing a reference to the dispatch interface that declares the events we are interested in:

public IDispEventImpl<IDD_MAINDLG,CMainDlg,&DIID__ISkypeEvents,&LIBID_SKYPE4COMLib,1,0>

We then create a COM event sink map for the event(s):

BEGIN_SINK_MAP(CMainDlg)
        SINK_ENTRY_EX(IDD_MAINDLG, DIID__ISkypeEvents, 0x8, &OnCallStatusChange)
END_SINK_MAP()

Finally, we need to actually tell Skype we are interested in the events:

// Hook up for event notifications
hr = DispEventAdvise(ptr);
if (FAILED(hr)) { 
...
}

The actual details of COM event sinks and connection points is beyond the scope of this article, and can be a bit involved (for instance you may need to use the OLE/COM Type Library Viewer utility to view the dispatch interface details) but both MSDN and the COM book referenced above have good examples.

Our actual event callback looks like the following:

void __stdcall OnCallStatusChange(ICall* pCall, enum TCallStatus status) {
        std::ostringstream str;

        USES_CONVERSION;


        if (status == clsInProgress) {
                // A call has been initiated.
        }
        else if (status == clsRouting) {
        }
        else if (status == clsRinging) {
        }
        else if (status == clsFailed) {
        }
        else if (status == clsFinished) {
        }
}

Within the callback, we can retrieve a handle to the call or counterparty and use the information contained within these objects. For instance, the code below creates an instance of a CallInfo object and populates it with data retrieved from the Call and User objects (the ugly BSTR() and LPCTSTR casting is not ideal, but the only way I could effectively cast the COM BSTR to an ANSI string – if you know a better way, please let me know). For instance, the CallInfo struct below has fields for the call counterparty, id, duration, and user country.

if (status == clsInProgress) {
   str << "Call in progress with " << (LPCTSTR)OLE2T(pCall->PartnerHandle.GetBSTR());
    IUserPtr user = ptr->GetUser(pCall->PartnerHandle);

    CallInfo* callInfo = new CallInfo();
    callInfo->callId = pCall->Id;
    callInfo->duration = 0L;

    std::string* country = new std::string(OLE2T(user->Country.GetBSTR()));
    callInfo->callerCountry = country;

...

}

When the call has finished, we can retrieve the call duration:


else if (status == clsFinished) {
   str << "Call # " << pCall->Id << " finished, duration: " << pCall->Duration << " seconds.";

   // Retrieve the call from the active calls list
   CallList::const_iterator it = calls.find(pCall->Id);
   if (it == calls.end()) {
   OutputDebugString("Call Id not found!");
   return;
}

                                CallInfo* callInfo = it->second;
                                callInfo->duration = pCall->Duration;

The sample program available for download below contains a very simple example, which basically displays some call-related information as and when Skype reports it. I originally had planned to make it a bit more ambitious, and attempt to deduce the potential costs savings of making a Skype call vs. a fixed-line call on a standard BT tariff, but this proved unreliable for a few different reasons:

  • The callee’s profile may not have a country field;
  • If present, the country field may be incorrect;
  • It is unclear whether it is more appropriate to compare mobile or fixed tariffs.

However, I have left the code in there.

Running the Plugin

If Skype is running, compile and run the plugin. You should see a dialog appear like the following:

Once you have connected, the plugin dialog should appear, and you can see event callbacks as they happen (just make and receive calls as normal):

skype_dialog.png

Wrapping Up

In short, Skype offers fairly decent plugin functionality. However, the actual plugin architecture is clunky and dated, and inconsistent across platforms. I suspect that Skype will spend some time in the future on developing a richer and more uniform API across all platforms, that is not tightly coupled to the Skype GUI.

Just for completeness, here are my VC++ compiler settings:

/Od /D "WIN32" /D "_WINDOWS" /D "STRICT" /D "_DEBUG" /D "_ATL_STATIC_REGISTRY" /D "_MBCS" /Gm /EHsc /RTC1 /MTd /Yu"stdafx.h" /Fp"Debug\SkypePlugin.pch" /Fo"Debug\\" /Fd"Debug\vc80.pdb" /W3 /nologo /c /ZI /TP /errorReport:prompt

Downloads

I have packed the source files into a ZIP, which you can download from here:SkypePlugin.zip

The source tree also contains an installer and the NSIS install script. The installer can be downloaded separately from here:Skype Plugin Installer

Finally, the source tree also contains some documentation generated by Doxygen.