Using COM from js-ctypes

This article needs a technical review. How you can help.

The Windows API mostly concerns itself with the interaction between the operating system and an application. For communication between the different Windows applications among themselves, Microsoft has developed a series of technologies alongside the main Windows API. This started out with Dynamic Data Exchange (DDE), which was superseded by Object Linking and Embedding (OLE) and later by the Component Object Model (COM), Automation Objects, ActiveX controls, and the .NET Framework.

COM is C++ and it cannot be written directly with js-ctypes. This documentaion explains how to convert the COM C++ code into js-ctypes code.

Converting COM code to C code

To convert COM code to js-ctypes, we need to write C++ VTable pointers in C. After that, we can pass a pointer .

Speech Synthesis Example

Let's start with following C++ code, which invokes Microsoft Speech API and says "Hello, Firefox!" with system default voice, then wait until the speaking done.

#include <sapi.h>

int main(void)
{
    if (SUCCEEDED(CoInitialize(NULL)))
    {
        ISpVoice* pVoice = NULL;
        HRESULT hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL,
                                      IID_ISpVoice, (void**)&pVoice);

        if (SUCCEEDED(hr))
        {
            pVoice->Speak(L"Hello, Firefox!", SPF_DEFAULT, NULL);

            pVoice->Release();
        }
    }

    // MSDN documentation says that even if CoInitalize fails, CoUnitialize
    // must be called
    CoUninitialize();

    return 0;
}

To run the code, save it as test.cpp, and run following command in the directory (needs Visual Studio).

$ cl ole32.lib test.cpp

vtable

Needs vtable description here.

The order of the vtable is very critical. When used from C or js-ctypes, the order of the vtable methods should match the order of the C++ vtable.

__stdcall and __cdecl

Needs __stdcall and __cdecl (CALLBACK_ABI) description here.

Call by reference

Needs C++ reference (&) and C pointer (*) description here.

Converted C code

Now we can translate whole code into C syntax.

#include <sapi.h>

int main(void)
{
    if (SUCCEEDED(CoInitialize(NULL)))
    {
        struct ISpVoice* pVoice = NULL;
        HRESULT hr = CoCreateInstance(&CLSID_SpVoice, NULL, CLSCTX_ALL,
                                      &IID_ISpVoice, (void**)&pVoice);
        if (SUCCEEDED(hr))
        {
            pVoice->lpVtbl->Speak(pVoice, L"Hello, Firefox!", 0, NULL);

            pVoice->lpVtbl->Release(pVoice);
        }
    }

    // MSDN documentation says that even if CoInitalize fails, CoUnitialize
    // must be called
    CoUninitialize();

    return 0;
}

To run the code, save it as test.c, and run following command in the directory.

$ cl ole32.lib test.c

C code with pseudo struct

Needs pseudo struct description here.

#include <sapi.h>

struct MyISpVoiceVtbl;
struct MyISpVoice {
    struct MyISpVoiceVtbl* lpVtbl;
};
struct MyISpVoiceVtbl {
    /* start inherit from IUnknown */
    void* QueryInterface;
    void* AddRef;
    ULONG (__stdcall *Release)(struct MyISpVoice*);
    /* end inherit from IUnknown */
    /* start inherit from ISpNotifySource */
    void* SetNotifySink;
    void* SetNotifyWindowMessage;
    void* SetNotifyCallbackFunction;
    void* SetNotifyCallbackInterface;
    void* SetNotifyWin32Event;
    void* WaitForNotifyEvent;
    void* GetNotifyEventHandle;
    /* end inherit from ISpNotifySource */
    /* start inherit from ISpEventSource */
    void* SetInterest;
    void* GetEvents;
    void* GetInfo;
    /* end inherit from ISpEventSource */
    /* start ISpVoice */
    void* SetOutput;
    void* GetOutputObjectToken;
    void* GetOutputStream;
    void* Pause;
    void* Resume;
    void* SetVoice;
    void* GetVoice;
    HRESULT (__stdcall *Speak)(struct MyISpVoice*, LPCWSTR pwcs, DWORD dwFlags,
                               ULONG* pulStreamNumber);
    void* SpeakStream;
    void* GetStatus;
    void* Skip;
    void* SetPriority;
    void* GetPriority;
    void* SetAlertBoundary;
    void* GetAlertBoundary;
    void* SetRate;
    void* GetRate;
    void* SetVolume;
    void* GetVolume;
    void* WaitUntilDone;
    void* SetSyncSpeakTimeout;
    void* GetSyncSpeakTimeout;
    void* SpeakCompleteEvent;
    void* IsUISupported;
    void* DisplayUI;
    /* end ISpVoice */
};

int main(void)
{
    if (SUCCEEDED(CoInitialize(NULL)))
    {
        struct MyISpVoice* pVoice = NULL;
        HRESULT hr = CoCreateInstance(&CLSID_SpVoice, NULL, CLSCTX_ALL,
                                      &IID_ISpVoice, (void**)&pVoice);
        if (SUCCEEDED(hr))
        {
            pVoice->lpVtbl->Speak(pVoice, L"Hello, Firefox!", SPF_DEFAULT,
                                  NULL);

            pVoice->lpVtbl->Release(pVoice);
        }
    }

    // MSDN documentation says that even if CoInitalize fails, CoUnitialize
    // must be called
    CoUninitialize();

    return 0;
}

Converting C code to js-ctypes code

Now we have working C code. It could be converted into js-ctypes in almost straightforward way.

CLSID and IID

Needs CLSID and IID of SpVoice and ISpVoice description here.

COM types and functions

Needs COM types (GUID) and functions (CoInitialize etc) description here.

Converted js-ctypes code

Here's converted code, which works with copy-n-paste into Scratchpad, with Browser Environment.

This example uses busy loop and thus Firefox won't respond until the speaking is done. If this code were to be used in a production add-on then to avoid Firefox from locking up, this code should be run from a ChromeWorker.
let { ctypes } = Components.utils.import("resource://gre/modules/ctypes.jsm", {});

// SOME GROUNDWORK
let is64bit;
if (ctypes.voidptr_t.size == 4 /* 32-bit */) {
    is64bit = false;
} else if (ctypes.voidptr_t.size == 8 /* 64-bit */) {
    is64bit = true;
}
let WINABI = is64bit ? ctypes.default_abi : ctypes.winapi_abi;
let CALLBACK_ABI = is64bit ? ctypes.default_abi : ctypes.stdcall_abi;

// LIBRARIES
let lib = ctypes.open('ole32.dll');

// TYPES
// Simple Types
let BYTE = ctypes.unsigned_char;
let DWORD = ctypes.unsigned_long;
let LONG = ctypes.long;
let LPVOID = ctypes.voidptr_t;
let VOID = ctypes.void_t;
let ULONG = ctypes.unsigned_long;
let USHORT = ctypes.unsigned_short;
let WCHAR = ctypes.jschar;

// Advanced Types - based on simple types
let HRESULT = LONG;
let LPCWSTR = WCHAR.ptr;

// Guess Types - these just work I couldnt find a proper defintion for it
let LPUNKNOWN = ctypes.voidptr_t;

// STRUCTURES
// Simple Structures
let GUID = ctypes.StructType('GUID', [
  { 'Data1': ULONG },
  { 'Data2': USHORT },
  { 'Data3': USHORT },
  { 'Data4': BYTE.array(8) }
]);

// Advanced Structures
let CLSID = GUID;
let IID = GUID;

// Super Advanced Structures
let REFIID = IID.ptr;
let REFCLSID = CLSID.ptr;

// VTABLES
let ISpVoiceVtbl = ctypes.StructType('ISpVoiceVtbl');
let ISpVoice = ctypes.StructType('ISpVoice', [{
    'lpVtbl': ISpVoiceVtbl.ptr
}]);
ISpVoiceVtbl.define([
    // start inherit from IUnknown
    { 'QueryInterface': ctypes.voidptr_t },
    { 'AddRef': ctypes.voidptr_t },
    { 'Release': ctypes.FunctionType(CALLBACK_ABI,
        ULONG,            // return
        [
            ISpVoice.ptr
        ]).ptr
    },
    // end inherit from IUnknown
    // start inherit from ISpNotifySource
    // can set to ctypes.voidptr_t if arent going to use it
    { 'SetNotifySink': ctypes.voidptr_t },
    { 'SetNotifyWindowMessage': ctypes.voidptr_t },
    { 'SetNotifyCallbackFunction': ctypes.voidptr_t },
    { 'SetNotifyCallbackInterface': ctypes.voidptr_t },
    { 'SetNotifyWin32Event': ctypes.voidptr_t },
    { 'WaitForNotifyEvent': ctypes.voidptr_t },
    { 'GetNotifyEventHandle': ctypes.voidptr_t },
    // end inherit from ISpNotifySource
    // start inherit from ISpEventSource
    { 'SetInterest': ctypes.voidptr_t },
    { 'GetEvents': ctypes.voidptr_t },
    { 'GetInfo': ctypes.voidptr_t },
    // end inherit from ISpEventSource
    // start ISpVoice
    { 'SetOutput': ctypes.voidptr_t },
    { 'GetOutputObjectToken': ctypes.voidptr_t },
    { 'GetOutputStream': ctypes.voidptr_t },
    { 'Pause': ctypes.voidptr_t },
    { 'Resume': ctypes.voidptr_t },
    { 'SetVoice': ctypes.voidptr_t },
    { 'GetVoice': ctypes.voidptr_t },
    { 'Speak': ctypes.FunctionType(CALLBACK_ABI,
        HRESULT,            // return
        [
            ISpVoice.ptr,
            LPCWSTR,        // *pwcs
            DWORD,          // dwFlags
            ULONG           // *pulStreamNumber
        ]).ptr
    },
    { 'SpeakStream': ctypes.voidptr_t },
    { 'GetStatus': ctypes.voidptr_t },
    { 'Skip': ctypes.voidptr_t },
    { 'SetPriority': ctypes.voidptr_t },
    { 'GetPriority': ctypes.voidptr_t },
    { 'SetAlertBoundary': ctypes.voidptr_t },
    { 'GetAlertBoundary': ctypes.voidptr_t },
    { 'SetRate': ctypes.voidptr_t },
    { 'GetRate': ctypes.voidptr_t },
    { 'SetVolume': ctypes.voidptr_t },
    { 'GetVolume': ctypes.voidptr_t },
    { 'WaitUntilDone': ctypes.voidptr_t },
    { 'SetSyncSpeakTimeout': ctypes.voidptr_t },
    { 'GetSyncSpeakTimeout': ctypes.voidptr_t },
    { 'SpeakCompleteEvent': ctypes.voidptr_t },
    { 'IsUISupported': ctypes.voidptr_t },
    { 'DisplayUI': ctypes.voidptr_t }
    // end ISpVoice
]);

// FUNCTIONS
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms695279%28v=vs.85%29.aspx
let CoInitializeEx = lib.declare('CoInitializeEx', WINABI,
    HRESULT,    // result
    LPVOID,     // pvReserved
    DWORD       // dwCoInit
);

// http://msdn.microsoft.com/en-us/library/windows/desktop/ms688715%28v=vs.85%29.aspx
let CoUninitialize = lib.declare('CoUninitialize', WINABI,
    VOID    // return
);

// http://msdn.microsoft.com/en-us/library/windows/desktop/ms686615%28v=vs.85%29.aspx
let CoCreateInstance = lib.declare('CoCreateInstance', WINABI,
    HRESULT,        // return
    REFCLSID,       // rclsid
    LPUNKNOWN,      // pUnkOuter
    DWORD,          // dwClsContext
    REFIID,         // riid
    LPVOID          // *ppv
);

// HELPER FUNCTIONS
function checkHRESULT(hr /*primative HRESULT*/, funcName /*jsStr*/) {
    // primative because thats what is returned by declared functions that
    // return HRESULT
    hr = hr.toString(); // makes it primative
    if (hr < 0) {
        console.error('HRESULT', hr, 'returned from function ', funcName
                      /*, 'getStrOfResult:', getStrOfResult(hr)*/);
        throw new Error('HRESULT ' + hr + ' returned from function ' + funcName);
    }
}

let CLSIDFromArr = IIDFromArr = function(jsArr_pieces) {
    let guid = GUID(); // CLSID and IID are same they are GUID

    guid.Data1 = parseInt(jsArr_pieces[0], 16);
    guid.Data2 = parseInt(jsArr_pieces[1], 16);
    guid.Data3 = parseInt(jsArr_pieces[2], 16);

    let j = 2;
    for (let i=0; i<8; i++) {
        j++;
        guid.Data4[i] = parseInt(jsArr_pieces[j], 16);
    };

    return guid;
}

// CONSTANTS
let COINIT_MULTITHREADED = 0;
let COINIT_APARTMENTTHREADED = 2;
let CLSCTX_ALL = 0x17;
let CLSID_SpVoice = CLSIDFromArr(['0x96749377', '0x3391', '0x11D2',
                                  '0x9E', '0xE3', '0x00', '0xC0',
                                  '0x4F', '0x79', '0x73', '0x96']);
let IID_ISpVoice = IIDFromArr(['0x6C44DF74', '0x72B9', '0x4992',
                               '0xA1', '0xEC', '0xEF', '0x99',
                               '0x6E', '0x04', '0x22', '0xD4']);
let SPF_DEFAULT = 0;

function main() {
    let spVoice;
    let spVoicePtr;
    try {
        // MSDN Docs tell us ot use CoInitEx instead of CoInit, and default is 0
        // which is COINIT_MULTITHREADED but it wouldnt work so I used
        // COINIT_APARTMENTTHREADED and it worked checkHRESULT would throw a bad
        // HRESULT of RPC_E_CHANGED_MODE which is 0x80010106 which is
        // -2147417850.
        primative_hr = CoInitializeEx(null, COINIT_APARTMENTTHREADED);
        checkHRESULT(primative_hr, "CoInitializeEx");

        spVoicePtr = ISpVoice.ptr();
        primative_hr = CoCreateInstance(CLSID_SpVoice.address(), null,
                                        CLSCTX_ALL, IID_ISpVoice.address(),
                                        spVoicePtr.address());
        checkHRESULT(primative_hr, "CoCreateInstance");
        spVoice = spVoicePtr.contents.lpVtbl.contents;

        let aText = 'Hello Firefox!';
        let aFlags = SPF_DEFAULT;
        primative_hr = spVoice.Speak(spVoicePtr, aText, aFlags, 0);
        checkHRESULT(primative_hr, "CoCreateInstance");

    } catch (ex) {
        console.error('ex occured:', ex);
    } finally {
        if (spVoice) {
            spVoice.Release(spVoicePtr);
        }
        CoUninitialize();
    }
}
main();

lib.close();

Other Examples

Document Tags and Contributors

 Contributors to this page: kylmis43, Noitidart, arai
 Last updated by: kylmis43,