Click here to Skip to main content
15,867,330 members
Articles / Desktop Programming / MFC
Article

C++ Class Mapping - An XML Parser example

Rate me:
Please Sign up or sign in to vote.
3.71/5 (8 votes)
21 Nov 20045 min read 82.3K   985   36   13
How to use C++ macros to map class members for serialization or other purposes.

Introduction

Ever wonder what those Microsoft MFC message mapping macros did under the covers? It was always black magic to me - how clicking a button caused a function to be called. Luckily for the curious, Visual Studio lets you sleuth through the macros by using "Go to Definition" from the context menu.

I recently had a need to convert a structure to XML and vice versa in order to allow a web server to send TCP/IP messages to my server process - the messages would be formatted in XML, but the server would of course need to have access to the parsed XML elements in some sort of structure. There are a lot of XML parsers out there, including ActiveX controls - and so I make a disclaimer here that this is not intended to be a full featured parser. Our XML needs were very simple, and this is a simple solution.

But the purpose of the article is not really about XML parsing - it is about mapping the members of a class - either data or function - to a list, which contains information about the members, and associations to some function, so you can do something useful with the class members without having to write a bunch of code for each of your derived classes.

To better illustrate the point, take a look at the Microsoft message mapping macros (afxwin.h). There are four basic macro functions at work here.

We will use the following code as an example:

BEGIN_MESSAGE_MAP( CMyClass, CFrameWnd )
   ON_MESSAGE(CM_SOCKET_EVENT, OnSocketEvent)
   ON_MESSAGE(CM_FOLDER_CHANGED, OnFolderChanged )
END_MESSAGE_MAP()
  • DECLARE_MESSAGE_MAP() - This goes in the class declaration - the header file. It declares a hidden array of structures, each of which contains information about how to handle a particular Windows message. It also declares a function that returns a pointer to the array of structures, so Windows can walk this list when a Windows message arrives and call the appropriate handler. These members are declared as static - and so they must be defined in the source file, as all statics must be. This generates the following code declarations in the header file:
    private:
       static const AFX_MSGMAP_ENTRY _messageEntries[];
    protected: 
       static 
    const AFX_MSGMAP messageMap;
       virtual
     const
     AFX_MSGMAP* GetMessageMap() 
    const;
  • BEGIN_MESSAGE_MAP() - This defines the body of the function to retrieve the list of structures, and also defines the array itself, but leaves it open ended, like so:
    const AFX_MSGMAP* CMyClass::GetMessageMap() const 
       { return &CMyClass::messageMap; }
    AFX_COMDAT const AFX_MSGMAP CMyClass::messageMap =
       { &CFrameWnd::messageMap, &CMyClass::_messageEntries[0] };
    AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] =
    {  // Note the open ended initialization.......
  • ON_MESSAGE( event, function ) - There are many variants, such as ON_COMMAND, etc. These macros add a structure to the array with initialization values:
    // Wow, that's an ugly cast!
    { CM_SOCKET_EVENT, 0, 0, 0, AfxSig_lwl,(AFX_PMSG)(AFX_PMSGW)(
     static_cast< LRESULT (AFX_MSG_CALL CWnd::*)
      (WPARAM, LPARAM) >(OnSocketEvent)) },
  • END_MESSAGE_MAP() - This simply adds a zero filled termination structure to the array.
    {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }};

That's basically all there is. In our XML mapping example, we use the mapping macros to create structures that describe the member variables. This initial version supports the following basic types, but can easily be extended for support of other atomic data types like float, etc.

Integers

  • Integer
  • Integer Pointer
  • Integer Array
  • Integer Pointer Array

Strings (CStrings, but could be ported to use any string type)

  • CString
  • CString Pointer
  • CString Array
  • CString Pointer Array

Classes - Must be derived from the CXMLMessage class

  • Class
  • Class Pointer
  • Class Pointer Array
  • Class Array is not supported - this is possible, but extremely complicated, and not worth the coding effort.

New Features for Version 1.01 (November 16, 2004)

  • Base class function to generate XML Schema Definition file - CXMLMessage::GenerateXSD()
  • Base class function to generate Document Type Definition file - CXMLMessage::GenerateDTD()
  • No longer need to pass in a buffer to Deflate function
  • Copy constructor automatically defined for derived classes
  • Member initialization and cleanup automatically performed for derived classes.

New Features for Version 1.2 (November 18, 2004)

  • Support for derived classes
  • Inflate functions from a file handle or filename. This is useful when using XML for a configuration file.

Background

Familiarity with Windows message handling, using the Windows macros described above.

Using the code

The XML mapping macros in this example are very easy to use. Exporting a class to XML is done using the Deflate() function, and Importing from XML is done using the Inflate() function.

Here is an example of the XML map, which is in the CPP file for the derived class.

Note: The syntax for Inflate and Deflate has changed for V1.01 - much simpler to use.

BEGIN_XML_MAP( CMsgDestinationInfo, _T("destinationInfo") )
   // OR with XML_POINTER, XML_ARRAY as necessary
   XML_ELEMENT_DTYPE( _T("destination"),  m_destination,  XML_STRING )
   XML_ELEMENT_DTYPE( _T("filePathRoot"), m_filePathRoot, XML_STRING )
END_XML_MAP()

Version 1.2 now supports derived classes. Use the BEGIN_XMP_MAP2 macro for these. You can derive as many levels as you need to. Each derived class will include the XML mapped members from its base class. In this example, CMsgFTPDestinationInfo will have the members "destination" and "filePathRoot" included from CMsgDestinationInfo.

BEGIN_XML_MAP2( CMsgFTPDestinationInfo, _T("ftpDestinationInfo"), 
                                           CMsgDestinationInfo )
   XML_ELEMENT_DTYPE( _T("username"),  m_username,  XML_STRING )  
   XML_ELEMENT_DTYPE( _T("password"),  m_password,  XML_STRING )
END_XML_MAP()

And the header file for our class - with map declaration. Note: constructors and destructors are automatically provided by the macros. If you need special behavior here, override Initialize() or Cleanup() in your derived class.

class CMsgDestinationInfo : public CXMLMessage
{
   public:
  
   CString m_destination;
   CString m_filePathRoot;

   DECLARE_XML_MAP(CMsgDestinationInfo);
};

Or in the case of a derived class:

class CMsgFTPDestinationInfo : public CMsgDestinationInfo 
{
   public:
  
   CString m_destination;
   CString m_filePathRoot;

   DECLARE_XML_MAP(CMsgFTPDestinationInfo );
};

I'll spare you the initialization of the class members, but assuming that is done, here is how you generate the XML:

// Our XML Message object
CMsgFTPDestinationInfo message1;

int size = 0;
TCHAR* ptr = 0;

// Deflate the object ( save as XML )
if ( message1.Deflate( ptr, size ) != 0 )
   return 1; // Failed if ( message1.Deflate( ptr, size ) != 0 )

// ptr & size are now valid. ptr is deleted when message1 destructs.

And here is how you load the class members from an XML buffer (read from a file, socket, etc.):

// Inflate a new object from the XML buffer
CMsgFTPDestinationInfo message2;
if ( message2.Inflate( ptr, size ) != 0 )
    return 1; // Failed

Generating schema definition files is easy:

CMsgFTPDestinationInfo message3;
if ( message3.GenerateDTD() != 0 ) // Document Type Definitions
    return 1; 
if ( message3.GenerateXSD() != 0 ) // XML Schema Definitions
    return 1; // Failed

Points of Interest

I enjoy doing things you aren't supposed to be able to do - like manipulating and recasting pointers. Everything that is done in software is done by interpreting the meaning of a block of memory. And trying to understand how the compiler interprets pointers and what expects to find in a block of memory is always a fun challenge.

History

  • Nov 10 2004 - Article submitted.
  • Nov 16 2004 - Version 1.01 submitted - XSD and DSD generation, initialization, cleanup and copy constructors.
  • Nov 18 2004 - Version 1.2 submitted - support for derived classes.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralReference to XML_ATTRIBUTE Pin
c-sharp28-Apr-06 3:54
c-sharp28-Apr-06 3:54 
GeneralRe: Reference to XML_ATTRIBUTE Pin
Steve Johnson (Sven)28-Apr-06 4:14
Steve Johnson (Sven)28-Apr-06 4:14 
QuestionBug in CXMLMessage::DeflateInteger(...) ? Pin
OKN4-Nov-05 0:02
OKN4-Nov-05 0:02 
AnswerRe: Bug in CXMLMessage::DeflateInteger(...) ? Pin
Steve Johnson (Sven)4-Nov-05 5:37
Steve Johnson (Sven)4-Nov-05 5:37 
GeneralGreat for serialization Pin
CasualT30-May-05 12:33
CasualT30-May-05 12:33 
QuestionHow to use with Collections Pin
c-sharp27-May-05 5:32
c-sharp27-May-05 5:32 
AnswerRe: How to use with Collections Pin
Steve Johnson (Sven)27-May-05 6:14
Steve Johnson (Sven)27-May-05 6:14 
GeneralRe: How to use with Collections Pin
Steve Johnson (Sven)27-May-05 6:28
Steve Johnson (Sven)27-May-05 6:28 
( Sorry, previous reply had XML tagging enabled, which removed important stuff from the post. )

Hi Scott,

Yes and No. All arrays supported are implemented using CArray. You can use CArray for class pointers, but NOT CArray of classes. You could probably extend the code to do this. ( No guarantees, but give it a try ).

Look in CXMLMessage::InflateClass(). Under the array section, you will see where an array of class objects are not supported, ( but array of class pointers are ). In this section you could try:

CArray<CXMLMessage> *array = ( CArray<CXMLMessage>* )((char*) this + a_entry->m_offset );
while(1)
{
// Call the create function to return a pointer to the derived class.
// This create function is declared and
// defined by the macros DECLARE_XML_MAP and BEGIN_XML_MAP.

CXFUNC func = a_entry->m_fpCreate;
CXMLMessage* newClass = (*func)();

array->Add( *newClass ); // Now add class instead of class pointer.

// Recurse for this class
if ((rc = newClass->Inflate( a_buffer, a_limit, a_size, tag.GetBuffer()) != 0 ))
return rc;

// Peek ahead at next element - same type?
if ( CheckTag( a_buffer, a_limit, a_size, tag ) != 0 )
{
// No, end of array
return 0;
}
}

You will also need the inverse for CXMLMessage::DeflateClass()
Also check the mapping macros to allow for non-pointers to be
declared for class arrays. I will let you figure it out, but here is the
current macro, which as you can see forces the pointer attribute:

// Member variable is a class. Only class pointers are supported for arrays.
#define XML_ELEMENT_CLASS( token, memberElement, classType, flags ) \
{ token, MEMBER_OFFSET( memberElement ), \
flags | XML_CLASS | ((flags & XML_ARRAY ) ? XML_POINTER : 0 ), \
(CXFUNC) (reinterpret_cast< CXMLMessage* (*)() > \
(classType::newInstance)) }, \

Here is sample code for CArray of class pointers. You should be able to
figure out how to change for CArray of class.

// header file:
class CGroupList : public CXMLMessage
{
public:

CArray<CGroup*,CGroup*> m_groups;

DECLARE_XML_MAP(CGroupList);
};

// source file:
BEGIN_XML_MAP( CGroupList, _T("CGroupList") )
XML_ELEMENT_CLASS( _T("group"), m_groups, CGroup, XML_ARRAY | XML_OCCUR_0M )
END_XML_MAP()

You would want to change the macro statement so this would have to read as follows if you
were using class pointers, and unchanged ( as above ) if you were using class.

XML_ELEMENT_CLASS( _T("group"), m_groups, CGroup, XML_ARRAY | XML_POINTER | XML_OCCUR_0M )

Good Luck!

Steve

GeneralRe: How to use with Collections Pin
Steve Johnson (Sven)27-May-05 6:39
Steve Johnson (Sven)27-May-05 6:39 
GeneralImprovements for multiple inheritance Pin
Rainer Schuster17-Nov-04 23:03
Rainer Schuster17-Nov-04 23:03 
GeneralRe: Improvements for multiple inheritance Pin
Steve Johnson (Sven)18-Nov-04 10:05
Steve Johnson (Sven)18-Nov-04 10:05 
GeneralThis is What I Want Pin
Sudhir Mangla15-Nov-04 20:36
professionalSudhir Mangla15-Nov-04 20:36 
GeneralNice, look what I did Pin
Anonymous11-Nov-04 6:31
Anonymous11-Nov-04 6:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.