Tuesday, 4 April 2017

COM System Functions

So COM is complex and unfortantely there are many toolkits and layers that are designed to help but actually can obscure understanding.  Sometimes, it is better to know the underlying mechanism and in COM so very often this means calling a System function.  These system functions are exported by well know COM dlls like OLE32.DLL and OLEAUT32.DLL.

On this page we show all the functions that these DLLs export so they can be browsed.

Array Manipulation Functions Decimal Arithmetic Functions String Manipulation Functions
SafeArrayAccessData VarDecAbs SysAddRefString
SafeArrayAddRef VarDecAdd SysAllocString
SafeArrayAllocData VarDecCmp SysAllocStringByteLen
SafeArrayAllocDescriptor VarDecCmpR8 SysAllocStringLen
SafeArrayAllocDescriptorEx VarDecDiv SysFreeString
SafeArrayCopy VarDecFix SysReAllocString
SafeArrayCopyData VarDecInt SysReAllocStringLen
SafeArrayCreate VarDecMul SysReleaseString
SafeArrayCreateEx VarDecNeg SysStringByteLen
SafeArrayCreateVector VarDecRound SysStringLen
SafeArrayCreateVectorEx VarDecSub Type Building Functions
SafeArrayDestroy Dispatch functions CreateTypeLib
SafeArrayDestroyData CreateDispTypeInfo CreateTypeLib2
SafeArrayDestroyDescriptor CreateStdDispatch OaBuildVersion
SafeArrayGetDim DispCallFunc Type Description Functions
SafeArrayGetElement DispGetIDsOfNames LHashValOfNameSys
SafeArrayGetElemsize DispGetParam LHashValOfNameSysA
SafeArrayGetIID DispInvoke LoadRegTypeLib
SafeArrayGetLBound Error-Handling Functions LoadTypeLib
SafeArrayGetRecordInfo CreateErrorInfo LoadTypeLibEx
SafeArrayGetUBound GetErrorInfo OaEnablePerUserTLibRegistration
SafeArrayGetVartype SetErrorInfo QueryPathOfRegTypeLib
SafeArrayLock Formatting Functions RegisterTypeLib
SafeArrayPtrOfIndex VarFormat RegisterTypeLibForUser
SafeArrayPutElement VarFormatCurrency UnRegisterTypeLib
SafeArrayRedim VarFormatDateTime UnRegisterTypeLibForUser
SafeArrayReleaseData VarFormatFromTokens UDT Functions and Interfaces
SafeArrayReleaseDescriptor VarFormatNumber ClearCustData
SafeArraySetIID VarFormatPercent GetRecordInfoFromGuids
SafeArraySetRecordInfo VarMonthName GetRecordInfoFromTypeInfo
SafeArrayUnaccessData VarTokenizeFormatString Variant Arithmetic Functions
SafeArrayUnlock VarWeekdayName VarAbs
BSTR and Vector Conversion Functions Numeric Parsing Functions VarAdd
BstrFromVector VarNumFromParseNum VarAnd
SetOaNoCache VarParseNumFromStr VarCat
VarBstrCat OLE and Data Transfer VarCmp
VarBstrCmp OleCreateFontIndirect VarDiv
VectorFromBstr OleCreatePictureIndirect VarEqv
COM Fundamentals OleLoadPicture VarFix
DllCanUnloadNow OleLoadPictureEx VarIdiv
DllGetClassObject Picture Functions VarImp
DllRegisterServer OleLoadPictureFile VarInt
DllUnregisterServer OleLoadPictureFileEx VarMod
OleIconToCursor OleSavePictureFile VarMul
Controls and Property Pages Registration Functions VarNeg
OleCreatePropertyFrame GetActiveObject VarNot
OleCreatePropertyFrameIndirect RegisterActiveObject VarOr
OleLoadPicturePath RevokeActiveObject VarPow
OleTranslateColor RPC Data Marshaling Functions VarR4CmpR8
Currency Arithmetic Functions BSTR_UserFree VarR8Pow
VarCyAbs BSTR_UserFree64 VarR8Round
VarCyAdd BSTR_UserMarshal VarRound
VarCyCmp BSTR_UserMarshal64 VarSub
VarCyCmpR8 BSTR_UserSize VarXor
VarCyFix BSTR_UserSize64 Variant Manipulation Functions
VarCyInt BSTR_UserUnmarshal VariantChangeType
VarCyMul BSTR_UserUnmarshal64 VariantChangeTypeEx
VarCyMulI4 VARIANT_UserUnmarshal64 VariantClear
VarCyMulI8 VARIANT_UserUnmarshal VariantCopy
VarCyNeg VARIANT_UserSize64 VariantCopyInd
VarCyRound VARIANT_UserSize VariantInit
VarCySub VARIANT_UserMarshal64 (not documented)
Date and Time Conversion Functions VARIANT_UserMarshal OACreateTypeLib2
DosDateTimeToVariantTime VARIANT_UserFree64 OACleanup
GetAltMonthNames VARIANT_UserFree HWND_UserUnmarshal64
SystemTimeToVariantTime LPSAFEARRAY_UserUnmarshal64 HWND_UserUnmarshal
VarDateFromUdate LPSAFEARRAY_UserUnmarshal HWND_UserSize64
VarDateFromUdateEx LPSAFEARRAY_UserSize64 HWND_UserSize
VariantTimeToDosDateTime LPSAFEARRAY_UserSize HWND_UserMarshal64
VariantTimeToSystemTime LPSAFEARRAY_UserMarshal64 HWND_UserMarshal
VarUdateFromDate LPSAFEARRAY_UserMarshal HWND_UserFree64
LPSAFEARRAY_UserFree64 HWND_UserFree
LPSAFEARRAY_UserFree
LPSAFEARRAY_Unmarshal
LPSAFEARRAY_Size
LPSAFEARRAY_Marshal

What is also exported from OleAut32.dll is a whole number of Variant conversion functions which are best presented cross-tabulated.

Dec Bool Bstr Cy Date Disp I1 I2 I4 I8 R4 R8 UI1 UI2 UI4 UI8
Dec # # # # # # # # # # # # # # #
Bool # # # # # # # # # # # # # # #
Bstr # # # # # # # # # # # # # # #
Cy # # # # # # # # # # # # # # #
Date # # # # # # # # # # # # # # #
I1 # # # # # # # # # # # # # # #
I2 # # # # # # # # # # # # # # #
I4 # # # # # # # # # # # # # # #
I8 # # # # # # # # # # # # # #
R4 # # # # # # # # # # # # # # #
R8 # # # # # # # # # # # # # # #
UI1 # # # # # # # # # # # # # # #
UI2 # # # # # # # # # # # # # # #
UI4 # # # # # # # # # # # # # # #
UI8 # # # # # # # # # # # # # #

So another nice dll is OleDlg.dll which seems to be a repository of OLE and Data Transfer dialogs. Most of the entry points are duplicated with A meaning Windows 98 and W meaning Windows 2000 onwards.

oleDlg.dll
OleUIAddVerbMenuA OleUIAddVerbMenuW
OleUIBusyA OleUIBusyW
OleUICanConvertOrActivateAs
OleUIChangeIconA OleUIChangeIconW
OleUIChangeSourceA OleUIChangeSourceW
OleUIConvertA OleUIConvertW
OleUIEditLinksA OleUIEditLinksW
OleUIInsertObjectA OleUIInsertObjectW
OleUIObjectPropertiesA OleUIObjectPropertiesW
OleUIPasteSpecialA OleUIPasteSpecialW
OleUIPromptUserA OleUIPromptUserW
OleUIUpdateLinksA OleUIUpdateLinksW

And now from OLE32.DLL which is the daddy of all COM System DLLs. Breaking it down first we have the error info functions

Automation - Error Info
CreateErrorInfo GetErrorInfo SetErrorInfo

Also from OLE32.DLL a chunk of funcitons that handle OLE and Data Transfer such as clipboards, links, drag and drop.

OLE and Data Transfer
CreateDataAdviseHolder CreateDataCache CreateOleAdviseHolder
DoDragDrop OleCreate OleCreateDefaultHandler
OleCreateEmbeddingHelper OleCreateEx OleCreateFromData
OleCreateFromDataEx OleCreateFromFile OleCreateFromFileEx
OleCreateLink OleCreateLinkEx OleCreateLinkFromData
OleCreateLinkFromDataEx OleCreateLinkToFile OleCreateLinkToFileEx
OleCreateMenuDescriptor OleCreateStaticFromData OleDestroyMenuDescriptor
OleDraw OleDuplicateData OleFlushClipboard
OleGetClipboard OleInitialize OleIsCurrentClipboard
OleIsRunning OleLoad OleLoadFromStream
OleLockRunning OleMetafilePictFromIconAndLabel OleNoteObjectVisible
OleQueryCreateFromData OleQueryLinkFromData OleRegEnumFormatEtc
OleRegEnumVerbs OleRun OleSave
OleSaveToStream OleSetClipboard OleSetContainedObject
OleSetMenuDescriptor OleTranslateAccelerator OleUIAddVerbMenu
OleUninitialize RegisterDragDrop ReleaseStgMedium
RevokeDragDrop

Also in OLE32.Dll function concerning Structured Storage

Structured Storage
CreateILockBytesOnHGlobal CreateStreamOnHGlobal
FmtIdToPropStgName FreePropVariantArray
GetConvertStg GetHGlobalFromILockBytes
GetHGlobalFromStream OleConvertIStorageToOLESTREAM
OleConvertIStorageToOLESTREAMEx OleConvertOLESTREAMToIStorage
OleConvertOLESTREAMToIStorageEx PropStgNameToFmtId
PropVariantClear PropVariantCopy
ReadClassStg ReadClassStm
ReadFmtUserTypeStg StgConvertPropertyToVariant
SetConvertStg StgConvertVariantToProperty
StgCreateDocfile StgCreateDocfileOnILockBytes
StgCreatePropSetStg StgCreatePropStg
StgCreateStorageEx StgGetIFillLockBytesOnFile
StgGetIFillLockBytesOnILockBytes StgIsStorageFile
StgIsStorageILockBytes StgOpenAsyncDocfileOnIFillLockBytes
StgOpenPropStg StgOpenStorage
StgOpenStorageEx StgOpenStorageOnILockBytes
StgPropertyLengthAsVariant StgSetTimes
WriteClassStg WriteClassStm
WriteFmtUserTypeStg

Finally, the canonical functions described as COM Fundamentals.

COM Fundamentals
BindMoniker CLSIDFromProgID
CLSIDFromProgIDEx CLSIDFromString
CoAddRefServerProcess CoAllowSetForegroundWindow
CoAllowUnmarshalerCLSID CoCancelCall
CoCopyProxy CoCreateFreeThreadedMarshaler
CoCreateGuid CoCreateInstance
CoCreateInstanceEx CoCreateInstanceFromApp
CoDisableCallCancellation CoDisconnectContext
CoDisconnectObject CoDosDateTimeToFileTime
CoEnableCallCancellation CoEnterApplicationThreadLifetimeLoop
CoFileTimeNow CoFileTimeToDosDateTime
CoFreeAllLibraries CoFreeLibrary
CoFreeUnusedLibraries CoFreeUnusedLibrariesEx
CoGetApartmentType CoGetCallContext
CoGetCallerTID CoGetCancelObject
CoGetClassObject CoGetContextToken
CoGetCurrentLogicalThreadId CoGetCurrentProcess
CoGetInstanceFromFile CoGetInstanceFromIStorage
CoGetInterceptor CoGetInterfaceAndReleaseStream
CoGetMalloc CoGetMarshalSizeMax
CoGetObject CoGetObjectContext
CoGetPSClsid CoGetStandardMarshal
CoGetStdMarshalEx CoGetSystemSecurityPermissions
CoGetTreatAsClass CoHandlePriorityEventsFromMessagePump
CoImpersonateClient CoIncrementMTAUsage
CoInitialize CoInitializeEx
CoInitializeSecurity CoInstall
CoInvalidateRemoteMachineBindings CoIsHandlerConnected
CoIsOle1Class CoLoadLibrary
CoLockObjectExternal CoMarshalHresult
CoMarshalInterface CoMarshalInterThreadInterfaceInStream
CoQueryAuthenticationServices CoQueryClientBlanket
CoQueryProxyBlanket CoRegisterActivationFilter
CoRegisterChannelHook CoRegisterClassObject
CoRegisterInitializeSpy CoRegisterMallocSpy
CoRegisterMessageFilter CoRegisterPSClsid
CoRegisterSurrogate CoRegisterSurrogateEx
CoReleaseMarshalData CoReleaseServerProcess
CoResumeClassObjects CoRevertToSelf
CoRevokeClassObject CoRevokeInitializeSpy
CoRevokeMallocSpy CoSetCancelObject
CoSetMessageDispatcher CoSetProxyBlanket
CoSuspendClassObjects CoSwitchCallContext
CoTaskMemAlloc CoTaskMemFree
CoTaskMemRealloc CoTestCancel
CoTreatAsClass CoUninitialize
CoUnmarshalHresult CoUnmarshalInterface
CoWaitForMultipleHandles CoWaitForMultipleObjects
CreateAntiMoniker CreateBindCtx
CreateClassMoniker CreateFileMoniker
CreateGenericComposite CreateItemMoniker
CreateObjrefMoniker CreatePointerMoniker
DllDebugObjectRPCHook DllGetClassObject
DllRegisterServer GetClassFile
GetRunningObjectTable IIDFromString
IsAccelerator IsEqualGUID
MkParseDisplayName MonikerCommonPrefixWith
MonikerRelativePathTo OleDoAutoConvert
OleGetAutoConvert OleGetIconOfClass
OleGetIconOfFile OleRegGetMiscStatus
OleRegGetUserType OleSetAutoConvert
ProgIDFromCLSID StringFromCLSID
StringFromGUID2 StringFromIID

Using DUMPBIN.exe to replicate functionality of Dependency Walker (Depends.exe)

So Depends.exe is a nice program that can interrogate an executable and determine its dependencies as its name suggests but it is packed with tons of extra features as well. One such feature is the ability to see the entry points into a Dll.

Here is a screenshot showing Excel.exe and its dependent Dlls, one of which OLE32.DLL is selected. On the right hand side one can see the entry points of OLE32.DLL. In other words this shows you all the functions you can call into. OLE32.DLL is a COM runtime and it comes as no surprise that it implements the most famous COM functions such CoCreateInstance.

Although Depends.exe has a clipboard feature that allows copying and pasting of fragments of this list it would be nicer to get a list of these functions programatically. Listing a DLL's exported functions is what the command line program DUMPBIN.exe can do. It will be installed as part of Microsoft Visual Studio if you selected C++ as an option (DUMPBIN.exe is a program familiar to C++ programmers).

In the example below I have shortened the path to a temporary environment variable so its fits on one blog page and does not spill over. You may ignore such typesetting cosmetics.


C:\set dumpbinpath="C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\
                   VC\Tools\MSVC\14.10.25017\bin\HostX86\x86"

C:\%dumpbinpath%\dumpbin /EXPORTS c:\windows\system32\oleaut32.dll > n:\dump.txt

C:\n:\dump.txt

The final line should launch notepad or whatever text editor you have assigned to launch for .txt files. Inside the files you should see contents such as this extract

        866   1A 000B0C20 CoCheckElevationEnabled
        867   1B          CoCopyProxy (forwarded to ...
        868   1C          CoCreateFreeThreadedMarshaler (forwarded to ...
        869   1D          CoCreateGuid (forwarded to ...
        870   1E          CoCreateInstance (forwarded to ...
        871   1F          CoCreateInstanceEx (forwarded to ...
        872   20          CoCreateInstanceFromApp (forwarded to ...
        873   21          CoCreateObjectInContext (forwarded to ...
        874   22          CoDeactivateObject (forwarded to ...


It can be seen that the function name always begins at column 27 and is terminated either by end of line or by a space. So we can write some code for this.

Option Explicit

Sub Test()
    Dim vOle32Exports As Variant, plCount As Long
    vOle32Exports = GetExports("n:\dumpbin_ol32_dll_exports.txt", plCount)
    
    shDllExports.Cells(1, 1) = "ole32.dll"
    shDllExports.Cells(2, 1).Resize(plCount).Value = Application.Transpose(vOle32Exports)
    
    
    Dim vOleAut32Exports As Variant
    vOleAut32Exports = GetExports("n:\dumpbin_oleaut32_dll_exports.txt", plCount)
    
    shDllExports.Cells(1, 2) = "oleaut32.dll"
    shDllExports.Cells(2, 2).Resize(plCount).Value = Application.Transpose(vOleAut32Exports)
    
End Sub

Function GetExports(ByVal sFileName As String, ByRef plCount As Long) As Variant
    Dim fso As Scripting.FileSystemObject
    Set fso = New Scripting.FileSystemObject
    
    Dim dicLines As Scripting.Dictionary
    Set dicLines = New Scripting.Dictionary
    
    Dim txt As Scripting.TextStream
    Set txt = fso.OpenTextFile(sFileName)
    
    Dim bExportsSection As Boolean
    bExportsSection = False
    While Not txt.AtEndOfStream
        Dim sLine As String
        sLine = txt.ReadLine
        If Not bExportsSection Then
            If Trim(sLine) = "ordinal hint RVA      name" Then
            
                bExportsSection = True
                sLine = txt.ReadLine
            End If
        Else
            If Trim(sLine) = "" Then
                bExportsSection = False
            Else
                Dim sExport As String
                sExport = Mid(sLine, 27)
                sExport = Split(sExport)(0)
                
                dicLines.Add dicLines.Count, sExport
            End If
        End If
        
    Wend
    txt.Close
    Set txt = Nothing
    
    Set fso = Nothing
    plCount = dicLines.Count
    
    GetExports = dicLines.Items
    
    Set dicLines = Nothing

End Function


Monday, 3 April 2017

Another WMI Disk Query in VBA

So in the preparation on the WinForms/WebBrowser blog entry I investigated some WMI code and found a VBS script that did a lot of querying of disks. I refactored the code because I wanted to understand it and also write the disk details to a block of cells.

Here is the code.

Option Explicit

Private moWMIService As Object
Private mvCells() As Variant
Private mlPass As Long
Private mlRowCount As Long

'IterateDisk
Private mlCurrentDriveIndex As Long
Private msCurrentDriveInterfaceType As String
Private msCurrentDriveCaption As String
Private mvCurrentDriveSize As Variant

Private mvCurrentPartitionNumber As Variant
Private msCurrentActive As String '* same as bootable
Private msCurrentPrimary As String

Private Enum coColumnOrdinals
    coDriveIndex
    coDriveInterfaceType
    coDriveCaption
    coDriveSize
    
    coPartitionNumber
    coActive
    coPrimary

    coLogicalDiskDeviceId
    coLogicalDiskFileSystem
    coLogicalDiskSize
    coLogicalDiskFreeSpace
    coLogicalDiskVolumeName
    

    coFirst = coDriveIndex
    coLast = coLogicalDiskVolumeName
    coCount = coLast - coFirst + 1
End Enum


Sub Test()

    Set moWMIService = GetObject("winmgmts:\\.\root\cimv2")
    
    Dim cDiskDrives As Object
    Set cDiskDrives = moWMIService.ExecQuery("SELECT * FROM Win32_DiskDrive")
    
    Erase mvCells
    
    For mlPass = 0 To 1
        mlRowCount = 0
        IterateDisks cDiskDrives
        
        If mlPass = 0 Then
        If mlRowCount > 0 Then ReDim mvCells(1 To mlRowCount, coFirst To coLast)
        End If
    Next mlPass
    
    Dim vColHeadings As Variant
    vColHeadings = Array1dto2d(Array("DeviceId", "Interface Type", "DeviceDesc", _
            "DeviceSize", "Partition", "Bootable", "Primary", "DriveLetter", _
            "FileSystem", "PartitionSize", "PartitionFreeSpace", "VolumeName"))
    shDrives.Cells(1, 1).Resize(1, coCount).Value = vColHeadings
    shDrives.Cells(2, 1).Resize(mlRowCount, coCount).Value = mvCells
End Sub

Function Array1dto2d(v) As Variant
    Array1dto2d = Application.Transpose(Application.Transpose(v))
End Function

Sub IterateDisks(ByVal cDiskDrives As Object)
    Dim oDrive As Object
    
    Dim dicSort As Scripting.Dictionary
    Set dicSort = New Scripting.Dictionary
    
    For Each oDrive In cDiskDrives
        Debug.Assert IsNumeric(oDrive.Index)
        dicSort.Add CInt(oDrive.Index), oDrive
    Next
    
    Dim lDriveLoop As Long
    For lDriveLoop = 0 To dicSort.Count - 1
        Set oDrive = dicSort.Item(lDriveLoop)
        IterateDisk oDrive
    Next lDriveLoop
End Sub


Sub IterateDisk(ByVal oDrive As Object)

    mlCurrentDriveIndex = oDrive.Index
    msCurrentDriveInterfaceType = oDrive.InterfaceType
    msCurrentDriveCaption = oDrive.Caption
    mvCurrentDriveSize = oDrive.Size
 
    Dim cPartitions As Object
    Set cPartitions = moWMIService.ExecQuery( _
        "ASSOCIATORS OF {Win32_DiskDrive.DeviceID=""" _
        & Replace(oDrive.DeviceID, "\", "\\") & """} WHERE AssocClass = " & _
        "Win32_DiskDriveToDiskPartition")
 
 
    IteratePartitions cPartitions

End Sub

Sub IteratePartitions(ByVal cPartitions As Object)
    Dim oPartition As Object

    
    For Each oPartition In cPartitions
        IteratePartition oPartition

    Next
End Sub

Sub IteratePartition(ByVal oPartition As Object)
    
    mvCurrentPartitionNumber = Split(oPartition.DeviceID)(3)
    
    msCurrentActive = VBA.IIf(oPartition.Bootable, "Yes", "No")
    
    msCurrentPrimary = VBA.IIf(oPartition.PrimaryPartition, "Yes", "No")
    
    IterateLogicalDisks oPartition.DeviceID

End Sub

Sub IterateLogicalDisks(ByVal sPartition_DeviceID As String)
    
    Dim cLogicalDisks As Object
    Set cLogicalDisks = moWMIService.ExecQuery _
        ("ASSOCIATORS OF {Win32_DiskPartition.DeviceID=""" & sPartition_DeviceID _
        & """} WHERE AssocClass = Win32_LogicalDiskToPartition")
    
    If cLogicalDisks.Count > 0 Then
        ReDim v(1 To cLogicalDisks.Count, 1 To 5)
    End If
    
    Dim oLogicalDisk As Object
    For Each oLogicalDisk In cLogicalDisks
        mlRowCount = mlRowCount + 1
        IterateLogicalDisk oLogicalDisk
    Next

End Sub

Sub IterateLogicalDisk(oLogicalDisk As Object)
    With oLogicalDisk
        
        If mlPass = 1 Then
            
            mvCells(mlRowCount, coDriveIndex) = mlCurrentDriveIndex
            mvCells(mlRowCount, coDriveInterfaceType) = msCurrentDriveInterfaceType
            mvCells(mlRowCount, coDriveCaption) = msCurrentDriveCaption
            mvCells(mlRowCount, coDriveSize) = Format3(mvCurrentDriveSize)

            mvCells(mlRowCount, coPartitionNumber) = mvCurrentPartitionNumber
            mvCells(mlRowCount, coActive) = msCurrentActive
            mvCells(mlRowCount, coPrimary) = msCurrentPrimary
        
            mvCells(mlRowCount, coLogicalDiskDeviceId) = oLogicalDisk.DeviceID
            mvCells(mlRowCount, coLogicalDiskFileSystem) = oLogicalDisk.FileSystem
            mvCells(mlRowCount, coLogicalDiskSize) = Format3(.Size)
            mvCells(mlRowCount, coLogicalDiskFreeSpace) = Format3(.FreeSpace)
            mvCells(mlRowCount, coLogicalDiskVolumeName) = oLogicalDisk.VolumeName
            
        End If
        
    End With


End Sub

Function Format3(n)
    Format3 = Format(n / 1000000, "#,###") ' 0, -1, 0, -1)
End Function


Sadly VBA's GetObject's Custom Activation Syntax is not implemented in C#/.NET

For a while I have been curious as to a syntax of VBA's GetObject which has become quite prevalent.  GetObject is used to get an object that have been registered in the RunningObjectTable, C# has the equivalent of Marshal.GetActiveObject.  VBA's GetObject has an extra use cases in that if called with a filename and the file is not loaded into an application then it is loaded on demand, I have yet to establish if C# can do this.

The real extra use case of GetObject that caught my eye is when using either WMI or LDAP.  Here is some sample code instantiating LDAP

    Set objUser = GetObject("LDAP://" & strUserDN) 

And here is some code instantiating WMI

    Set oWMIService = GetObject("winmgmts:\\.\root\cimv2")

And third and final case is a Windows Communication Foundation WCF Moniker

Set typedServiceMoniker = GetObject(  
"service4:address=http://localhost/ServiceModelSamples/service.svc, binding=wsHttpBinding,   
contractType={9213C6D2-5A6F-3D26-839B-3BA9B82228D3}") 

So you can see some very different syntaxes beyond the plain COM Server's ProgId of "Excel.Application" and filenames then. There is some pattern, they all start with a text string and then a colon ":", after the colon they can be custom. This is a custom activation syntax. Sadly, it is not callable/useable from C#. This is ironic because WCF services are written in C# (though C# to C# code should clearly avoid VBA patterns).

I'm afraid to say I've wasted some time investigating this. I will post my interim findings here but not to a conclusion.

A key resource is a good book by Guy and Henry Eddon titled Essential COM, fortunately there is an online version. The relevant section is The MkParseDisplayName Function .  Here is a quote

MkParseDisplayName accepts two primary string formats. The first ... The second string format is the more general and thus more important of the two formats. In this format, MkParseDisplayName accepts any string in the form ProgID:ObjectName, where ProgID is a registered program identifier. This architecture allows anyone to write a custom moniker that hooks into the COM+ namespace simply by creating a program identifier (ProgID) entry in the registry.  
The following steps are executed when MkParseDisplayName encounters a string that has the ProgID:ObjectName format: 

  1. The ProgID is converted to a CLSID using the CLSIDFromProgID function. The result is the CLSID of the moniker. 
  2. CoGetClassObject is called to instantiate the moniker. 
  3. IUnknown::QueryInterface is called to request the IParseDisplayName interface. 
  4. The IParseDisplayName::ParseDisplayName method is called to parse the string passed to MkParseDisplayName. 
  5. In the moniker's IParseDisplayName::ParseDisplayName method, a moniker that names the > object identified by the string is created. 
  6. The resulting IMoniker pointer is returned to the client. 

For example, if the string "Hello:Maya" is passed to MkParseDisplayName, the HKEY_CLASSES_ROOT section of the registry is searched for the ProgID Hello. If Hello is found, the CLSID subkey below the ProgID is used to locate and load the moniker. The moniker's IParseDisplayName::ParseDisplayName method is then called to create a moniker object that names the Maya object. Figure 11-2 shows the registry entries involved in this hypothetical example; the numbered labels indicate the order in which the information is obtained from the registry.

So I found this fascinating and I knew of LDAP, WMI and a WCF service moniker which I had developed much earlier I wondered what other COM servers followed this pattern.  I wrote some VBA code which scanned the registry looking for the above pattern and write to a file.  I need a separate C++ program to scan through that file and instantiate the COM server candidates and QueryInterface for IParseDisplayName.  The C++ program is given here.

// C++LookingForCustomActivation.cpp : Defines the entry point for the console application.
//

#include <iostream>
#include <vector>
#include <fstream>
#include <sstream> 
#include "stdafx.h"
#include "Objbase.h"  // required for CoInitialize
#include "atlbase.h"  // required for CComBSTR

using namespace std;

bool ClassImplementsInterface(CLSID clsid1, IID interfaceId, std::string progidInfo);

int _tmain(int argc, _TCHAR* argv[])
{
	::CoInitialize(0);

	std::string idsFilename = "N:\\ProgIdsClassIds.txt";
	std::ifstream idsFileStream(idsFilename, ios_base::in);

	std::string progid;
	std::string clsid;
	std::vector<std::pair<std::string, std::string>> ids;

	while (idsFileStream >> progid >> clsid) {
		bool goodClsId = false ;
		if (clsid.size() == 38) {
			if (clsid[0] == '{' && clsid[37] == '}') {
				goodClsId = true;
			}
		}

		if (goodClsId) {
			
			auto id = std::make_pair(progid, clsid);
			ids.push_back(id);
		}
		else
		{
			cout << "Problem!";
		}
	}

	CLSID iParseDisplayName;
	CLSIDFromString(CComBSTR("{0000011A-0000-0000-C000-000000000046}"), 
                   &iParseDisplayName);

	for (std::vector<std::pair<std::string, std::string>>::iterator
                          it = ids.begin(); it != ids.end(); ++it) {
		progid = it->first;
		clsid = it->second;

		std::wstring stemp = std::wstring(clsid.begin(), clsid.end());
		LPCWSTR sw = stemp.c_str();

		CLSID clsidLoop;
		CLSIDFromString(sw, &clsidLoop);

		if (ClassImplementsInterface(clsidLoop, iParseDisplayName, progid)) {
			std::cout << progid << endl;
		}
		else
		{
			//std::cout << "Nope.";
		}
	}

	int wait;
	std::cin >> wait;

	::CoUninitialize();
	return 0;
}

bool ClassImplementsInterface(CLSID clsid1, IID interfaceId, 
                                                    std::string progidInfo)
{
	CLSID iUnknown;
	CLSIDFromString(CComBSTR("{00000000-0000-0000-C000-000000000046}"), 
                                                               &iUnknown);

	IUnknown* pUnk = NULL;
	HRESULT hr;
	bool abort = false;
	try {
		hr = CoCreateInstance(clsid1,
			NULL,
			CLSCTX_INPROC_SERVER,
			iUnknown,
			reinterpret_cast<void**>(&pUnk));
	}
	catch (const std::exception& e)
	{
		cout << "failed to CoCreateInstance " << progidInfo;
		abort = true;
	}
	if (!abort)
	{
		if (hr == S_OK) {
			void* pItf = NULL;
			hr = pUnk->QueryInterface(interfaceId, &pItf);
			if (hr == S_OK) {
				return true;
			}
			else
			{
				return false;
			}
		}
	}
}

Unfortunately this threw out hundreds of false positives so I was stuck with my three examples of LDAP, WMI and WCF. At this point I turned to writing my own example and I got it working though it does little. Here is the C# code.

    using System;
    using System.IO;
    using System.Runtime.InteropServices;
    using System.Runtime.InteropServices.ComTypes;

    namespace MonikerParseDisplayName
    {
        // https://www.microsoft.com/msj/1199/wicked/wicked1199.aspx

        // In Project Properties->Build->Check 'Interop for COM'
        // In AssemblyInfo.cs [assembly: ComVisible(true)]
        // compile with /unsafe 
        // In Project Properties->Build->Check 'Allow unsafe code'

        public static class Win32PInvoke
        {
            [DllImport("ole32.dll")]
            public static extern int CreateClassMoniker([In] ref Guid rclsid, 
                                                    out IMoniker ppmk);
        }

        [Guid("0FD50B85-CE66-47E2-9C71-2E780EBB8D54")]
        public interface ICalculator
        {
            double add(double a, double b);
            double mult(double a, double b);
        }

        [ComImport]
        [System.Security.SuppressUnmanagedCodeSecurity]
        [Guid("0000011a-0000-0000-C000-000000000046")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        internal interface IParseDisplayName
        {
            void ParseDisplayName(IBindCtx pbc,
                [MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName,
                IntPtr pchEaten, IntPtr ppmkOut);
            //void ParseDisplayName(IBindCtx pbc,
            //    [MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName,
            //    out int  pchEaten, out IMoniker  ppmkOut);
        }

        [Guid("30194303-435D-44E1-9FB2-A625CEDB8B68")]
        [ClassInterface(ClassInterfaceType.None)]
        [ComDefaultInterface(typeof(ICalculator))]
        public class Calculator : ICalculator, IParseDisplayName,
            IMoniker
        {
            [ComVisible(true)]
            [ComRegisterFunction()]
            public static void DllRegisterServer(string sKey)
            {

                Microsoft.Win32.RegistryKey key;
                key = Microsoft.Win32.Registry.ClassesRoot.CreateSubKey 
                     ("SimonsCalc");
                key.SetValue("", 
                   "Simon's experiment with GetObject(\"SimonsCalc:adder\")");
                Microsoft.Win32.RegistryKey subkey;
                subkey = key.CreateSubKey("Clsid");
                subkey.SetValue("", "{30194303-435D-44E1-9FB2-A625CEDB8B68}");
                subkey.Close();
                key.Close();

            }
            [ComVisible(true)]
            [ComUnregisterFunction()]
            public static void DllUnregisterServer(string sKey)
            {

                try
                {
                    Microsoft.Win32.Registry.ClassesRoot.DeleteSubKeyTree(
                           "SimonsCalc");
                }
                catch (Exception)
                {}
            }

            public static void Log(string logMessage, TextWriter w)
            {
                w.Write("\r\nLog Entry : ");
                w.WriteLine("{0} {1}", DateTime.Now.ToLongTimeString(),
                    DateTime.Now.ToLongDateString());
                w.WriteLine("  :");
                w.WriteLine("  :{0}", logMessage);
                w.WriteLine("-------------------------------");
            }

            public double add(double a, double b)
            {
                return a + b;
            }
            public double mult(double a, double b)
            {
                return a * b;
            }


            void IParseDisplayName.ParseDisplayName(IBindCtx pbc, 
                string pszDisplayName, IntPtr pchEaten, IntPtr ppmkOut)
            {
                using (StreamWriter w = File.AppendText("n:\\log.txt"))
                {
                    IMoniker mon=null;
                    int retVal=0;

                    try { 
                        //consume the whole lot
                        System.Runtime.InteropServices.Marshal.WriteInt32(pchEaten, 
                               pszDisplayName.Length);
                        
                    }
                    catch (Exception ex)
                    {
                        Log("Unsuccessful attempt to populate pchEaten:" + 
                              ex.Message , w);
                    }

                    try
                    {
                        Guid rclsid = new 
                              Guid("30194303-435D-44E1-9FB2-A625CEDB8B68");
                        retVal = Win32PInvoke.CreateClassMoniker(ref rclsid, 
                              out mon );
                    }
                    catch (Exception ex)
                    {
                        Log("Unsuccessful attempt to call CreateClassMoniker:" + 
                               ex.Message, w);
                    }
                    const int S_OK = 0;
                    if (retVal==S_OK)
                    {
                        //unsafe { 
                        //void* pvMon = (void*)mon;
                        //ppmkOut = new IntPtr(pvMon);
                        //} [StructLayout(LayoutKind.Explicit)]
                        try
                        {
                            //ppmkOut=mon;
                            //Marshal.StructureToPtr(mon, ppmkOut, true);
                            //https://searchcode.com/file/115732569/mcs/ ...
                            // ...class/referencesource/System.ServiceModel/ ...
                            // ...System/ServiceModel/ComIntegration/ ...
                            // ...ServiceMoniker.cs#l-159
                            
                            IntPtr ppv = InterfaceHelper.GetInterfacePtrForObject(
                                typeof(IMoniker).GUID, this);

                            System.Runtime.InteropServices.Marshal.WriteIntPtr(
                                  ppmkOut, ppv);
                            
                        }
                        catch (Exception ex)
                        {
                            Log("Unsuccessful attempt to " +
                                "call Marshal.StructureToPtr:" 
                                + ex.Message, w);
                        }
                    }
                }
            }


            void IMoniker.BindToObject(IBindCtx pbc, IMoniker pmkToLeft, 
                ref Guid riidResult, out object ppvResult)
            {
                ppvResult = this;
                //throw new NotImplementedException();
            }

            void IMoniker.BindToStorage(IBindCtx pbc, IMoniker pmkToLeft, 
                ref Guid riid, out object ppvObj)
            {
                throw new NotImplementedException();
            }

            void IMoniker.CommonPrefixWith(IMoniker pmkOther, 
                out IMoniker ppmkPrefix)
            {
                throw new NotImplementedException();
            }

            void IMoniker.ComposeWith(IMoniker pmkRight, bool fOnlyIfNotGeneric, 
                out IMoniker ppmkComposite)
            {
                throw new NotImplementedException();
            }

            void IMoniker.Enum(bool fForward, out IEnumMoniker ppenumMoniker)
            {
                throw new NotImplementedException();
            }

            void IMoniker.GetClassID(out Guid pClassID)
            {
                throw new NotImplementedException();
            }

            void IMoniker.GetDisplayName(IBindCtx pbc, IMoniker pmkToLeft, 
                out string ppszDisplayName)
            {
                throw new NotImplementedException();
            }

            void IMoniker.GetSizeMax(out long pcbSize)
            {
                throw new NotImplementedException();
            }

            void IMoniker.GetTimeOfLastChange(IBindCtx pbc, IMoniker pmkToLeft, 
                out System.Runtime.InteropServices.ComTypes.FILETIME pFileTime)
            {
                throw new NotImplementedException();
            }

            void IMoniker.Hash(out int pdwHash)
            {
                throw new NotImplementedException();
            }

            void IMoniker.Inverse(out IMoniker ppmk)
            {
                throw new NotImplementedException();
            }

            int IMoniker.IsDirty()
            {
                throw new NotImplementedException();
            }

            int IMoniker.IsEqual(IMoniker pmkOtherMoniker)
            {
                throw new NotImplementedException();
            }

            int IMoniker.IsRunning(IBindCtx pbc, IMoniker pmkToLeft, 
                                                 IMoniker pmkNewlyRunning)
            {
                throw new NotImplementedException();
            }

            int IMoniker.IsSystemMoniker(out int pdwMksys)
            {
                throw new NotImplementedException();
            }

            void IMoniker.Load(IStream pStm)
            {
                throw new NotImplementedException();
            }

            void IMoniker.ParseDisplayName(IBindCtx pbc, IMoniker pmkToLeft, 
                    string pszDisplayName, out int pchEaten, out IMoniker ppmkOut)
            {
                throw new NotImplementedException();
            }

            void IMoniker.Reduce(IBindCtx pbc, int dwReduceHowFar, 
                          ref IMoniker ppmkToLeft, out IMoniker ppmkReduced)
            {
                throw new NotImplementedException();
            }

            void IMoniker.RelativePathTo(IMoniker pmkOther, 
                                      out IMoniker ppmkRelPath)
            {
                throw new NotImplementedException();
            }

            void IMoniker.Save(IStream pStm, bool fClearDirty)
            {
                throw new NotImplementedException();
            }
        }

        internal static class InterfaceHelper
        {
            // only use this helper to get interfaces that 
            // are guaranteed to be supported
            internal static IntPtr GetInterfacePtrForObject(Guid iid, object obj)
            {
                IntPtr pUnk = Marshal.GetIUnknownForObject(obj);
                if (IntPtr.Zero == pUnk)
                {
                    //throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(
                    //new ArgumentException(SR.GetString(SR.UnableToRetrievepUnk)));
                }

                IntPtr ppv = IntPtr.Zero;
                int hr = Marshal.QueryInterface(pUnk, ref iid, out ppv);

                Marshal.Release(pUnk);

                if (hr != 0)
                {
                    throw new Exception("QueryInterface should succeed");
                }

                return ppv;
            }
        }    
    }

A shout out has to be made to a code search resoure called searchcode.com which helped me find sample fragments simply not anywhere on StackOverflow or on official Microsoft documentation.

On reflection there is nothing in this custom activation syntax that cannot be shipped in a method call after a COM server has been instantiated by New or CreateObject or the C# equivalents. If the COM server is remote then I suppose one saves a network round trip. LDAP would be a remote call in an enterprise. WMI could query remote resources. WCF is a remote-ing technology. So you can see the use cases. For same machine calls custom activation syntax's absence is no great loss.

Embed Html page as resource in C# Winforms

So in previous posts I have shown a Winforms WebBrowser control navigating to a local html file.  It is possible to set the WebBrowser's DocumentStream property to a System.IO.Stream.  With the following code it is possible to create a System.IO.Stream from a string


        public static System.IO.Stream GenerateStreamFromString(string s)
        {
            MemoryStream stream = new MemoryStream();
            StreamWriter writer = 
                  new StreamWriter(stream,System.Text.Encoding.Unicode);
            writer.Write(s);
            writer.Flush();
            stream.Position = 0;
            return stream;
        }


The string would be an Html string but we want to avoid files so it looks like we can add the file as an embedded resource.

I named mine HTMLPage1 and then it can be referred to in code with


            string myFile2 = WebBrowserInWinForms.Properties.Resources.HTMLPage1;

            System.IO.Stream s = GenerateStreamFromString(myFile2);


But beware, the document needs to complete loading and whilst that is happening the stream needs to kept alive so no early disposal. To solve define stream variable at class level and dispose later. So the code becomes

using System;
using System.Windows.Forms;
using System.IO;

namespace WebBrowserInWinForms
{
    public partial class Form1 : Form
    {
        Stream s = null;

        public Form1()
        {
            
            InitializeComponent();

            string myFile2 = 
               WebBrowserInWinForms.Properties.Resources.HTMLPage1;

            this.webBrowser1.DocumentCompleted += webBrowser1_DocumentCompleted;
            s = GenerateStreamFromString(myFile2);
            this.webBrowser1.DocumentStream = s;

        }

        private void webBrowser1_DocumentCompleted(object sender, 
                                   WebBrowserDocumentCompletedEventArgs e)
        {
            s.Dispose(); // finished with this now
            System.Windows.Forms.HtmlElement clickMe = 
                              this.webBrowser1.Document.Body.All["clickMe"];
            clickMe.Click += clickMe_Click;
        }

        public static Stream GenerateStreamFromString(string s)
        {
            MemoryStream stream = new MemoryStream();
            StreamWriter writer = 
                      new StreamWriter(stream,System.Text.Encoding.Unicode);
            writer.Write(s);
            writer.Flush();
            stream.Position = 0;
            return stream;
        }
        
        void clickMe_Click(object sender, HtmlElementEventArgs e)
        {
            //...
        }
    }
}

Copying the HTML string into the resource editor is clunky, the resource string editor is probably just for words and phrases when one ships an edition in a foreign language. It would be nice if in Debug environment we could use the file navigate method and then for Release we'd do a custom build step and pack the files into the embedded resources with code. We'd need conditional compilation.

The embedded resources can be found in <$SolutionDirector$>/Properties/Resources.resx which is Xml so they are entitised, this looks possible for one day.

Creating a simple Winforms and WebBrowser application to shows disk drives

This article has been migrated to our sister C# blog.

Loading files relative to an assembly

So continuing on the theme of using .NET's WebBrowser control ... we need to load our first page. If we add HTML file to our solution then we can use Reflection to find where our assembly is executing and go up a couple of directories to find the HTML file and navigate to it.


    Assembly myExe = System.Reflection.Assembly.GetExecutingAssembly();
    string filePath = Path.GetDirectoryName(new Uri(myExe.CodeBase).LocalPath);
    string myFile = System.IO.Path.Combine(filePath, @"..\..\HtmlPage1.html");


    if (File.Exists(myFile))
    {
        this.webBrowser1.DocumentCompleted += webBrowser1_DocumentCompleted;
        this.webBrowser1.Navigate(myFile);
    }


An alternative to using the Navigate method is to open a file IO Stream and then set this as the WebBrowser control's DocumentStream property like this ...


    Assembly myExe = System.Reflection.Assembly.GetExecutingAssembly();
    string filePath = Path.GetDirectoryName(new Uri(myExe.CodeBase).LocalPath);
    string myFile = System.IO.Path.Combine(filePath, @"..\..\HtmlPage1.html");


    if (File.Exists(myFile))
    {
        this.webBrowser1.DocumentCompleted += webBrowser1_DocumentCompleted;

        System.IO.Stream str = System.IO.File.Open(myFile, FileMode.Open);

        //this.webBrowser1.Navigate(myFile);
        this.webBrowser1.DocumentStream = str;

    }


Also note is both examples that you'd best let the document completely load before doing any processing, even if it is a local file, we use event handling and capture the DocumentCompleted event.

.NET's WebBrowser control is surprisingly configurable in the registry

So currently experimenting with .NET's WebBrowser control to see if we can build user interfaces in HTML and skip Winforms, WPF, Silverlight, ActiveX and any other proprietary GUI technology that Microsoft introduces and then withdraws at a later date.  Seriously, keep your GUI standards open is the advice.

So if we add a WebBrowser control to a WinForm then it emulates Internet Explorer, but which version, the answer is its configurable.  Here is a good link for the Browser Emulation setting and on the same page are other settings. It is configurable in the registry by writing the name of your executable/assembly as the value name.

If you perhaps might change the name of your executable then you'd might like some code to write the code automatically, here is it.  So it uses Reflection to get the executable name and then writes to Registry to say use IE11 for WebBrowser control instances.

    // Make WebBrowser control emulate IE11
    RegistryKey keyIeFeatureControl = Registry.LocalMachine.OpenSubKey(
            @"Software\Microsoft\Internet Explorer\Main\FeatureControl");

    RegistryKey keyIeFeatureBrowserEmulation = keyIeFeatureControl.
            OpenSubKey("FEATURE_BROWSER_EMULATION", true);

    string exeName=Assembly.GetExecutingAssembly().GetName().Name;
    keyIeFeatureBrowserEmulation.SetValue(
            exeName, 69632); //69532=&h11000

    keyIeFeatureBrowserEmulation.Close();
    keyIeFeatureControl.Close();


Your code will need admin rights or some sort of elevated privileges to run this code otherwise you will get a security exception.

Saturday, 25 March 2017

"Interface marked as restricted" compile error prevents QueryInterface

In Com the method IUnknown.QueryInterface allows a client to hop between interfaces, this is callable via C++ but not VBA.  VBA has a different mechanism, one uses Dim itfFoo as IKung to declare the interface and then a call to Set itfFoo = objBar, where objBar is a Com object that implements the interface IKung.

However, this is also dependent of being able to write the Dim statement as such.  Not all interfaces play ball, they have types which cannot be represented in VBA and so the compiler chokes.  Following is an example.  When this happens you need to turn to C++.


Option Explicit
Option Private Module

Private Sub Test()


'**********************************************************************************
'* The following works and demonstrates a successful QueryInterface
'**********************************************************************************

    Dim obj As Object 'This is VBA way of asking of declaring IDispatch
    Set obj = VBA.CreateObject("Scripting.Dictionary")


    Dim itfUnk As stdole.IUnknown   'The canonical COM interface
    Set itfUnk = obj   '--- this does a QueryInterface , it asks for IUnknown

'**********************************************************************************
'* The following fails with compile errors, try uncommenting to see
'**********************************************************************************

    '* Compile error:
    '*
    '* Function or interface marked as restricted, or the function uses an
    '* Automation type not supported in Visual Basic


    'Dim itfDisp As stdole.IDispatch
    'Set itfDisp = obj

'******************************************************************************
    
    '* Compile error: User-defined type not defined
    '* Needs a Tools->Reference but where is IParseDisplayName defined?
    '* shame vba cannot declare interfaces by IID like C++
    
    'Dim itfUnk As IParseDisplayName
    'Set itfUnk = obj

'******************************************************************************

End Sub


Ideone.com: Online C++ Editor

Following on from a post about JSFiddle I have chanced across an online C++ editor, http://ideone.com.

I have written a quick piece of code in C++14 which iterates through some strings and reads them into pairs. The code uses modern C++ (C++11/C++14) structures such as: only standard library strings, the auto keyword to simplify variable declarations, use of vector as the canonical container, using iterators begin() and end().
However, like JSFiddle questions over intellectual property rights have to be asked. Also, code needs input and whilst there is space for input one cannot upload files etc.