'Message' based system.(long)

Started by
8 comments, last by DrEvil 18 years, 2 months ago
I'm trying to develop a generic message-based communication module for a project that is contained in a dll, but must communicate various information back and forth to the host process. At a simple level, a message system could be some enumerated message id's, and a void * for the data. In an effort to make it slightly safer and to support a few more features here's what I'd like to do. Anyone can 'subscribe' to certain messages(still enumerated ids). Subscribers pass a functor that wraps the callback function that is to be called when the message is being sent through the system. Here is an example on the usage of the system, being that I have most of it already written.

typedef struct  
{
	int myint;
	short myshort;
} Message1;

typedef struct 
{
	int myint[2];
	short myshort;
} Message2;

class TestClassA
{
public:
	void testfunc1(const MessageHelper &_helper)
	{
		Message1 *pData = _helper.Get<Message1>();		
	}
	void testfunc2(const MessageHelper &_helper)
	{
		Message2 *pData = _helper.Get<Message2>();
	}
};

int main()
{
MessageRouter router;
	TestClassA a;
	MessageFunctorPtr func1(new MessageFunctor<TestClassA>(&a, &TestClassA::testfunc1));	
	MessageFunctorPtr func2(new MessageFunctor<TestClassA>(&a, &TestClassA::testfunc2));

	const int MSG_1 = 1;
	const int MSG_2 = 2;

	MessageRouter::SubscriberHandle h1, h2;
	h1 = router.SubscribeToMessage(MSG_1, func1);
	h2 = router.SubscribeToMessage(MSG_2, func2);
	
	// If this evaluates to false, there is no subscriber to this message,
	// so don't bother creating or sending it.
	if(MessageHelper hlp1 = router.BeginMessage(MSG_1, sizeof(Message1)))
	{
		Message1 *pMyStruct = hlp1.Get<Message1>();
		pMyStruct->myint = 1;
		pMyStruct->myshort = 2;
		router.EndMessage(hlp1);
	}
	if(MessageHelper hlp1 = router.BeginMessage(MSG_2, sizeof(Message1)))
	{
		Message2 *pMyStruct = hlp1.Get<Message2>();
		pMyStruct->myint[0] = 1;
		pMyStruct->myint[1] = 2;
		pMyStruct->myshort = 3;
		router.EndMessage(hlp1);
	}
	
	router.Unsubscribe(h1);
	router.Unsubscribe(h2);
}
I can post the implementation of these classes if requested. It all seems to work great, but there's one issue that I'm concerned about that seems like it could bring the whole concept to a grinding halt. Being that the goal of this is to have a generic communication system between a process and a dll, isn't it a potential problem of mismatched struct sizes if the dll is built with a different compiler or build setting than the process using the dll? Any way around this? I really like the overall idea of this implementation, but it's important that it will be reliable and not go up in flames by something as simple as struct member alignment or something. Also the templated Get function checks with the size of the message to make sure that the message is capable of holding the struct being cast to. If not an assert is triggered. I'd think this could be a problem as well, because sizeof(Message1) could differ on different compilers right? With these problems in mind, what then would be a good way to set up a communication system between a process and its dlls. In my particular example, both the host process and the dll would contain the same headers with the definition of the message structs Message1, Message2, etc... With that in mind, could I ensure the consistancy of struct size and alignment with pragma packs or anything like that which would be consistant between compilers? Any ideas are appreciated. Thanks.
Advertisement
The compiler has to put members in structs in the order you have specified. So the only problem may be with some weird alignment settings or if the library is compiled on another platform (eg. dll compiled with 32 bit compiler and application compiled with 64 bit one). In the most cases, alignment should not be an issue since most of the compiler probably use 4 B struct member alignment.
The only problem may be with differently sized integers for various architectures, but you can write templated class to work around this. I wrote my own sized integer and it was not that hard to do.
Hmm, so is there any way to detect differences in the struct alignments, or is it a small enough potential problem that it can be safely ignored? The reason I'm concerned is because the host module and the dll are commonly compiled with gcc as well as msvc 2003 and 2005. Can I standardize the alignment option between compilers by wrapping all the Message struct definitions with #pragma pack(4) in their headers, just to make sure?
According to this gcc supports #pragma directives exactly like the ones in MSVC. So in theory you should be able to compile with same member alignment.
Different alignment will cause structures to have different sizes. So it should be sufficient to test for struct size and if they differ, then report error.
To detect various alignments you can use something like this:
#pragma pack(push)#pragma pack(1)// One-byte alignmentstruct Struct1{    int x;    char y;    int z;};#pragma pack(pop)#pragma pack(push)#pragma pack(8)// 8 byte alignstruct Struct2{    int x;    char y; // this will cause troubles    int z;};#pragma pack(pop)// calculate offset of member name__ in type__ struct#define OFFSET(type__, name__) ((char *) &(type__.name__) - (char *) &type__)...Struct1 s1; // 1 B alignedStruct2 s2; // 8 B alignedcout << "sizeof(Struct1): " << sizeof(Struct1) << endl;cout << "sizeof(Struct2): " << sizeof(Struct2) << endl;cout << "Struct1: " << OFFSET(s1, x) << " " << OFFSET(s1, y) << " " << OFFSET(s1, z) << endl;cout << "Struct2: " << OFFSET(s2, x) << " " << OFFSET(s2, y) << " " << OFFSET(s2, z) << endl;

This should output this:
sizeof(Struct1): 9sizeof(Struct2): 12Struct1: 0 4 5Struct2: 0 4 8


As you can see they also differ in size. So in your dll you can have function which will return size of your message types and host application should compare it with size of its own messages.
Eg.
size_t getMessageSize(size_t message_type_id){    switch (message_type_id)    {        case MESSAGE_TYPE_1:            return sizeof(Message1);        ...        default:            error    }}

But this should not be neccessary, since you can guarantee sizes of integer data types by writing your own templates and then explicitly setting alignment for all message types.
HTH
Quote:Original post by b2b3
But this should not be neccessary, since you can guarantee sizes of integer data types by writing your own templates and then explicitly setting alignment for all message types.
HTH


Can you clarify what you mean by writing your own templates?
Still curious about the question above, but have more questions about this that I figured would be better asked in this thread than making another.

Suppose my MessageHelper class is just a class with a few members in it, all implemented in a header, no cpp file needed. Suppose my dll uses the MessageHelper quite a bit internally.

Here's the class.
class MessageHelper{public:	template<class Type>	Type *Get()	{		assert(sizeof(Type) <= m_BlockSize && "Memory Block Too Small!");		return static_cast<Type*>(m_pVoid);	}	MessageHelper(int _msgId, void *_void, size_t _size) :		m_MessageId	(_msgId),		m_pVoid		(_void),		m_BlockSize	(_size)	{	}	~MessageHelper() {};private:	int		m_MessageId;	void	*m_pVoid;	size_t	m_BlockSize;	MessageHelper();};


My question is this: Is it safe for my dll to export function that return MessageHelpers by value?

Such as this:

router is a function pointer in the dll, gotten with GetProcAddress
MessageHelper hlp1 = router->BeginMessage(MSG_1, sizeof(Message1));

Is this safe to return MessageHelper by value? Obviously the application and the plugin are both going to be including the MessageHelper header.

Specifically what I'm concerned about is whether there can be issues with the size of the class for example not being consistant between the dll and the host application or other potential problems that might arise.

Basically the BeginMessage function is going to return a MessageHelper holds the size of the memory block allocated, the pointer to the memory block, and the message id. The memory will be allocated from a pool in the dll, the host process will not be able to free it, which is partly why it's wrapped in a class. The other reason it's wrapped it because rather than casting a void * themselves, the user would have the templated Get function that can have size assertions to make sure the size of the memory block that is referenced is capable of holding the type they attempt to cast to.

If there are no subscribers to the message, the MessageHelper returned will basically be invalid, and an IsValid() function or overloaded bool operator can be used to check if it's good before the user attempts to pack any data into it and send it, like the examples show above.

I just want to verify before I get too far into implementation that I'm not going to have bad issues with alignment or other unforseen problems that often plague communication between applications and dlls. Perhaps I'm being overly paranoid about these things. If so please let me know.

Edit: oops almost forgot the other question.

The 2nd question I'm pretty sure is definately going to be problematic, at least how the example above shows it implemented.

How can I safely allow the host application to register functors with the dlls in order to subscribe to messages? Clearly if I do it as the example above, the host application would be allocating memory and giving it to the dll, and the dll would clean it up later, which is a no-no.

Would it be best to simply have a Subscribe function that takes a function pointer rather than a MessageFunctor class? Something like:

typedef void (*pfnMessageFunction)(const MessageHelper &_helper);
MessageRouter::SubscribeToMessage(int _msgid, pfnMessageFunction _func);

Currently I only have:
SubscribeToMessage(int _msgId, MessageFunctorPtr _func);
where MessageFunctorPtr is a typedef for boost::shared_ptr to a Functor class. This class allows the calling of non static member functions of particular classes.

If I added the simpler SubscribeToMessage that just took a function pointer, that would mean the host application couldn't register non static functions as callbacks. I can live with this, but if anyone see's a simple solution to still support that please let me know.

Thanks
Quote:Original post by DrEvil
Quote:Original post by b2b3
But this should not be neccessary, since you can guarantee sizes of integer data types by writing your own templates and then explicitly setting alignment for all message types.
HTH


Can you clarify what you mean by writing your own templates?


By that I mean that you can write template like Int< int n > which will always choose n-bit signed integer form the list of all integer types supported by compiler. Then you can use this template to define your messages and set alignment manually. For example:
#pragma pack(push) // push it first, so we don't set alignment for other data structures in file                   // this is optional, you may want to set it for everything#pragma pack(4)struct Message1{Int< 32 >::type message_id;Int< 32 >::type message_data1;Int< 8 >::type message_dat2;};#pragma pack(pop)  // restore alignment

This should be same size no matter which platform or compiler you run (provided they support such pragmas and data types).
Then you can pass such messages without worrying about their size, since it should be always same. To write those int templates you can use typelists
(see loki library) and a little of template meta-programming. I wrote such classes and I can post them here, if you want.

Quote:Original post by DrEvil
My question is this: Is it safe for my dll to export function that return MessageHelpers by value?


I the sizes of classes in dll and host app match, there should'n be problem with returning by value.
What may not be that easy is to create dll with c++ functions/classes and then loading it at runtime. When you put c++ function or class into dll every compiler has to mangle names of classes/functions.
Every compiler uses another algorithm and names may differ even between different builds with the same compiler - when you use anonymous namespace/class/enum/whatever compiler will use random numbers to generate unique name. This may even affect performance - linker can put such functions on different places and this may cause cache misses on different places than before.
If you plan to compile dlls with different compiler than exes, you may not be able to get pointers to functions from dll. The only way to write "compatible" dlls is to disable name mangling. To do that you need to declare all exported functions as export "C", but then you can't return classes by value.
I don't know of any simple way aroud this. You can declare everything as c export and then return pointers to data structures, but then you have to be careful about releasing memory.

Quote:
How can I safely allow the host application to register functors with the dlls in order to subscribe to messages? Clearly if I do it as the example above, the host application would be allocating memory and giving it to the dll, and the dll would clean it up later, which is a no-no.

Would it be best to simply have a Subscribe function that takes a function pointer rather than a MessageFunctor class? Something like:

typedef void (*pfnMessageFunction)(const MessageHelper &_helper);
MessageRouter::SubscribeToMessage(int _msgid, pfnMessageFunction _func);

Currently I only have:
SubscribeToMessage(int _msgId, MessageFunctorPtr _func);
where MessageFunctorPtr is a typedef for boost::shared_ptr to a Functor class. This class allows the calling of non static member functions of particular classes.

If I added the simpler SubscribeToMessage that just took a function pointer, that would mean the host application couldn't register non static functions as callbacks. I can live with this, but if anyone see's a simple solution to still support that please let me know.


The easiest way is of course using function pointers. I think you should try it first and get something working. If you find that this solution does not provide what you want, you can always change it later.
As for those functors, well, I have no idea [smile]. Only way I can think of is something like this:
// with functors, I'm using method rather tha overloaded operator for clarity :)class NonStaticBase{public:virtual DiStuff(const MessageHelper &) = 0;};class NonStatic : public NonStaticBase{public:void DoStuff(const MessageHelper &) { ... }};// without functors// Just to provide common base, so you can use pointers without need for templatesclass StaticBase{typedef void (*DoStuffFunction)(int, StaticBase *);};class Static : public StaticBase{public:static void DoStuff(const MessageHelper &, StaticBase *) { ... }};// to subscribe to messageMessageRouter::SubscribeToMessage(int _msgid, NonStaticBase &_func);// orMessageRouter::SubscribeToMessage(int _msgid, StaticBase::DoStuffFunction _func, StaticBase *This);

Well, this does not look nice, but it should work. It has one drawback - you will need to implement all functions which work with instances of Static as static functions with at least one parameter which will be pointer to the instance. In your message router you will then need to call message handlers with appropriate pointers.
I'm not very advanced when it comes to templates, so I'm not familiar with typelists or meta programming. Any help with that would be much appreciated.

Do you think these advanced templates are necessary? Am I being overly paranoid about data sizes getting mismatched? The size of the ints don't matter so much to me as the struct being consistant in the host app and the dll. Consistant in that a block of memory is both the same size, and if the host app casts a pointer to various struct *'s, and passes it to the dll as the data for a message, when the dll casts it to the same struct pointer the data will be correct and no padding or differences in offset would result in jacked up data on one side or the other. Plus, message will commonly have void *'s in them as well and floats.

Oh and, btw. I'm already using extern "C" to eliminate name mangling and such.
I reread your posts and I realized, that you won't be sending messages between various platforms via network.

I think, that for any reasonable compiler settings it will not cause problems. Data types have usually same size (eg. int is 32 bit on gcc and msvc, float is always 32 bit, pointers are 32 bit). A lot of programs rely on int being 32 bit, so I don't think they will change that soon. Only problem may be, when you try to use 32 bit library with 64 bit application (is this possible? I don't have 64-bit Win, so I can't test) since pointers are 64 bit on 64-bit computers. Then your structs will ofcourse have different size and 32 bit app won't be able to interpert 64 bit pointers.
But if your dlls and apps are both compiled on same platform sizes of data types should be the same. So if you don't mess with alignment, it should not cause any problems. Default alignment is 4 B for gcc and for msvc (in 32 bit mode), so structs will look same in gcc and in msvc and therefore you probably won't need any complex templates.
That's what I was hoping. Just wanted to get some other input.

I really appreciate all the help.

This topic is closed to new replies.

Advertisement