Nearly Automatic Function Registration

Started by
12 comments, last by Deyja 18 years, 10 months ago
I had a giant function in my project that did nothing but call asIScriptEngine::RegisterGlobalFunction over and over. This bugged me. My current solution is still less than perfect, but, IMHO, much better. It does not require some other cpp file to include every header that happens to contain a function I need bound to angelscript. It allows me to say in the cpp file the function is defined in 'Hey, bind this with AS!'. It works by inserting code to be executed before main() begins into the program using the constructor of a static object. This code builds a list of functions to be registered using the command pattern. Right now, it only supports global functions, but could be easily expanded. AutoBind.h

#ifndef JM_SERVER_AUTOBIND_H
#define JM_SERVER_AUTOBIND_H

#include <angelscript.h>

#define REG_NUM __LINE__

//Normally, the preprocessor pastes tokens before expanding them. REG_CAT gets around
//that problem by delaying concatanation until after expansion. I don't know how it
//works; I took this trick from boost.
#define REG_CAT(a,b) REG_CAT_I(a,b)
#define REG_CAT_I(a,b) REG_CAT_II(a ## b)
#define REG_CAT_II(a) a

//Must be in a CPP file. Inserts a call to AutoBind::BindGlobalFunction at program
//startup. Hides identifiers in anonymous namespace - safe across compilation units.
#define REGISTER_GLOBAL_FUNCTION(AS_STR,FUNC)										namespace {																		class REG_CAT(RGF,REG_NUM)														{ public: REG_CAT(RGF,REG_NUM)() {												AutoBind::BindGlobalFunction(AS_STR,FUNC); }									};																				REG_CAT(RGF,REG_NUM) REG_CAT(RGF_instance_,REG_NUM) ;							};

namespace AutoBind
{
	void Bind(asIScriptEngine*);

	class Binder
	{
	public:
		virtual void bind(asIScriptEngine*) = 0;
	};

	void PushBinder(Binder*);

	template <typename T>
	class TBinder : public Binder
	{
	public:
		std::string in_as;
		T fptr;
		TBinder(const std::string& str, T fp) : in_as(str), fptr(fp) {}
		virtual void bind(asIScriptEngine* engine)
		{
			engine->RegisterGlobalFunction(in_as.c_str(),fptr,asCALL_CDECL);
		}
	};

	//Use the function argument to determine the type of the function pointer and generate
	//the proper TBinder object
	template <typename T>
	void BindGlobalFunction(const std::string& str, T fp)
	{
		PushBinder(new TBinder<T>(str,fp));
	}

};

#endif

AutoBind.cpp

#include "AutoBind.h"
#include <list>

namespace {
	typedef std::list<AutoBind::Binder*> BinderList;
	BinderList& GetBinder() //Static creation method
	{
		static BinderList binders;
		return binders;
	}
};

void AutoBind::PushBinder(AutoBind::Binder* b)
{
	GetBinder().push_back(b);
}

void AutoBind::Bind(asIScriptEngine* engine)
{
	while (!GetBinder().empty())
	{
		GetBinder().front()->bind(engine);
		delete GetBinder().front();
		GetBinder().pop_front();
	}
}

usage

#include "AutoBind.h"

void Function() {}
REGISTER_GLOBAL_FUNCTION("void Function()",asFUNCTION(Function));

int main()
{
   //initiate script engine...

   AutoBind::Bind(pointer_to_script_engine);
}

Advertisement
The forum messed up the main macro because it mistook the macro continuation escape (\\) as an escape sequence, and removed the line breaks. I expect it will also detect those double slashes as an escape, but if it does, it will show up as just one slash, which is correct. :D The macro works without them; it is just unreadable.
This is a really good piece of code. However there are a few minor things that needs to be corrected.

You don't need to make the TBinder a template, since all function pointers are of the same type, i.e. asUPtr.

It should also be possible to set the calling convention used, i.e. asCALL_CDECL, asCALL_STDCALL, or asCALL_GENERIC.

It would also be a good idea to add the possibility of registering types with behaviours and methods in the same way.



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

Quote:You don't need to make the TBinder a template, since all function pointers are of the same type, i.e. asUPtr.


I did not know this. TBinder was a template so that I would not have to dig into the angelscript source and find out. That will greatly simplify it. Do you see any immediate reason not to limit it to using std::string?

Quote:It should also be possible to set the calling convention used, i.e. asCALL_CDECL, asCALL_STDCALL, or asCALL_GENERIC.


Left out simply because I have no immediate use for this - and I wanted to get it working in general first.

Quote:It would also be a good idea to add the possibility of registering types with behaviours and methods in the same way.


Ditto.
Well, since these declarations are meant to be made globally, then I personally prefer using string literals, i.e. const char* instead of std::string. There is just no reason for using std::string when you don't intend to manipulate the string. But this is more a personal preference so I see no reason for you to change your code.

I would very much like to see this code evolve into something more complete, that will allow you to do most if not all of the engine registration this way.

It is definitely a very good thing to be able to register the functions as close to their implementation as possible, as it makes it easier to keep the registration in sync when the implementation changes.

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

Very interesting and useful code.. I do something very similar in my code, but with the registration of objects and methods by enumerating over DLL files and calling an exported function in each DLL that registers global functions as well as calls a static function in a contained class that registers objects, methods, and properties. I use a templated base class that does most of the work, and use macros heavily so writing new objects can be done quickly and easily. The best thing is that I can work on a DLL seperate from my app and just drop it in and I have new scripting functionality without changing my main app. I was hoping to get it cleaned up enough to post it here..

For example creating a new script object in a DLL would look like this:

DLL_INTERFACE( testPlugin );class testPlugin : public CBasePlugin<testPlugin>{    public:        BEGIN_REGISTER( testPlugin )            REGISTER_METHOD( testPlugin, "int move(int,int,int)", move )            REGISTER_METHOD( testPlugin, "void add(int)", add )            REGISTER_PROP  ( testPlugin, "const int test", m_test_int )        END_REGISTER ~testPlugin();        testPlugin() : m_test_int( 0 )        {        }        int move( int x, int y, int z )        {            return 0;        }        int add( int x )        {            m_test_int += x;            return m_test_int;        }    private:        int m_test_int;};


If anyone is interested I could clean it up and post it, it has some things particular to what I'm using it for, but it would not take much to strip it down for general use it anyone thinks they might get some use out of it..

Thanks for the post Deyja I know I will find it useful..

~Scott
[size=1]'Behold! It is not over unknown seas but back over well-known years that your quest must go; back to the bright strange things of infancy and the quick sun-drenched glimpses of magic that old scenes brought to wide young eyes.'
Drop-in DLLs are something I plan to do with a GUI project; but I ran into a problem when I wanted to use controls defined in one DLL in a composite control defined in another DLL. But I don't see how registering script functions from DLLs works here. This mechanism wouldn't work across a DLL boundary. Though the actual registration is delayed until you call bind, the list of things to register is built during initialization. If the DLL didn't use the macros and instead called AutoBind::RegisterGlobalFunction from inside an init function of some kind, it would work.

I've made changes, and hit a snag. Since the order of initalization of static objects is undefined, there is no way to predict what order things will end up in the queu. This means that it could very well attempt to register an objects methods before registering that object. Additionally, you could not define functions that take a registered object type in another compilation unit and have any expectation of them being registered after the object type. In practice, the former problem is simple. My compiler, atleast, initializes globals in the order they are found in the source file. The latter problem is unpredictable and will take something more sophisticated to solve.
I'm thinking of a dependancy system. When you register a function, you can optionally tell what other objects it is dependent on, and it will not try and bind that function until those objects are bound. And, naturally, as I was typing this I thought of something much simpler. I can actually ensure that things are always registered in the same order that they appear in a source file - I am already using the line number to create unigue identifiers - it would be trivial to stick that line number in the binder class and sort based on it. Sorting sparked something and I reliezed that, while a function can be dependent on any number of types, a type is never dependent on a function. The solution is simple: When binding a function, stick it on the end of the gueu. When binding a type, stick it on the begining.

I will start with these basic macros:
REGISTER_TYPE("str; name in AS",Type,asOBJECT_TYPE)
REGISTER_TYPE_BEHAVIOR(asBEHAVIOR,"sig",asUPtr,asCALL_CONV)
REGISTER_BEHAVIOR(asBEHAVIOR,"sig",asUPtr,asCALL_CONV)
REGISTER_METHOD("type","sig",asUPtr,asCALL_CONV)
REGISTER_FUNCTION("sig",asUPtr,asCALL_CONV)

Notice I am using the terms 'Method' and 'Function' instead of 'MemberFunction' and 'Global/FreeFunction'. I like the macros short. The REGISTER_TYPE macro will not need a size; since it receives the actual type I will use sizeof directly.
#ifndef JM_SERVER_AUTOBIND_H#define JM_SERVER_AUTOBIND_H#include <angelscript.h>#define REG_NUM __LINE__#define REG_CLASS REG_CAT(REGISTER,REG_NUM)#define REG_INSTANCE REG_CAT(REGISTER_INSTANCE,REG_NUM)//Normally, the preprocessor pastes tokens before expanding them. REG_CAT gets around//that problem by delaying concatanation until after expansion. I don't know how it//works; I took this trick from boost.#define REG_CAT(a,b) REG_CAT_I(a,b)#define REG_CAT_I(a,b) REG_CAT_II(a ## b)#define REG_CAT_II(a) a//Must be in a CPP file. Inserts a call to AutoBind::XXX at program//startup. Hides identifiers in anonymous namespace - safe across compilation units.#define REGISTER_FUNCTION(SIG,FUNC,CALL_CONV)												namespace {																				class REG_CLASS																			{ public: REG_CLASS() {																	AutoBind::BindFunction(SIG,FUNC,CALL_CONV); }											};																						REG_CLASS REG_INSTANCE ;																};#define REGISTER_TYPE(OBJ,TYPE,FLAGS)														namespace {																				class REG_CLASS																			{ public: REG_CLASS() {																	AutoBind::BindType(OBJ,sizeof(TYPE),FLAGS); }											};																						REG_CLASS REG_INSTANCE ;																};#define REGISTER_METHOD(OBJ,SIG,FUNC,CALL_CONV)												namespace {																				class REG_CLASS																			{ public: REG_CLASS() {																	AutoBind::BindMethod(OBJ,SIG,FUNC,CALL_CONV); }											};																						REG_CLASS REG_INSTANCE ;																};#define REGISTER_BEHAVIOR(BEHAVIOR,SIG,FUNC,CALL_CONV)										namespace {																				class REG_CLASS																			{ public: REG_CLASS() {																	AutoBind::BindBehavior(BEHAVIOR,SIG,FUNC,CALL_CONV); }									};																						REG_CLASS REG_INSTANCE ;																};#define REGISTER_TYPE_BEHAVIOR(OBJ,BEHAVIOR,SIG,FUNC,CALL_CONV)											namespace {																				class REG_CLASS																			{ public: REG_CLASS() {																	AutoBind::BindTypeBehavior(OBJ,BEHAVIOR,SIG,FUNC,CALL_CONV); }							};																						REG_CLASS REG_INSTANCE ;																};namespace AutoBind{	void Bind(asIScriptEngine*);		void BindFunction(const char*,asUPtr,asDWORD);	void BindType(const char*,int,asDWORD);	void BindBehavior(asDWORD,const char*,asUPtr,asDWORD);	void BindTypeBehavior(const char*,asDWORD,const char*,asUPtr,asDWORD);	void BindMethod(const char*, const char*,asUPtr,asDWORD);};#endif


#include <angelscript.h>#include "AutoBind.h"#include <list>namespace {	class Binder	{	public:		virtual void bind(asIScriptEngine*) = 0;		virtual ~Binder() {}	};	class FunctionBinder: public Binder	{	public:		const char* sig;		asUPtr fptr;		asDWORD call_conv;		FunctionBinder(const char* str, asUPtr fp, asDWORD cc) 			: sig(str), fptr(fp), call_conv(cc) {}		virtual void bind(asIScriptEngine* engine)		{			engine->RegisterGlobalFunction(sig,fptr,call_conv);		}	};	class TypeBinder: public Binder	{	public:		const char* ident;		asDWORD flags;		int size;		TypeBinder(const char* i, int s, asDWORD t) : ident(i), size(s), flags(t) {}		virtual void bind(asIScriptEngine* engine)		{			engine->RegisterObjectType(ident,size,flags);		}	};	class MethodBinder: public Binder	{	public:		const char* obj;		const char* sig;		asUPtr fptr;		asDWORD call_conv;		MethodBinder(const char* o, const char* s, asUPtr f, asDWORD c) 			: obj(o), sig(s), fptr(f), call_conv(c) {}		virtual void bind(asIScriptEngine* engine)		{			engine->RegisterObjectMethod(obj, sig, fptr, call_conv);		}	};	class BehaviorBinder: public Binder	{	public:		asDWORD behave;		const char* sig;		asUPtr fptr;		asDWORD call_conv;		BehaviorBinder(asDWORD b, const char* s, asUPtr f, asDWORD c) 			: behave(b), sig(s), fptr(f), call_conv(c) {}		virtual void bind(asIScriptEngine* engine)		{			engine->RegisterGlobalBehaviour(behave, sig, fptr, call_conv);		}	};	class TypeBehaviorBinder: public Binder	{	public:		const char* obj;		asDWORD behave;		const char* sig;		asUPtr fptr;		asDWORD call_conv;		TypeBehaviorBinder(const char* o, asDWORD b, const char* s, asUPtr f, asDWORD c) 			: obj(o), behave(b), sig(s), fptr(f), call_conv(c) {}		virtual void bind(asIScriptEngine* engine)		{			engine->RegisterObjectBehaviour(obj, behave, sig, fptr, call_conv);		}	};	typedef std::list<Binder*> BinderList;	BinderList& GetBinder() //Static creation method	{		static BinderList binders;		return binders;	}	void PushBackBinder(Binder* b)	{		GetBinder().push_back(b);	}	void PushFrontBinder(Binder* b)	{		GetBinder().push_front(b);	}};void AutoBind::Bind(asIScriptEngine* engine){	while (!GetBinder().empty())	{		GetBinder().front()->bind(engine);		delete GetBinder().front();		GetBinder().pop_front();	}}void AutoBind::BindFunction(const char* s, asUPtr f, asDWORD c){	PushBackBinder(new FunctionBinder(s,f,c));}void BindType(const char* o, int s, asDWORD f){	PushFrontBinder(new TypeBinder(o,s,f));}void BindBehavior(asDWORD b, const char* s, asUPtr f, asDWORD c){	PushBackBinder(new BehaviorBinder(b,s,f,c));}void BindTypeBehavior(const char* o, asDWORD b, const char* s, asUPtr f, asDWORD c){	PushBackBinder(new TypeBehaviorBinder(o,b,s,f,c));}void BindMethod(const char* o, const char* s, asUPtr f, asDWORD c){	PushBackBinder(new MethodBinder(o,s,f,c));}


All I need to add now are properties. Ideally I want to just say 'REGISTER(XXX)' in the application and have it bound properly. Given the actual object, template meta-programming may be able to do this. This would probably preclude me from doing some of the evil things I have done in one project - that is, registering a pointer to an object as a type, then registering free functions as methods on that pointer type, so the script thinks it has an X, and does x.something, but really has a smart pointer to X and is doing something(x).
Yea, you're right, composite objects was something I never fully implemented. I got around most of the DLL boundary issues by using local factories inside the DLL for construction and the object had to be self releasing in the situation where application and DLL did not share the same CRT. I'm still just experimenting with a lot of it and have no idea how it will scale.. I'm just trying a bunch of different ideas..
[size=1]'Behold! It is not over unknown seas but back over well-known years that your quest must go; back to the bright strange things of infancy and the quick sun-drenched glimpses of magic that old scenes brought to wide young eyes.'
Of course it can be done. However, if you can just write the 'new stuff' as script functions, you can skip the DLL hell! You would need a preprocessor. I suggest mine. (It's actually about to go through -another- revision. I added ifdef/ifndef/endif just to support include guards. However, the addition of boost::path allows me to go back to preventing double inclusion automatically, like I was before. It's either that, or implement the rest of the C preprocessor - which I really don't want to do)

Also, never code while drunk. That's why I'm not going to go work on it right now.

This topic is closed to new replies.

Advertisement