Dll plugin system: cross plugin "shared" classes

Started by
16 comments, last by GameDev.net 10 years, 7 months ago

Hm, that sounds like it, but also complicated again, having to define lots of stuff for each possible data type, which can get quite enormous (theoretically, I'd have to define each primitive type, engine class and possible common std::classes). I quess the safety comes at a cost, but I didn't expect that it would need so much extra work per type... I thought there'd be a more general solution. Well, browsing through the web I've found kind of a universal solution, still requiring RTTI but appearantly way faster, which I could apply in a helper function. I'm talking about using typeid. Since I only care about that the types are equal, wouldn't it fare just as well if I just did that?

Thats kind of like what you suggested, just with using RTTI instead of making up a custom system, and thus not having to do a custom . Does that sound reasonable, or are there any drawbacks I'm not seeing right now?


Unfortunately there is a gotcha in that. If I'm not mistaken, typeid returns a const type_info reference which the content of can (will?) change between dll's. With so's on *nix variants, it is probably all nice and safe given the consistent memory model, that is not the case on Windows and I'm almost positive this would end up with multiple type_info's which don't match if passed over dll boundaries. This and the bloat/performance issues with RTTI is why you see so many games that roll their own RTTI replacements, and it is almost always only a problem on Window's because of the memory model. I haven't investigated it myself in a long time, so all I can suggest is testing it in debug/release modes and being very careful before you decide it works.

Now, with all the above, there is one other item to keep in mind when moving to the DLL solution. Memory allocations. I assume you either know about this or have worked around it. Basically unless all the plugins and the executable share a common copy of msvcrt.dll, the memory will come from different memory managers and passing the memory around is unsafe. Even using the std::string stuff is unsafe since the memory comes from different allocators. In the past I
<snippy>


I've definately heard of it, thats why I tried to keep as much as possible internal in my plugins. But what I've also heard so far is that using pointer/references is somewhat safe, like that a std::wstring& will work cross-compiler, since its a pointer and the size will be the same regardless, while only the internal "layout" of the wstring will differ... am I missinformed? Plus, right now my whole engine is a dll that both the plugins and the application have to link against, that seems to be just what you are talking about, isn't it? By god, I don't want to write a dealloc-function for every simply getter I export :/

Unfortunately things are considerably trickier.. sad.png Yes, reference counted objects will solve this and by nature you might think that stl takes care of the problem, you'd be wrong and bad things are going to happen. Take the silly example:


// in your plugin:
const std::string  GetName() const;  // Note: non-reference for example..

// in other plugin or somewhere:
const std::string theName = thePlugin->GetName();
When 'theName' leaves scope you are going to trash the allocator list in the calling code. The reference count works as intended, the memory transfers to "theName" as intended, the reference is increased, the temporary goes away and the reference is back to 1. (or a move happens that just puts it directly in theName, either way) Unfortunately the problem is that the allocation took place in the memory space of the plugin and because stl is header only and likely inlined the delete call takes place in the other plugin or program which made the call. Boom, you just leaked in the plugin and corrupted the callers memory.

You have a number of ways to correct such things:

1. Guarantee *all* plugins, executables etc link to the dll version of msvcrt.
2. Make a dll library which everything links to which provides the link the msvcrt. (Actually a bit safer than #1 because it guarantee's everyone uses the same version.)
3. Use custom allocators for all STL. The functions must be behind a non-inlinable interface and in a single location. This is a pain in the butt and I don't suggest trying it, too easy to miss things.

Probably more solutions. Sorry to make it sound like such a pain. #1 or #2 are not particularly difficult.
Advertisement


Hmm, thats not really what I wanted to imply, the only real "problem" I had with the GetInterface solution (as well as AllEightUps) was the I required to declare a new interface with custom id etc.. for each type that was possibly going to be used, which at least in my had made it seem a little much work, where I'd prefered a more generalized method. In that matter, what are your thoughts on the method using typeid I proposed before?

In your case typeid would likely work as well as a dynamic cast - both end up calling strcmp in order to get cross dll compatibility. For this reason generally all performance aware applications like games generally end up writing their own type system like I've described, and additionally many games build without RTTI.


Guarantee *all* plugins, executables etc link to the dll version of msvcrt.

Note that it has to be the exact same version - the debug version is not compatible with the release version.


Unfortunately there is a gotcha in that. If I'm not mistaken, typeid returns a const type_info reference which the content of can (will?) change between dll's. With so's on *nix variants, it is probably all nice and safe given the consistent memory model, that is not the case on Windows and I'm almost positive this would end up with multiple type_info's which don't match if passed over dll boundaries. This and the bloat/performance issues with RTTI is why you see so many games that roll their own RTTI replacements, and it is almost always only a problem on Window's because of the memory model. I haven't investigated it myself in a long time, so all I can suggest is testing it in debug/release modes and being very careful before you decide it works.

As far as I've tested it in debug mode, the references fromt typeid are equal and will compare true even cross-dll. I'll have to configure plugins for release mode first, so I'll test it at another timer, but so far, it at least seems to work. I'll do some more in-depth testing but for now, just to get all my modules up and running as plugins, I'll consider it at least a temporary base.


You have a number of ways to correct such things:

1. Guarantee *all* plugins, executables etc link to the dll version of msvcrt.
2. Make a dll library which everything links to which provides the link the msvcrt. (Actually a bit safer than #1 because it guarantee's everyone uses the same version.)
3. Use custom allocators for all STL. The functions must be behind a non-inlinable interface and in a single location. This is a pain in the butt and I don't suggest trying it, too easy to miss things.

Probably more solutions. Sorry to make it sound like such a pain. #1 or #2 are not particularly difficult.

Uh wow, thats much more complicated that I thought it'd be. Now granted, I wanted to wait with implementing dll plugins some more time, but after I've relealized its at least doable (though I'm pretty sure I'm far away from portability or anything like it), I wanted to do go for it right away. At least I'm learning a lot of new things. Regarding the linking to msvcrt, do I have to manually make sure that the dll libary everything likes to (as in solution #2) is linking to a specific version of msvcrt? If so, how would I achieve that?


In your case typeid would likely work as well as a dynamic cast - both end up calling strcmp in order to get cross dll compatibility. For this reason generally all performance aware applications like games generally end up writing their own type system like I've described, and additionally many games build without RTTI.

Regarding the strcmp, I'm not so sure.. at least in debug build, even the returned hash_code() is equal cross-dll, so I suppose it could do that instead, making it at least a bit faster than dynamic_cast, also due to that those are compiler constants and dynamic_cast has to traverse some more... I'm not so sure, just from what I've read in numerous posts on stackoverflow, etc... .Thinking about it, I wonder if dynamic_cast would even work in any way? I mean, for the virtual overloaded GetAttribute()-method, I can just return void*, since I can't make it a template, since this can't be made virtual... and I've heard dynamic_cast won't work with void*. So just out of interest, I'm not employing it anyway, but would there be any way to get dynamic cast to work here (see my example code in my last post before this one)?

Regarding the performance issue, I do get what you are saying, and its definately noted on my todo list, eigther if the typid-thing doesn't work e.g. in release mode, or if I'm noticing performance issues here, or just as a general optimization I'll apply when I feel a lack of motivation to implement new features or so. In that regard, thanks to you and AllEightUp for offering me those solutions, I'll have to decide which one I'm going to apply though when it come to it.

Just one note, is there any possible way in any of you guys methods to at least automatize the way of "registering" the types at compile time? Something like


// in one header file:

REGISTER_TYPE<int>();
REGISTER_TYPE<float>();
REGISTER_TYPE<gfx::IModel>();
REGISTER_TYPE<std:vector<int>();

That somehow auto-generates all the interfaces/entries and type ids?

EDIT:

Thinking more in-depth about it, can't I just go:


// in typeinfo.h

class TypeInfo
{
public:
	static size_t GenerateTypeId(void);
private:
	static size_t typeId;
};

// in typeinfo.cpp
size_t TypeInfo::GenerateTypeId(void)
{
	return typeId++;
}

// in type.h
template<typename Type>
size_t GetTypeId(void)
{
	static size_t type = TypeInfo::GenerateTypeId();
	return type;
}

template<typename Type, typename Type2>
bool TypesEqual(void)
{
	return GetTypeId<Type>() == GetTypeId<Type2>();
}

// in Component.h

template<typename Type>
Type* BaseComponent::GetAttributeSafe(const std::wstring& stName)
{
	size_t typeId = 0;
	if(auto pAttribute = GetAttribute(stName, &typeId))
	{
		if(typeId == GetTypeId<Type>())
			return (Type*)pAttribute;
	}

	return nullptr;
}

// implementatation

void* Light::GetAttribute(const std::wstring& stName, size_t& type) const
{
	if(stName == L"Model")
	{
		type = GetTypeId<gfx::IModel>(); // maybe add some ReturnAttributeSafe - template method or macro to unsure type and id match
		return pModel;
	}

	return nullptr;
}

// usage:

gfx::IModel* pModel = pLight->GetAttribute<gfx::IModel>(L"Model");

Wouldn't that work? It would both be fast (one virtual function call for the unavoidable overloaded GetAttribute-method) and secure to a certain point. It would not require any special work for a new type, and only types really used would be created. Or are there any more gotchas I'm missing? Any thoughts on it compared to the GetInterface/Variant method?


Wouldn't that work?

An automatic local id system is probably do-able, but there are gotchas. I think the example you use may not work as the template function may get instantiated multiple times (at least once for each dll) leading to multiple static members with different values for type for the same Type.

Guarantee *all* plugins, executables etc link to the dll version of msvcrt.


Note that it has to be the exact same version - the debug version is not compatible with the release version.


Actually, if you have a fixed ABI such as pure virtual interfaces in front of the plugins and no cases of passed memory for the various reasons given, using a debug version or different msvc's is quite possible. Of course in such a case you are basically using a stripped down variation of COM which is what I have almost always used. COM as a "pattern", not the MS implementation, is actually quite nice and solid with many benefits such as this ability to mix and match so long as the ABI's are standardized.

Wouldn't that work?


An automatic local id system is probably do-able, but there are gotchas. I think the example you use may not work as the template function may get instantiated multiple times (at least once for each dll) leading to multiple static members with different values for type for the same Type.


I suspect a runtime ID system will not be effectively available until VC gets it's act together and implements C++11 'constexpr'. I've done many experiments on this and the best solution I know of is generally a hash function with salt. (I.e. item name + say header where it is defined name.) Of course you have potential issues with collisions (can be caught in debug builds) but without constexpr you can't get it to be really general purpose. I wrote an experimental Clang implementation of djb2 hashing which happens at compile time and is independent of modules, but of course it only works with GCC/Clang since VC throws a tantrum right now. sad.png


I suspect a runtime ID system will not be effectively available until VC gets it's act together and implements C++11 'constexpr'. I've done many experiments on this and the best solution I know of is generally a hash function with salt. (I.e. item name + say header where it is defined name.) Of course you have potential issues with collisions (can be caught in debug builds) but without constexpr you can't get it to be really general purpose. I wrote an experimental Clang implementation of djb2 hashing which happens at compile time and is independent of modules, but of course it only works with GCC/Clang since VC throws a tantrum right now.

The lack of constexpr is certainly a nuisance. You can do compile-time hashing with VS though - have a look here (and in the comments): http://www.altdevblogaday.com/2011/10/27/quasi-compile-time-string-hashing/

I suspect a runtime ID system will not be effectively available until VC gets it's act together and implements C++11 'constexpr'. I've done many experiments on this and the best solution I know of is generally a hash function with salt. (I.e. item name + say header where it is defined name.) Of course you have potential issues with collisions (can be caught in debug builds) but without constexpr you can't get it to be really general purpose. I wrote an experimental Clang implementation of djb2 hashing which happens at compile time and is independent of modules, but of course it only works with GCC/Clang since VC throws a tantrum right now.


The lack of constexpr is certainly a nuisance. You can do compile-time hashing with VS though - have a look here (and in the comments): http://www.altdevblogaday.com/2011/10/27/quasi-compile-time-string-hashing/


Yup, the hash is possible at run time but the general purpose nature is missing. Some of the rather annoying limitations are that it can't be used in switch statements, constants in templates, enumeration assignments etc. Without the constexpr keyword I found such implementations to be mostly curiosity items, not to say they don't have some cases of usability, they unfortunately lack general application.

This topic is closed to new replies.

Advertisement