Wednesday, 21 February 2018

C# - 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.

No comments:

Post a Comment