Advertisement

Getting rid of the generic calling convention once and for all !

Started by September 23, 2010 11:34 PM
3 comments, last by pifor106 14 years, 2 months ago
Hi everyone,

In the past days, I've been trying real hard to get an engine registration framework that automatically deals with the generic calling convention when using AngelScript on platforms that do not support the native calling conventions. The motivation behind this is that I didn't want the end-user of our pipeline, typically gameplay programmers, to worry about the intrinsics of AngelScript too much since I think they'd make mistakes or forget to register for the generic calling convention.

I know you're probably thinking, why on earth is he using the generic calling convention anyway ? Well, this is another debate but it suffices to say that I'm working on some, as of yet, unsupported platforms, and that my team often ports our game solution to new platforms to follow the market trends, so this is more a business decision than anything and I don't really have the time/skills to implement the native calling conventions for each of those platforms.

Anyway, the point is, I personally didn't like very much the concept of the generic calling convention, because basically there were 2 paths when registering the engine stuff and that was quite unpleasant for us ( about the only thing about AngelScript ... so that's quite excellent ! ). So, I came up with a solution that works ( unit tested / functionally tested ) within which the end-user only register as if he were using the native calling convention.

The process is quite simple actually, and requires little effort and when the dust settles, it's actually pretty easy to implement.

Here's what you'll need :

1) The autowrapper add-on with some modifications, which I can provide.
2) The user-data on asIScriptFunction which Witchlord added.
3) Some C++ meta-programming skills.
4) Diet Pepsi. Because you always need pepsi, but without the calories !

Beside the shameless plug, the first thing to do is to start by creating a "façade" object on top of an instance of a asIScriptEngine. I'll show the implementation for the methods, since it's the hardest one to do but you can get the idea and derive the function/behavior registration from the concept.

class Registry
{
public:
Registry(asIScriptEngine* engine);

template< typename Convention, typename FunctionPointer >
int RegisterObjectMethod(const char* object, const char* declaration, FunctionPointer functionPointer)
{
... we'll implemented afterwards ...
}
};

Notice the two templated arguments passed to the RegisterObjectMethod. The first one, typename Convention, replaces the asDWORD callConv typically passed to the asIScriptEngine* call. We'll get to this a bit later. The second one, well, that's the const asSFuncPtr& passed to the asIScript::RegisterObjectMethod function, but instead of using asMETHOD/asMETHODPR, we'll pass the function pointer directly. So, for example :

Registry registry(m_engine);
registry.RegisterObjectMethod<ThisCall>("Foo", "void Bar()", &Foo::Bar);

You can also use the following macro to get the function pointer and resolve ambiguity for overloaded member functions

#define METHOD_PTR(Object, Method, Params, Return) static_cast<Return (Object::*) Params>(&Object::Method)

Forget about the templated ThisCall for now, we'll see about those later. The important thing to notice here is that the second template argument of the method is not explicitely typed, which is a good thing since it's actually the signature of the function. In this case, it is actually : void (Foo::*)(). But, since the compiler can detect the templated type from the expression, is is not required to type it, which is nice :)

Ok, so now we have two things :

1) The function pointer to the method and, of greater importance, it's SIGNATURE.
2) A, as of yet, unexplained ThisCall template argument.

Let's define ThisCall, and we'll understand a bit more about it ( and it's brothers CDecl, CDeclObjFirst, CDeclObjLast, Generic ).

This object will serve two purposes :

1) a) When compiling on a natively supported platform, it will provide the asDWORD callConv parameter.
2) a) When compiling on a natively supported platform, it will provide a asSFuncPtr built using a pointer to the method class.
2) b) When compiling on a genercly supported platform, it will provide a asSFuncPtr that will point to a function that will invoke the method through a dispatch mechanism.

Let's start by making the native calling convention work. Pretty easy :

struct ThisCall
{
enum E_NTC { NATIVE_CALLING_CONVENTION = asCALL_THISCALL };

template< bool IsNativeCallingConventionSupported >
struct Invoker
{
template< typename FunctionPointer >
static asSFuncPtr Create(FunctionPointer fp);
};
};

Notice the internal struct Invoker. It's templated to accept a bool, so we can either generate a version for the native calling convention and one for the non generic calling convention. Here's the specialization for ThisCall::Invoker when using the native calling convention :

template<>
struct ThisCall::Invoker<true>
{
template< typename FunctionPointer >
static asFuncPtr Create(FunctionPointer functionPointer)
{
typedef typename MethodTraits< FunctionPointer >::Object ObjectType;
return asMethodPtr<sizeof(void (ObjectType::*)())>::Convert(functionPointer);
}
};

Nothing to write your mom about, just some standard asSFuncPtr creation. The only "complex" stuff is the MethodTraits class, which I can provide the code of, but you can get your own version from boost ( http://www.boost.org, look for the Type Traits library ). Now on to the hard part, let's create the implementation for the generic calling convention :

#include "autowrapper/aswrappedcall.h" // For asCallWrappedFunc, asCallWrappedFuncObjFirst, asCallWrappedFuncObjLast

template<typename FunctionPointer>
void ThisCallInvoker(asIScriptGeneric* gen)
{
FunctionPointer fp = /* We'll see how to get it afterwards, be patient ! */
asCallWrappedFunc(fp, gen);
};

template<>
struct ThisCall::Invoker<false>
{
template< typename FunctionPointer >
static asFuncPtr Create(FunctionPointer functionPointer)
{
return asFUNCTION(ThisCallInvoker<FunctionPointer>);
}
};

Okay. That was the hard part. Not so hard right ? Well hold on a minute, how the hell does this work ? Well, when your friend the compiler stumbles upon ThisCallInvoker<FunctionPointer>, he generates a stub function that uses the autowrapper add-on to generate a marshalling function which will in turn call the native function. Sure, there's an overhead, but you'd have an overhead anyway if you'd have type the generic implementation yourself. The question is, where do you store the FunctionPointer ? Well, I added a Set/Get UserData to asIScriptFunction* so let's use it to store it. The asIScriptGeneric* points to the function descriptor, so we can retrieve the function pointer from there.

Your first idea, and I'm sure you'll do so, is to do : static_cast<FunctionPointer>(gen->GetFunctionUserData()); Well, I've got news for you. In C++, void* aren't able to store function pointers. Now, the standard is quite clear about that, so I'll let you have a look at it, but if you really want to do so using some clever type casting, know this : You don't screw the compiler, the compiler screws you. It *might* work on some platforms, but it will definitely crash on others so my advice is this : Be as nice with the compiler as if he were you're boss.

So, here I have two choices : Either I add a field to asCScriptFunction, which I don't want to because I'm not WitchLord and I don't want to be intrusive, or I do a simple proxy object to hold the function pointer. I'll go with this approach :

If you have boost, use the Any type. If you don't, this will do the trick :

struct FpHolder
{
virtual ~FpHolder() {}
};

template< typename FunctionPointer >
class FpHolderImpl : FpHolder
{
public:
FpHolderImpl(FunctionPointer fp)
: m_fp(fp)
{}

FunctionPointer GetFp() const { return m_fp; }

private:
FunctionPointer m_fp;
}

Again, I'm inheriting from a base class since I want to store them somewhere since I'll need to free them at some point ( when the engine is destroyed or when the application quits, for example. ) Now, if you want to leak them and simple have a FpHolder template class without the base class, be my guest, but know that I would never hire you :D

So, the ThisCallInvoker looks like this :

template<typename FunctionPointer>
void ThisCallInvoker(asIScriptGeneric* gen)
{
asCallWrappedFunc(static_cast< FpHolderImpl< FunctionPointer > >(gen->GetFunctionUserData())->GetFp(), gen);
};

If you run that at this point, you'll crash as we have not set the user data on the function. Let's do so now within the RegisterObjectMethod function of our registration façade.

template< typename Convention, typename FunctionPointer >
int RegisterObjectMethod(const char* object, const char* declaration, FunctionPointer functionPointer)
{
#ifdef AS_MAX_PORTABILITY
typedef FpHolderImpl< FunctionPointer > SpecificFpHolder;
SpecificFpHolder fpHolder = new SpecificFpHolder(functionPointer);
m_fpHolderDepot->Store(holder); // Keep the holders so we can eventually destroy them to avoid leaking­

int methodId = m_engine->RegisterObjectMethod(object, declaration, Convention::Invoker<false>::Create(functionPointer), asCALL_GENERIC);
if (methodId > 0)
{
m_engine->GetFunctionDescriptorById(methodId)->SetUserData(fpHolder);
}

return methodId;
#else
return m_engine->RegisterObjectMethod(object, declaration, Convention::Invoker<true>::Create(functionPointer), Convention::NATIVE_CALLING_CONVENTION);
#endif
}

Now, what's left to do ?

1) Implement the façade for RegisterGlobalFunction
2) Implement the façade for RegisterObjectBehavior
3) Implement the CDecl convention struct
4) Implement the CDeclObjFirst convention struct
5) Implement the CDeclObjLast convention struct
6) Implement the Generic convention struct

Most, if not all, of the above steps are easy to implement. If you need help, I can assist.

As I've written earlier, you'll need some modifications to autowrapper, mainly support for the CDeclObjFirst/CDeclObjLast calls and the user data on functions. If you don't have the user data on functions, you can get around by using a std::map<int, FpHolder*> and obtaining the FunctionId from the asIScriptGeneric*. You'll probably have some extra data cache misses since you'll be loading memory sections at random places ( due to the standard red-black tree implementation of the std:map ) but that'll work while you wait for the next AngelScript version.

I'm really open to comments on the above solution. It suits my needs, doesn't seem to incur too much extra cost both in terms of CPU/Memory. There's not too much code-bloating since a lot of the code is inline anyway in the end, and in any case you'd need to do some data marshalling in any cases when using the generic calling convention. The native calling convention works as before, without any changes, and no extra cost.

Any suggestions or things I could improve in your opinion ?

Pierre
Phew, that was a long post :)

This looks nice. Though I can't really comment on the details at this moment. I'd have to make some experiments with it.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

Advertisement
I'll try to extract the code that does not rely on anything from the engine in order to send you a working test case. Actually, it would be pretty easy to create an add-on for that, which I'll try to do if I have some spare time in the next few days.
Quote:
Your first idea, and I'm sure you'll do so, is to do : static_cast<FunctionPointer>(gen->GetFunctionUserData()); Well, I've got news for you. In C++, void* aren't able to store function pointers. Now, the standard is quite clear about that, so I'll let you have a look at it, but if you really want to do so using some clever type casting, know this : You don't screw the compiler, the compiler screws you. It *might* work on some platforms, but it will definitely crash on others so my advice is this : Be as nice with the compiler as if he were you're boss.


Whilst correct about the C++ standard and about some compilers, there is a guarantee made by POSIX that this will be valid and defined. Of course it your platform is not using Posix all bets are off.
I prefer staying on the standard, safe side :) But thanks for the input !

This topic is closed to new replies.

Advertisement