Tuesday, 4 April 2017

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.

Use JSFiddle to share Javascript problems

So online compilation is improving, during my travels I have seen online Javascript editing environments, for example there is JSFiddle (screenshot below).  Here is a specific Angular.js example

One can see the page is divided into 4 quadrants, HTML, CSS, JAVASCRIPT and resulting output. One can play aropund and experiment and if you get stuck then you can use it as a link in a StackOverflow question and gets some help. A truly useful online collaboration tool. However, questions could be asked about the intellectual property rights so I would advise limiting to learning concepts.

Friday, 24 March 2017

Code to Kill Support Scam Web Page

"Critical Alert from Microsoft" virus is a support scam and it can be very frustrating, one can try to close the comment window and then get to the close button but the virus seems to have some fiendish Javascript that prevents keyboard action. The Task Manager isn't very good in these situations, I have often tried to kill the specific Chrome.exe process but frequently all of them are dropped.

Update

Best way to handle to kill chrome is to use from the command line
taskkill.exe /im Chrome.exe /f
It is the /f flag which forces a termination.