How to serialize a COM object as a Base64 byte array

A component I’m working on needs to be able to serialize its data via JSON.  That’s fine for scalars, arrays and maps, but we also needed to be able to serialize COM objects (there’s a large legacy codebase underneath, much of which is deployed via COM).  It turns out ATL provides API functions for Base 64 encoding (which was a surprise to me and a number of my colleagues).  Here’s some code to make calling it easier (I’ve replaced the production code error handling with simple strings):

std::string to_base_64(int byte_length, const void* bytes  )
{
  if (bytes == 0)
    throw "ToBase64 called with zero bytes";
  
  ATL::CStringA base64;
  int base64_length = ATL::Base64EncodeGetRequiredLength(byte_length);

  if(!ATL::Base64Encode(static_cast<const BYTE*>(bytes), 
                        byte_length, 
                        base64.GetBufferSetLength(base64_length), 
                        &base64_length))
    throw "ATL::Base64Encode failed";

  base64.ReleaseBufferSetLength(base64_length);
  return static_cast<const char*>(CT2A(base64));
}

std::pair<size_t,std::unique_ptr<BYTE>> from_base_64( const std::string& encoded )
{
  // Predict length needed for decoding
  int decoded_length = Base64DecodeGetRequiredLength( encoded.size() );
  std::unique_ptr<BYTE> buffer( new BYTE[decoded_length] );

  // Actual decoded-length may be different to that predicted - method overwrites the value
  if (!Base64Decode(encoded.c_str(), encoded.size(), buffer.get(), &decoded_length))
  {
    throw "ATL::Base64Decode failed";
  }

  return std::make_pair( decoded_length, std::move(buffer) );
}

That leaves the requirement to turn a serializable COM object into a byte array.  The following code does the job provided the COM object implements IPersistStream:

std::pair<size_t, std::unique_ptr<BYTE>> object_to_bytes( IUnknown* pUnk )
{
  // Create an in-memory stream, HGlobalMem will be allocated internally
  HRESULT hr;
  CComPtr<IStream> pIStream;

  if (FAILED(hr = CreateStreamOnHGlobal(0, TRUE, &pIStream)))
    throw "Could not CreateStreamOnHGlobal";

  CComPtr<IPersistStream> pPersistStream;
  if (FAILED(hr = pUnk->QueryInterface(IID_IPersistStream, &pPersistStream ) )
    throw "Failed to QI to IPersistStream";

  CLSID clsid;
  if (FAILED(hr = pPersistStream->GetClassID(&clsid) )
    throw "Failed to get clsid";

  // Serialise CLSID into stream
  if (FAILED(hr = WriteClassStm( pIStream, clsid )))
    throw "WriteClassStm failed";

  // Serialise object into stream
  if (FAILED(hr = pPersistStream->Save( pIStream, TRUE ) )
    throw "Save object into IPersistStream failed";

  // Find out how big the stream is
  LARGE_INTEGER liStart = {0};
  if (FAILED(hr = pIStream->Seek( liStart, STREAM_SEEK_SET, NULL ) )
    throw "IPersistStream::Seek failed";

  STATSTG statstg;
  memset( &statstg, 0, sizeof(statstg) );

  if (FAILED(hr = pIStream->Stat( &statstg, STATFLAG_NONAME ) )
    throw "IPersistStream::Stat failed";

  // Populate a byte array from the stream
  std::unique_ptr<BYTE> buffer( new BYTE[statstg.cbSize.LowPart] );
  unsigned long size_read;
  if (FAILED(hr = pIStream->Read( (void*)buffer.get(), statstg.cbSize.LowPart, &size_read ) )
    throw "IPersistStream::Read failed";

  ASSERT( size_read == statstg.cbSize.LowPart );

  return std::make_pair( size_read, std::move(buffer) );
}

CComPtr<IUnknown> bytes_to_object( size_t length, void* bytes )
{
  HRESULT hr;
  CComPtr<IStream> pIStream;

  HGLOBAL handle = GlobalAlloc( GMEM_MOVEABLE | GMEM_NODISCARD, length );

  if (FAILED(hr = CreateStreamOnHGlobal(handle, TRUE, &pIStream)))
    throw "Could not CreateStreamOnHGlobal";

  unsigned long size_written;
  if (FAILED(hr = pIStream->Write( bytes, length, &size_written ) )
    throw "Failed to write to stream";

  // Reset the stream
  LARGE_INTEGER liStart = {0};

  if (FAILED(hr = pIStream->Seek( liStart, STREAM_SEEK_SET, NULL ) )
    throw "IPersistStream::Seek failed";

  // Serialise CLSID out of stream
  CLSID clsid;

  if (FAILED(hr = ReadClassStm( pIStream, &clsid )))
    throw "ReadClassStm failed";

  // Create object from the clsid and stream from IStream into the object, using IPersistStream
  CComPtr<IUnknown> pUnk;
  if (FAILED(hr = ::CoCreateInstance(clsid, NULL, CLSCTX_ALL, &pUnk ) )
    throw "Failed to CoCreate clsid";

  CComPtr<IPersistStream> pStream;
  if (FAILED(hr = pUnk->QueryInterface(IID_IPersistStream, &pStream ) )
    throw "Failed to QI to IPersistStream";

  if (FAILED(hr = pStream->Load( pIStream ) )
    throw "IPersistStream::Load failed";

  return pUnk;
}

Then it’s a matter of putting the methods together to achieve a simple API:

std::string to_base64_encoded_byte_array( IUnknown* pUnk )
{
  auto bytes = object_to_bytes( pUnk );
  return to_base_64( bytes.first, bytes.second.get() );
}

CComPtr<IUnknown> from_base64_encoded_byte_array( const std::string& encoded)
{
  auto bytes = from_base_64( encoded );
  return bytes_to_object( bytes.first, bytes.second.get() );
}

Leave a comment

Filed under C++, C++ Code, Programming

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.