3
votes

I have an old Windows DLL, without source code, who implement a table of utility functions. Years ago it was planned to convert it in a COM object so an IUnknown interface was implemented. To work with this DLL, there is a header file (simplified):

interface IFunctions : public IUnknown
{
    virtual int function1(int p1, int p2) = 0;
    virtual void function2(int p1) = 0;
    // and the likes ...
}

But no CLSID was defined for IFunctions interface. And eventually the interface definition in header file is non-compliant with COM standard.

From C++ the DLL can be loaded with

CoCreateInstance(clsid, 0, CLSCTX_INPROC_SERVER, clsid, ptr);

and with some pointer arithmetic from 'ptr' I find the addresses of funcion1(), etc. Since it worked, no complete COM implementation were done so I cannot QueryInterface for IFunctions interface because the interface is not a COM interface. In Windows Registry I find only the CLSID of the object and a reference to the DLL as it InprocServer32.

I do not have much experience in Python, but I need to use this DLL from Python, perhaps using ctypes and comtypes. I can load the DLL with (CLSID from registry)

unk = CreateObject('{11111111-2222-3333-4444-555555555555}', clsctx=comtypes.CLSCTX_INPROC_SERVER)

I know that in the VTable of the COM object function1() address is just after QueryInterface(), AddRef(), Release() but I cannot find a solution to implement a class like:

class DllFunction:
    # not necessary, but for completeness ...
    def QueryInterface(self, interface, iid=None):
        return unk.QueryInterface(comtypes.IUnknown)
    def AddRef(slef):
        return unk.AddRef()
    def Release(self):
        return unk.Release()
    # Functions I actually need to call from Python
    def Function1(self, p1, p2):
        # what to do ??
    def Function2(self, p1):
    # etc.

I would like to implement this solution in Python trying to avoid the development of an extension module in C++.

Thanks for any help.

1
Seems unlikely, you probably would be better off either fixing the DLL, or making a wrapper in C++ that does present a complete COM interfaceM.M
The way of this article may be applicable. Because it is Japanese, please use Google translation etc. for reading.kunif

1 Answers

5
votes

Thanks to who provided some hints. Actually I cannot fix the DLL because I do not have the source code. Wrapping it in C++ was an option, but developing a wrapping Python module in C sounds better. My plan was to use Python only, possibly without additional modules, so I managed to solve the issue using only ctypes. The following code show the solution. It works, but it needs some improvements (error checking, etc).

'''
    Simple example of how to use the DLL from Python on Win32.

    We need only ctypes.
'''
import ctypes
from ctypes import *
'''
    We need a class to mirror GUID structure
'''
class GUID(Structure):
    _fields_ = [("Data1", c_ulong),
                ("Data2", c_ushort),
                ("Data3", c_ushort),
                ("Data4", c_ubyte * 8)]

if __name__ == "__main__":
    '''
        COM APIs to activate/deactivate COM environment and load the COM object
    '''
    ole32=WinDLL('Ole32.dll')
    CoInitialize = ole32.CoInitialize
    CoUninitialize = ole32.CoUninitialize
    CoCreateInstance = ole32.CoCreateInstance
    '''
        COM environment initialization
    '''
    rc = CoInitialize(None)
    '''
        To use CoCreate Instance in C (not C++):
            void * driver = NULL;
            rc = CoCreateInstance(&IID_Driver,      // CLSID of the COM object
                           0,                       // no aggregation
                           CLSCTX_INPROC_SERVER,    // CLSCTX_INPROC_SERVER = 1
                           &IID_Driver,             // CLSID of the required interface
                           (void**)&driver);        // result
        In Python it is:
    '''
    clsid = GUID(0x11111111, 0x2222, 0x3333, 
               (0x44, 0x44, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55))
    drv = c_void_p(None)
    rc = CoCreateInstance(byref(clsid), 0, 1, byref(clsid), byref(drv))
    '''
        Pointers manipulation. Short form:
        function = cast( c_void_p( cast(drv, POINTER(c_void_p))[0] ), POINTER(c_void_p))
    '''
    VTable = cast(drv, POINTER(c_void_p))
    wk = c_void_p(VTable[0])
    function = cast(wk, POINTER(c_void_p))
    #print('VTbale address: ', hex(VTable[0]))
    #print('QueryInterface address: ', hex(function[0]))
    #print('AddRef address: ', hex(function[1]))
    #print('Release address: ', hex(function[2]))
    '''
        To define functions from their addresses we first need to define their WINFUNCTYPE.
        In C we call QueryInterface:
            HRESULT rc = driver->lpVtbl->QueryInterface(driver, &IID_IUnknown, (void**)&iUnk);
        So we need a long (HRESULT) return value and three pointers. It would be better to be
        more accurate in pointer types, but ... it works!
        We can use whatever names we want for function types and functions
    '''
    QueryInterfaceType = WINFUNCTYPE(c_long, c_void_p, c_void_p, c_void_p)
    QueryInterface = QueryInterfaceType(function[0])
    AddRefType = WINFUNCTYPE(c_ulong, c_void_p)
    AddRef = AddRefType(function[1])
    ReleaseType = WINFUNCTYPE(c_ulong, c_void_p)
    Release = ReleaseType(function[2])
    '''
        The same for other functions, but library functions do not want 'this':
            long rc = driver->lpVtbl->init(0);
    '''
    doThisType = WINFUNCTYPE(c_long, c_void_p)
    doThis=doThisType(function[3])

    getNameType = WINFUNCTYPE(c_int, c_char_p)
    getName = getNameType(function[4])
    getName.restype = None      # to have None since function is void

    getVersionType = WINFUNCTYPE(c_long)
    getVersion = getVersionType(function[5])

    getMessageType = WINFUNCTYPE(c_int, c_char_p)
    getMessage = getMessageType(function[6])
    getMessage.restype = None       # to have None since function is void
    '''
        Now we can use functions in plain Python
    '''
    rc = doThis(0)
    print(rc)

    name = create_string_buffer(128)
    rc = getName(name)
    print(rc)
    print(name.value)

    ver = getVersion()
    print(ver)

    msg = create_string_buffer(256)
    rc = getMessage(msg)
    print(rc)
    print(msg.value)
    '''
        Unload DLL and reset COM environment
    '''
    rc = Release(drv)
    rc = CoUninitialize()

    print("Done!")

I hope this example will be useful to somebody. It can be used to wrap a COM object without comtypes and, to me, clarify how ctypes works.