none
Moving COM class from in-process to local server RRS feed

  • Question

  • All of my experience with COM is in C/C++, so I'm hoping there's an easy answer I'm missing due to lack of familiarity with .NET.

    I have to move a COM class and interface, written in C#, from a DLL activated as in-process to an EXE as a multi-use local server. Most of the process was fairly easy. I just moved the source files from the DLL project to the EXE project, added some code to register the class as LocalServer and ran regasm against the EXE.

    Now, from my COM client, when I instantiate the COM object, the EXE starts and the object gets created. I know this because I see a dialog box presented by the COM server code. But when I cast the object into an interface and try to call its methods, it blows up:

    Unable to cast COM object of type '~~~' to interface type '~~~'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{~~~}' failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).

    (Actual names removed) Let me reiterate that I took a working in-process COM class and interface and just moved it to an EXE server. So all the ComVisible and Guid stuff is still there. I should also mention that regasm does not seem to be writing anything about the interface to the registry since my change.

    Am I missing a step here, or can anyone tell me what I'm doing wrong? Thanks in advance.

    Wednesday, May 2, 2012 1:11 AM

Answers

  • pc-adam wrote:

    Since registering the interface, I'm able to call methods that take no parameters. Now I need some help passing parameters. Here is the client code:

            PWCHAR wcVUuidParent = m_pPidlMgr->GetVaultUUID(m_pidl);
    
            SAFEARRAYBOUND parDirBounds;
            parDirBounds.cElements = 1;
            parDirBounds.lLbound = 0;
            LPSAFEARRAY psaParDirTokens = SafeArrayCreate(VT_BSTR, 1, &parDirBounds);
    
            SafeArrayLock(psaParDirTokens);
            BSTR *parDirTokenArray = (BSTR *)psaParDirTokens->pvData;
            parDirTokenArray[0] = m_pPidlMgr->GetParDirToken(m_pidl);
            SafeArrayUnlock(psaParDirTokens);
    
            SAFEARRAYBOUND tokenBounds;
            tokenBounds.cElements = 1;
            tokenBounds.lLbound = 0;
            LPSAFEARRAY psaTokens = SafeArrayCreate(VT_BSTR, 1, &tokenBounds);
    
            SafeArrayLock(psaTokens);
            BSTR *tokenArray = (BSTR *)psaTokens->pvData;
            tokenArray[0] = m_pPidlMgr->GetToken(m_pidl);
            SafeArrayUnlock(psaTokens);
    
            HRESULT hr;
            hr = pIInteropCloud->IDeleteFiles(wcVUuidParent, psaParDirTokens,
    psaTokens); 

    That last line calls the auto-generated inline function:

    inline HRESULT IInteropCloud::IDeleteFiles ( _bstr_t vaultUuid, SAFEARRAY *
    directoryTokens, SAFEARRAY * fileTokens ) {         HRESULT _hr =
    raw_IDeleteFiles(vaultUuid, directoryTokens, fileTokens);         if
    (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));         return _hr;
    }

    And in the COM server (local out-of-process) it should hit this method:

                 public void IDeleteFiles(string vaultUuid, string[] directoryToken,
    string[] fileTokens)                 {
                            ...
                    }

    But the client side fails with E_OUTOFMEMORY. I suspect it has to do with those arrays not getting marshaled correctly across the process boundary. Is there anything special I need to do to handle those parameters?

    Safearrays should be marshaled well by the default marshaler.

    The outofmemory error looks like a bad pointer and the marshaler believes it should allocate more memory than the real requested. This can happen if the dword preceding the string in the BSTR is bad.

    Your code looks good to me, so take a look to:
    - the return values from GetParDirToken and GetToken and check if they are really good bstrs
    - take a look at the memory of the final safearrays and check it by hand in the debugger.


    Raffaele Rialdi  http://www.iamraf.net
    Weblog: http://blogs.ugidotnet.org/raffaele
    Microsoft MVP profile https://mvp.support.microsoft.com/profile/raffaele
    UGIdotNET - http://www.ugidotnet.org/


    Raffaele Rialdi [MVP] My articles and videos: http://www.iamraf.net Italian blog: http://blogs.ugidotnet.org/raffaele
    • Marked as answer by pc-adam Friday, May 4, 2012 12:48 AM
    Thursday, May 3, 2012 1:26 PM

All replies

  • Hello pc-adam,

    >> I have to move a COM class and interface, written in C#, from a DLL activated as in-process to an EXE as a multi-use local server. Most of the process was fairly easy. I just moved the source files from the DLL project to the EXE project, added some code to register the class as LocalServer and ran regasm against the EXE...

    1. Note that the .NET framework does not support EXE-based COM local server.

    2. You may expose COM-visible C# classes from an EXE assembly. However, when they are instantiated in an unmanaged application, the EXE assembly is not run as a separate process. It is also loaded into the memory space of the unmanaged application (as if it is another DLL).

    3. Hence if you have indeed succeeded in making your EXE assembly run as a seperate process, my question is : what specific code did you add to make your assembly run as a LocalServer ?

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    Wednesday, May 2, 2012 3:03 AM
  • Errors starting with 0x8 are permission error or invalid pointers.  I suspect yoiur problem is due to the variables you are passing to the DLL are in protected memory space and creating an error, or you aren't passing the correct type (8, 16, 32, 64 bit) variables to the DLL. Any variables that are points must be declared as "static" so they are located in memory space that the dll can accept.

    Below is code that I use to Get the ARP table using the IpHlpApi.dll that shows the proper way of calling a dll and allocting memroy for the dll.

           // Declare the GetIpNetTable function.
            [DllImport("IpHlpApi.dll")]
            [return: MarshalAs(UnmanagedType.U4)]
            static extern int GetIpNetTable(
               IntPtr pIpNetTable,
               [MarshalAs(UnmanagedType.U4)] 
             ref int pdwSize,
               bool bOrder);
            // The insufficient buffer error. 
            const int ERROR_INSUFFICIENT_BUFFER = 122;
            static IntPtr buffer;
            static int result;
            public GetArpTable()
            {
                // The number of bytes needed. 
                int bytesNeeded = 0;
                // The result from the API call. 
                result = GetIpNetTable(IntPtr.Zero, ref bytesNeeded, false);
                // Call the function, expecting an insufficient buffer. 
                if (result != ERROR_INSUFFICIENT_BUFFER)
                {
                    // Throw an exception. 
                    throw new Win32Exception(result);
                }
                // Allocate the memory, do it in a try/finally block, to ensure 
                // that it is released. 
                buffer = IntPtr.Zero;
                // Try/finally. 
                try
                {
                    // Allocate the memory. 
                    buffer = Marshal.AllocCoTaskMem(bytesNeeded);
                    // Make the call again. If it did not succeed, then 
                    // raise an error. 
                    result = GetIpNetTable(buffer, ref bytesNeeded, false);
                    // If the result is not 0 (no error), then throw an exception. 
                    if (result != 0)
                    {
                        // Throw an exception. 
                        throw new Win32Exception(result);
                    }
                }
                finally
                {
                   
                }
              }


    jdweng

    Wednesday, May 2, 2012 6:01 AM
  • pc-adam wrote:

    All of my experience with COM is in C/C++, so I'm hoping there's an easy answer I'm missing due to lack of familiarity with .NET.

    I have to move a COM class and interface, written in C#, from a DLL activated as in-process to an EXE as a multi-use local server. Most of the process was fairly easy. I just moved the source files from the DLL project to the EXE project, added some code to register the class as LocalServer and ran regasm against the EXE.

    Did you try simply moving the COM object in a COM+ application inside the component services?
    It's very easy:
    - register the com component dll as it was in-process
    - open component services mmc
    - create a new app and add your component inside

    From the client side there are no differences, same tlb/tlbimp, same code.
    The component will be hosted in dllhost.exe surrogate process and activated as required. (the host name is written in the registry)

    Few notes:
    - out-of-process require your component use regular types or providing a custom marshaler for your custom types
    - you can configure many new options such as object pooling (require your object to be stateless)
    -


    Raffaele Rialdi  http://www.iamraf.net
    Weblog: http://blogs.ugidotnet.org/raffaele
    Microsoft MVP profile https://mvp.support.microsoft.com/profile/raffaele
    UGIdotNET - http://www.ugidotnet.org/


    Raffaele Rialdi [MVP] My articles and videos: http://www.iamraf.net Italian blog: http://blogs.ugidotnet.org/raffaele
    Wednesday, May 2, 2012 6:39 AM
  • I followed the framework in this Microsoft example: http://code.msdn.microsoft.com/windowsdesktop/CSExeCOMServer-f2a59967

    The EXE activation seems to work quite well. No matter how many COM clients I start, I always have 1 EXE running. It doesn't matter if I start it myself or let COM start it.

    Wednesday, May 2, 2012 12:41 PM
  • I'm afraid your suggestion to use a generic surrogate process will not work in my case. I'm moving the COM class into an existing EXE that has some existing functionality and a GUI.

    I think you might be on to something with the custom marshaling. My methods all return custom data types (structs which are also exposed to COM). Can you provide some more information on implementing custom marshaling in C#? Under what specific circumstances to I need a custom marshaler? 

    Thanks

    Wednesday, May 2, 2012 12:54 PM
  • Has anybody looked at the sample code I posted?

    jdweng

    Wednesday, May 2, 2012 1:05 PM
  • Thank you for the sample code, but I don't think that it's applicable in my situation. First, I'm invoking a .NET COM server from a .NET client. Your example looks like you're calling a native DLL from a .NET program. Second, I'm not passing any parameters.
    Wednesday, May 2, 2012 2:14 PM
  • pc-adam wrote:

    I'm afraid your suggestion to use a generic surrogate process will not work in my case. I'm moving the COM class into an existing EXE that has some existing functionality and a GUI.

    it's your choice, ok


    I think you might be on to something with the custom marshaling.

    Well, E_NOINTERFACE is a different problem. It's simply a queryinterface that cannot find the requested interface inside the object.
    I would check that you implemented correctly the interfaces required for that com object.
    Take a look at the links at the end of the post.
     > My methods

    all return custom data types (structs which are also exposed to COM). Can you provide some more information on implementing custom marshaling in C#? Under what specific circumstances to I need a custom marshaler? 

    You need a custom marshaler when you don't use standard COM types and these cannot be adequately described in IDL.
    For example if you have structs that are not described in the type library, the tlbimport don't know how to marshal those types across the boundaries.

    You don't know a custom marshaler when your object is used in-process and with the same apartment model. If your object declare to work in STA (single-threaded) and your client is winform (that is STA too) you are ok as all the calls are made by pointers (and are also much faster).
    Obviously you need to know how to handle those parameters.

    If your object is hosted in another process or simply another apartment, a proxy/stub pair is required in order to cross the boundaries. Here is the place where the custom marshaler take place for types that COM doesn't know.
     Here you can find a long explanations about the cases custom marshaler is needed and how can be developed in C# (and a bit of C++).
    http://support.microsoft.com/kb/307713
    http://bugslasher.net/2011/02/12/how-to-create-and-use-a-custom-com-marshaler-in-net/


    Raffaele Rialdi  http://www.iamraf.net
    Weblog: http://blogs.ugidotnet.org/raffaele
    Microsoft MVP profile https://mvp.support.microsoft.com/profile/raffaele
    UGIdotNET - http://www.ugidotnet.org/


    Raffaele Rialdi [MVP] My articles and videos: http://www.iamraf.net Italian blog: http://blogs.ugidotnet.org/raffaele
    Wednesday, May 2, 2012 2:24 PM
  • After a few more hours of playing with this, I've determined that the E_NOINTERFACE error was caused by the interface not being registered. Without going through the gory details of how I determined that, I manually created the registry entries that should have been there and things are working better now.

    I would have assumed that regasm took care of that. By what process should interfaces be registered?

    Wednesday, May 2, 2012 4:44 PM
  • Hello pc-adam,

    >> But when I cast the object into an interface and try to call its methods, it blows up: ... Unable to cast COM object of type '~~~' to interface type '~~~'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{~~~}' failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE))...

    1. Make sure that the interface (that the COM-visible C# class is supposed to implement) is declared COM-visible and has been given a GUID, e.g. :

    [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"), ComVisible(true)]
    public interface IInterface2
    {
        int SomeMethod();
    	...
    	...
    	...
    }
    

    2. Make sure that the COM-visible C# class inherits from the additional interface (and implements its methods of course), e.g. (using the SimpleObject class of the CSExeCOMServer project) :

    [ClassInterface(ClassInterfaceType.None)]           // No ClassInterface
    [ComSourceInterfaces(typeof(ISimpleObjectEvents))]
    [Guid(SimpleObject.ClassId), ComVisible(true)]
    public class SimpleObject : ReferenceCountedObject, ISimpleObject, IInterface2
    {
    	...
    	...
    	...
    }
    

    >> I should also mention that regasm does not seem to be writing anything about the interface to the registry since my change...

    3. This is OK and is expected. The ComRegisterFunctionAttribute'd function (i.e. Register()) need only update the CLSID key for the C# class.

    4. The information on the various COM-visible interfaces will be generated by REGASM.EXE into a type library (as long as the /tlb flag is set). REGASM.EXE will also register the type library and so information on the various COM-visible interfaces will be written into the registry including information on its associated type library.

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    Wednesday, May 2, 2012 4:44 PM
  • pc-adam wrote:

    After a few more hours of playing with this, I've determined that the E_NOINTERFACE error was caused by the interface not being registered. Without going through the gory details of how I determined that, I manually created the registry entries that should have been there and things are working better now.

    I would have assumed that regasm took care of that. By what process should interfaces be registered?

    regasm should do it
    http://msdn.microsoft.com/en-us/library/aa645712(v=vs.71).aspx

    http://msdn.microsoft.com/en-us/library/tzat5yw6(v=vs.100).aspx
     Please note that if you registered the dll and than you created the exe, you should update the registration settings in order to activate the exe server instead.


    Raffaele Rialdi  http://www.iamraf.net
    Weblog: http://blogs.ugidotnet.org/raffaele
    Microsoft MVP profile https://mvp.support.microsoft.com/profile/raffaele
    UGIdotNET - http://www.ugidotnet.org/


    Raffaele Rialdi [MVP] My articles and videos: http://www.iamraf.net Italian blog: http://blogs.ugidotnet.org/raffaele
    Wednesday, May 2, 2012 5:31 PM
  • Since registering the interface, I'm able to call methods that take no parameters. Now I need some help passing parameters. Here is the client code:

    	PWCHAR wcVUuidParent = m_pPidlMgr->GetVaultUUID(m_pidl);
    
    	SAFEARRAYBOUND parDirBounds;
    	parDirBounds.cElements = 1;
    	parDirBounds.lLbound = 0;
    	LPSAFEARRAY psaParDirTokens = SafeArrayCreate(VT_BSTR, 1, &parDirBounds);
    		
    	SafeArrayLock(psaParDirTokens);
    	BSTR *parDirTokenArray = (BSTR *)psaParDirTokens->pvData;
    	parDirTokenArray[0] = m_pPidlMgr->GetParDirToken(m_pidl);
    	SafeArrayUnlock(psaParDirTokens);
    	
    	SAFEARRAYBOUND tokenBounds;
    	tokenBounds.cElements = 1;
    	tokenBounds.lLbound = 0;
    	LPSAFEARRAY psaTokens = SafeArrayCreate(VT_BSTR, 1, &tokenBounds);
    		
    	SafeArrayLock(psaTokens);
    	BSTR *tokenArray = (BSTR *)psaTokens->pvData;
    	tokenArray[0] = m_pPidlMgr->GetToken(m_pidl);
    	SafeArrayUnlock(psaTokens);
    	
    	HRESULT hr;
    	hr = pIInteropCloud->IDeleteFiles(wcVUuidParent, psaParDirTokens, psaTokens);

    That last line calls the auto-generated inline function:

    inline HRESULT IInteropCloud::IDeleteFiles ( _bstr_t vaultUuid, SAFEARRAY * directoryTokens, SAFEARRAY * fileTokens ) {
        HRESULT _hr = raw_IDeleteFiles(vaultUuid, directoryTokens, fileTokens);
        if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
        return _hr;
    }

    And in the COM server (local out-of-process) it should hit this method:

           public void IDeleteFiles(string vaultUuid, string[] directoryToken, string[] fileTokens)
            {
                ...
            }

    But the client side fails with E_OUTOFMEMORY. I suspect it has to do with those arrays not getting marshaled correctly across the process boundary. Is there anything special I need to do to handle those parameters?

    Thursday, May 3, 2012 11:08 AM
  • pc-adam wrote:

    Since registering the interface, I'm able to call methods that take no parameters. Now I need some help passing parameters. Here is the client code:

            PWCHAR wcVUuidParent = m_pPidlMgr->GetVaultUUID(m_pidl);
    
            SAFEARRAYBOUND parDirBounds;
            parDirBounds.cElements = 1;
            parDirBounds.lLbound = 0;
            LPSAFEARRAY psaParDirTokens = SafeArrayCreate(VT_BSTR, 1, &parDirBounds);
    
            SafeArrayLock(psaParDirTokens);
            BSTR *parDirTokenArray = (BSTR *)psaParDirTokens->pvData;
            parDirTokenArray[0] = m_pPidlMgr->GetParDirToken(m_pidl);
            SafeArrayUnlock(psaParDirTokens);
    
            SAFEARRAYBOUND tokenBounds;
            tokenBounds.cElements = 1;
            tokenBounds.lLbound = 0;
            LPSAFEARRAY psaTokens = SafeArrayCreate(VT_BSTR, 1, &tokenBounds);
    
            SafeArrayLock(psaTokens);
            BSTR *tokenArray = (BSTR *)psaTokens->pvData;
            tokenArray[0] = m_pPidlMgr->GetToken(m_pidl);
            SafeArrayUnlock(psaTokens);
    
            HRESULT hr;
            hr = pIInteropCloud->IDeleteFiles(wcVUuidParent, psaParDirTokens,
    psaTokens); 

    That last line calls the auto-generated inline function:

    inline HRESULT IInteropCloud::IDeleteFiles ( _bstr_t vaultUuid, SAFEARRAY *
    directoryTokens, SAFEARRAY * fileTokens ) {         HRESULT _hr =
    raw_IDeleteFiles(vaultUuid, directoryTokens, fileTokens);         if
    (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));         return _hr;
    }

    And in the COM server (local out-of-process) it should hit this method:

                 public void IDeleteFiles(string vaultUuid, string[] directoryToken,
    string[] fileTokens)                 {
                            ...
                    }

    But the client side fails with E_OUTOFMEMORY. I suspect it has to do with those arrays not getting marshaled correctly across the process boundary. Is there anything special I need to do to handle those parameters?

    Safearrays should be marshaled well by the default marshaler.

    The outofmemory error looks like a bad pointer and the marshaler believes it should allocate more memory than the real requested. This can happen if the dword preceding the string in the BSTR is bad.

    Your code looks good to me, so take a look to:
    - the return values from GetParDirToken and GetToken and check if they are really good bstrs
    - take a look at the memory of the final safearrays and check it by hand in the debugger.


    Raffaele Rialdi  http://www.iamraf.net
    Weblog: http://blogs.ugidotnet.org/raffaele
    Microsoft MVP profile https://mvp.support.microsoft.com/profile/raffaele
    UGIdotNET - http://www.ugidotnet.org/


    Raffaele Rialdi [MVP] My articles and videos: http://www.iamraf.net Italian blog: http://blogs.ugidotnet.org/raffaele
    • Marked as answer by pc-adam Friday, May 4, 2012 12:48 AM
    Thursday, May 3, 2012 1:26 PM
  • After tracing back those BSTRs to their source, I found that they were really just LPWCHARs cast into BSTRs, so there was probably garbage in the memory that is supposed to hold the length. I got those allocated properly and everything works fine.

    Thanks for the help.

    Friday, May 4, 2012 12:48 AM
  • pc-adam wrote:

    After tracing back those BSTRs to their source, I found that they were really just LPWCHARs cast into BSTRs, so there was probably garbage in the memory that is supposed to hold the length. I got those allocated properly and everything works fine.

    Exactly what I was thinking about.
    BSTR is a strange beast where the pointer points to the beginning of the string but in the preceding DWORD there is the length of the string.
    As in memory you had garbage in the length location, the default marshaller tried to allocate a huge amount of memory resulting in an out-of-memory error.


    Thanks for the help.

    You're welcome!


    Raffaele Rialdi  http://www.iamraf.net
    Weblog: http://blogs.ugidotnet.org/raffaele
    Microsoft MVP profile https://mvp.support.microsoft.com/profile/raffaele
    UGIdotNET - http://www.ugidotnet.org/


    Raffaele Rialdi [MVP] My articles and videos: http://www.iamraf.net Italian blog: http://blogs.ugidotnet.org/raffaele
    Friday, May 4, 2012 6:59 AM