Passing C++ Object in ATL DLL

Environment: ATL, COM, MFC

Introduction

A few weeks ago, I was desperately looking for a sample in which a C++ object can be sent across a COM interface but could not get any sample for the same. That's why I decided to post this article here.

Passing a C++ object across an ATL DLL is not difficult, but, of course, it is a bit tricky and interesting too.

Before starting the project, make sure that your client and server are both be C++-compliant. Secondly, you must be aware of setting your COM client and server.

Limitations with the Interface

COM demands a high degree of separation between the client and server; this is done through interfaces but the problem is that interfaces offer only a limited number of data types in methods. If the interface is IDispatch-based, the choices are even more limited. Keeping these limitations in mind, C++ objects only can be passed across the interface under these situations:

  1. Both Client and Server are written in VC++.
  2. They must be able to share object definitions (such as header files).
  3. Passing objects simplifies the application design.
  4. Your application may need to run in a distributed environment. You want COM's capabilities of remote activation, local/remote transparency, and security.

I would recommend that, before starting your work, you should also refresh the Serialization topic first.

Now, let's continue with the sample and do the following:

  1. Create an ATL DLL server.
  2. Add MFC class derived from CObject.
  3. Use DECLARE_SERIAL macro in class header.
  4. Use IMPLEMENT_SERIAL macro in class body.
  5. Override the Serialize() method.
    // Your CSimpleObj class looks like this:
    class CSimpleObj : public CObject
    {
      DECLARE_SERIAL( CSimpleObj )
    public:
      // constructor and destructor
      CSimpleObj();
      virtual ~CSimpleObj();
      // Set internal string data
      void SetString( CString csData );
      // Used to serialize data into an archive
      virtual void Serialize(CArchive& ar);
      // Display the data string
      void Show();
    private:
      CString m_strData;// Internal data string
    };
    
    // Write this object to an archive
    void CSimpleObj::Serialize(CArchive& ar)
    {
      CObject::Serialize( ar );
    
      if (ar.IsLoading())
      {
      // extract data from archive
        ar 
    >> m_strData;
      }
      else
      {
        // store data into archive
        ar << m_strData;
      }
    }
    
    // Method to display data in this object
    void CSimpleObj::Show()
    {
      AfxMessageBox(m_strData);
    }
    
    // save a string in data member
    void CSimpleObj::SetString(CString csData)
    {
      m_strData = csData;
    }
    
  6. Now, the next step is Serializing and De-Serializing (Loading and Storing Objects) with a CArchive. I'm using a different class called CBlob for it.
    class CBlob
    {
    public:
      CBlob() {};
      virtual ~CBlob() {};
    
      // Extract data from a CObject and load it into a SAFEARRAY.
      SAFEARRAY* Load( CObject *pObj );
      // Re-create an object from a SAFEARRAY
      BOOL Expand( CObject * &pObj, SAFEARRAY *pVar );
    private:
    };
    
    // Extract data from a CObject and use it to create a SAFEARRAY.
    SAFEARRAY* CBlob::Load( CObject *pObj)
    {
      CMemFile memfile;      // memory file
    
      // define the flag that tells the archive whether it should 
      // load or store
      long lMode = CArchive::store | CArchive::bNoFlushOnDelete;
      // create the archive using the memory file
      CArchive ar(&memfile, lMode );
    
      // m_pDocument is not used
      ar.m_pDocument = NULL;
      // serialize the object into the archive
      ar.WriteObject(pObj);
      // close the archive -- the data is now stored in memfile
      ar.Close();
    
      // get the length (in bytes) of the memory file
      long llen = memfile.GetLength();
    
      // detach the buffer and close the file
      unsigned char *pMemData = memfile.Detach();
    
      // set up safearray
      SAFEARRAY *psa;
    
      // create a safe array to store the stream data
      psa = SafeArrayCreateVector( VT_UI1, 0, llen );
    
      // pointers to byte arrays
      unsigned char *pData = NULL;
    
      // get a pointer to the safe array. Locks the array.
      SafeArrayAccessData( psa, (void**)&pData );
    
      // copy the memory file into the safearray
      memcpy( pData, pMemData, llen );
    
      // clean up buffer
      delete pMemData;
    
      // unlock access to safearray
      SafeArrayUnaccessData(psa);
    
      // return a pointer to a SAFEARRAY allocated here
      return psa;
    }
    
    // Re-create an object from a SAFEARRAY
    BOOL CBlob::Expand(CObject * &rpObj, SAFEARRAY *psa)
    {
      CMemFile memfile;          // memory file for de-serailze
      long lLength;              // number of bytes
      char *pBuffer;             // buffer pointer
    
      // lock access to array data
      SafeArrayAccessData( psa, (void**)&pBuffer );
    
      // get number of elements in array. This is the number of bytes
      lLength = psa->rgsabound->cElements;
    
      // attach the buffer to the memory file
      memfile.Attach((unsigned char*)pBuffer, lLength);
    
      // start at beginning of buffer
      memfile.SeekToBegin();
    
      // create an archive with the attached memory file
      CArchive ar(&memfile, CArchive::load | CArchive::bNoFlushOnDelete);
    
      // document pointer is not used
      ar.m_pDocument = NULL;
    
      // inflate the object and get the pointer
      rpObj = ar.ReadObject(0);
    
      // close the archive
      ar.Close();
    
      // Note: pBuffer is freed when the SAFEARRAY is destroyed
      // Detach the buffer and close the file
      pBuffer = (char*) memfile.Detach();
    
      // release the safearray buffer
      SafeArrayUnaccessData( psa );
    
      return TRUE;
    }
    

I'm using SAFEARRAY here because this is the best selection for our use. It can contain some complex multidimensional arrays, but for this example we are only using a very simple array. There's one problem with SAFEARRAY data: MIDL doesn't recognize this data type, but the easiest way is to use the VARIANT type that I will discuss in next article.

The next steps are as follows:

  1. Create a COM interface.
  2. Create a SAFEARRAY object.
  3. Define [helpstring("method SetArray")] HRESULT SetArray([in]SAFEARRAY (unsigned char) pData);[helpstring("method GetArray")] HRESULT GetArray([out/*,retval*/]SAFEARRAY(unsigned char) *pData); in an IDL file.
  4. Make a MFC-based client to test the application.

Your IDL file should look like this:

  interface IBolbData : IUnknown
  {
    [helpstring("method SetArray")] HRESULT SetArray([in]SAFEARRAY
      (unsigned char) pData);
    [helpstring("method GetArray")] HRESULT GetArray([out/*,retval*/]
      SAFEARRAY(unsigned char) *pData);
  };

// Sets object.
STDMETHODIMP CBolbData::SetArray(SAFEARRAY *pData)
{
  AFX_MANAGE_STATE(AfxGetStaticModuleState())

  // create a dummy pointer of CSimpleObj
  CSimpleObj *dummy=NULL; 
  // create blob obect to expand/deserialize
  CBlob blob;
  // Init dummy object using safe array through this function
  blob.Expand( (CObject*&)dummy, pData );
  dummy->Show();  // Call show function to test the object.
  delete dummy;      // Delete the pointer.

  return S_OK;
}

// Creates Object and sends to client.
STDMETHODIMP CBolbData::GetArray(SAFEARRAY **pData)
{
  AFX_MANAGE_STATE(AfxGetStaticModuleState())
  // create object to send to server
  CSimpleObj *pMyOb = new CSimpleObj();

  // set the string data
  pMyOb->SetString( "A SAFEARRAY from the server!" );

  // create blob to serialize object
  CBlob blob;

  // load the object into the blob
  *pData = blob.Load( pMyOb );
  // delete the pMyOb pointer
  delete pMyOb;

  return S_OK;
}

And, finally, write a dialog-based MFC application having two buttons and add the following code:

void CClientDlg::OnOK()
{
// create COM smart pointer from CLSID string
  try
  {
    IBolbDataPtr pI( "Server.BolbData.1" );
    SAFEARRAY *psa ;

    // Get the safearray from the server
    pI->GetArray( &psa );

    // create a pointer to an object
    CSimpleObj *dummy=NULL;

    // blob object to expand
    CBlob blob;

    // use the blob to expand the safearray into an object
    blob.Expand( (CObject *&)dummy, psa );
    // call a method on the object to test it
    dummy->Show();

    // delete the object
    delete dummy;
  }
  // Handle any COM exceptions from smart pointers
  catch (_com_error e)
  {
    // display the message string from the error
    AfxMessageBox( e.ErrorMessage() );
  }
}

void CClientDlg::OnLoad()
{
  try
  {
    // create smart pointer from CLSID string
    IBolbDataPtr pI( "Server.BolbData.1" );
    SAFEARRAY *psa ;

    // create object to send to server
    CSimpleObj *pMyOb = new CSimpleObj();

    // set the string data
    pMyOb->SetString( "The client sent a SAFEARRAY!" );

    // create blob to serialize object
    CBlob blob;

    // load the object into the blob
    psa = blob.Load( pMyOb );

    // delete the object
    delete pMyOb;

    pI->SetArray( psa );

  }
  // Handle any COM exceptions from smart pointers
  catch (_com_error e)
  {
    // display the message string from the error
    AfxMessageBox( e.ErrorMessage() );
  }
}

Conclusion

This article covers a number of topics; for example, how to use Serialization, how to use SAFEARRAY, and how to pass a C++ object across the interface. I also would like to say thanks to William Rubin, whose article helped me quite a lot. I was planning to explain this topic in a bit more detail but due to shortage of time I couldn't do so but will keep updating the document from time to time. In the meantime, please feel free to contact me.

Downloads

Download source files - 116 Kb