|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionAll the C/C++ textbooks I've ever read criticize the use of macros. "Don't use them, they're dangerous because they conceal what you actually write. Especially function-looking macros." Nevertheless macros are still used in some places. For example - debug macros, such as I know some people who never use those macros. Instead they just use functions that are implemented differently in debug and release builds. Why? Because they're afraid of the threat of using function-looking macros. That is, they prefer that the expression inside I personally always use macro-versions of those. That's because I use debug macros very heavily to get immediate indication when something goes wrong, whereas on the other side I don't want the final executable to come with all this crap. Another example of widely used macros is related to Ansi/Unicode. This includes So that despite the criticism macros are still being used. One may argue if this is justified or not, but this is not the subject of the article. I want to show in this article really amazing things that can be done via sophisticated use of macros. Wether to use those techniques or not - the decision is up to you. Communication protocol exampleSuppose you have to implement some communication protocol. This protocol consists of 'messages' of different kinds, every message has its specific parameters. Let's agree (for now) that every transferred message starts with its 4-byte size (thus restricting the largest message to order of 4GB), then comes its 2-byte code, and then come all its parameters which are message-dependent. Ordinal types ( For beginning we want the following message types:
So, how do we implement this ? For every message type we need the following:
For the purpose of this example we'll use the following abstract classes for streaming: struct OutStream {
virtual void Write(LPCVOID pPtr, size_t nSize) = 0;
// ordinal types
template <class T>
void Write_T(const T& val)
{
Write(&val, sizeof(val));
}
// variable-sized strings
void Write_Str(const CString& val)
{
ULONG nLen = val.GetLength();
Write_T(nLen);
Write((PCWSTR) val, nLen * sizeof(WCHAR));
}
};
struct InStream {
virtual size_t Read(LPVOID pPtr, size_t nSize) = 0;
bool ReadExactTry(LPVOID pPtr, size_t nSize)
{
while (true)
{
size_t nPortion = Read(pPtr, nSize);
if (nPortion == nSize)
return true; // ok
if (!nPortion)
return false; // not enough data.
nSize -= nPortion;
if (pPtr)
((PBYTE&) pPtr) += nPortion;
}
}
void ReadExact(LPVOID pPtr, size_t nSize)
{
if (!ReadExactTry(pPtr, nSize))
{
// not enough data, raise an appropriate exception
throw _T("not enough data!");
}
}
// ordinal types
template <class T>
void ReadExact_T(T& val)
{
ReadExact(&val, sizeof(val));
}
// variable-sized strings
void ReadExact_Str(CString& val)
{
ULONG nLen;
ReadExact_T(nLen);
PWSTR szPtr = val.GetBuffer(nLen);
ReadExact(szPtr, nLen * sizeof(WCHAR));
val.ReleaseBuffer(nLen);
}
};
Now let's implement our messages. struct MsgLogin
{
// message fields
ULONG m_Version;
CString m_Username;
CString m_Password;
MsgLogin()
{
// zero-init members.
m_Version = 0;
}
void Write(OutStream&);
void Read(InStream&);
};
void MsgLogin::Write(OutStream& out)
{
// first comes the message size (in bytes). Let's calculate it.
ULONG nSize =
sizeof(ULONG) + // message size
sizeof(USHORT) + // message code
sizeof(m_Version) +
sizeof(ULONG) + m_Username.GetLength() * sizeof(WCHAR) +
sizeof(ULONG) + m_Password.GetLength() * sizeof(WCHAR);
out.Write_T(nSize);
out.Write_T((USHORT) 1); // the code of the login.
out.Write_T(m_Version);
out.Write_Str(m_Username);
out.Write_Str(m_Password);
}
void MsgLogin::Read(InStream& in)
{
in.ReadExact_T(m_Version);
in.ReadExact_Str(m_Username);
in.ReadExact_Str(m_Password);
}
Then in order to send/save this message you'll have to write it this way: MsgLogin login;
login.m_Version = MAKELONG(1, 3);
login.m_Username = _T("user");
login.m_Password = _T("pass");
login.Write(my_out_stream);
Message parsing code should be something like this: while (true)
{
ULONG nSize;
if (!my_in_stream.ReadExactTry(&nSize, sizeof(nSize)))
break; // no more messages so far.
USHORT nCode;
my_in_stream.ReadExact_T(nCode);
switch (nCode)
{
case 1: // login
{
MsgLogin login;
login.Read(my_in_stream);
// process incoming message
HandleMsg(login);
}
break;
default:
// unknown message, bypass it.
for (nSize -= sizeof(ULONG) + sizeof(USHORT); nSize; )
{
BYTE pBuf[0x100];
size_t nPortion = min(sizeof(pBuf), nSize);
my_in_stream.ReadExact(pBuf, nPortion);
nSize -= (ULONG) nPortion;
}
}
}
Now we have to add other messages. For each of them we'll write their struct declaration, Writing all this is a pretty large routine work, with reasonable chance of typing/copy-paste mistakes. Now let's demonstrate how this can be done via macros. First we declare the list of messages we want to have: #define COMM_MSGS_All \
COMM_MSG(1, Login) \
COMM_MSG(2, LoginRes) \
COMM_MSG(3, Chat)
What does this mean ? By now it actually means nothing. The macro Now let's write for every message the members we want in it: #define COMM_MSG_Login(par) \
par(ULONG, Version) \
par(CString, Username) \
par(CString, Password)
#define COMM_MSG_LoginRes(par) \
par(UCHAR, Result)
#define COMM_MSG_Chat(par) \
par(CString, Recipient) \
par(CString, Test) \
par(UCHAR, Flags)
Again, those 3 macros we've just defined don't mean too much. They just list in an abstract (not yet defined) way what our messages should contain. NOTE: Each of this macros needs a parameter ( And now let's breath life into our macros. First we said we need to declare a struct for every message type. Let's do it: #define PAR_DECL(type, name) type m_##name;
#define PAR_ZERO(type, name) ZeroInit(m_##name);
template <class T>
What does this mean ? Let's see. The first line inside the struct declaration is Next we have a constructor. It has Next we declare template<class T>
ULONG CalcSizeOf(const T& val) { return sizeof(val); }
template <>
ULONG CalcSizeOf<CString>
We use again Inside Now the parser code turns into the following: while (true)
{
ULONG nSize;
if (!my_in_stream.ReadExactTry(&nSize, sizeof(nSize)))
break; // no more messages so far.
USHORT nCode;
my_in_stream.ReadExact_T(nCode);
switch (nCode)
{
#define COMM_MSG(code, name) case code: \
{ \
Msg##name msg; \
msg.Read(my_in_stream); \
HandleMsg(msg); \
} \
break;
COMM_MSGS_All
#undef COMM_MSG
default:
// unknown message, bypass it.
for (nSize -= sizeof(ULONG) + sizeof(USHORT); nSize; )
{
BYTE pBuf[0x100];
size_t nPortion = min(sizeof(pBuf), nSize);
my_in_stream.ReadExact(pBuf, nPortion);
nSize -= (ULONG) nPortion;
}
}
}
That is, for every known message we parse it and call the overloaded Let's make some conclusions. What exactly did we achive, apart of making the program absolutely unreadable ? The answer is that we made the generation of structs for messages, their serialization and parsing automatic. If you want to add another field to a message there is a *SINGLE* place you have to change: the appropriate If you add a new message then you'll have to list its parameters and append its entry into the Compare this with what we had at the beginning: For every message type you write all the methods. When you have tens of different message types - it's a nightmare! Now suppose we decided to change the protocol. For instance, we don't want to place the message size into the stream (thus making bypassing unknown messages impossible). You have tens places to fix !!! Let's go even further: for every message we want a run-time textual description of its members, which we can log/display. Let's implement it: #define PAR_FMT_UCHAR "u"
#define PAR_FMT_USHORT "u"
#define PAR_FMT_ULONG "u"
#define PAR_FMT_CString "s"
#define PAR_FMT1(type, name) _T("\t") _T(#name) _T(" = %") _T(PAR_FMT_##type) _T("\r\n")
#define PAR_FMT2(type, name) ,msg.m_##name
#define COMM_MSG(code, name) \
void TxtDescr(const Msg##name& msg, CString& sOut) \
{ \
sOut.Format(_T("Type=%s, Code=%u\r\n") \
COMM_MSG_##name(PAR_FMT1) \
,_T(#name), code \
COMM_MSG_##name(PAR_FMT2)); \
}
COMM_MSGS_All
#undef COMM_MSG
We generate And this is automatically done for all the messages. You don't like this implementation ? Then rewrite it. There's absolutely no problem, because You have a *SIGNLE* place to rewrite.
But what is the alternative? Writing, writing, copy+pasting and etc.? From my personal experience rewriting the same thing multiple times, besides of demoralization, is much more dangerous than using macros. If you write something wrong in the macro - most probably it just won't compile. And even if it will - it will work wrong probably for all kinds of messages. And if something works wrong - there's a *SINGLE* place that you have to fix. And if you just write manually all the functions for all the message types - what are the chances to do it without mistakes? Pretty close to zero, unless you're a robot. And if you make a mistake in one message which is rarely used - you'll not know it until you get the surprise. Yes, macros are very dangerous, they require good skills to write, it's very hard to read them, it's impossible to debug them. But they eliminate the need in rewriting the same thing several times. This may sound crazy, but I think it's easier to maintain macros then dozens of almost identical code lines. You need to change something - go ahead, change *ONE* specific place. Is there another way to implement messages without rewriting too many things and not using macros? In this specific example - yes. We could write it in this way: struct ParamBase {
PCTSTR m_szName;
virtual void Write(OutStream&) = 0;
virtual void Read(InStream&) = 0;
virtual ULONG CalcSize() = 0;
};
struct Param_UCHAR :public ParamBase {
UCHAR m_Val;
virtual void Write(OutStream&);
virtual void Read(InStream&);
virtual ULONG CalcSize();
};
// ...
struct MsgBase {
std::list
That is, we arrange all our parameters in a list, which can be used for message serialization. Macros on the other side give you the maximum flexibility.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms
of Use
Last Updated: 24 Apr 2008 Editor: |
Copyright 2008 by valdok Everything else Copyright © CodeProject, 1999-2008 Web08 | Advertise on the Code Project |