🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Script Interface Mini Library

Started by
5 comments, last by Deyja 18 years, 11 months ago
I just recently forked my script interfacing code out into it's own library so I could use it in multiple projects. It binds three seperate things together - AngelScript, my Preprocessor, and the Auto Binding tool I posted here some time ago. I'm planning on releasing it as whole instead sticking the auto binder up alone. Take a look. WL - I especially want to know if I'm using Angelscript in some wrong/inefficient way. ScriptManager.h

#ifndef JM_SERVER_SCRIPTMANAGER_H
#define JM_SERVER_SCRIPTMANAGER_H

#include <string>
#include <Preprocessor.h>

namespace ScriptInterface
{
	bool Initialize(Preprocessor::FileSource*,Preprocessor::OutStream*);
	void Destroy();

	void DiscardScript(const std::string&);

	bool ExecuteFunction(const std::string& script, const std::string& func);
	bool ExecuteString(const std::string& script);

};

#endif


ScriptManager.cpp

#include "../scriptinterface.h"
#include <angelscript.h>
#include <iostream>
#include <system/vfs.h>
#include <preprocessor/preprocess.h>
#include <cassert>
#include <sstream>


#ifdef JM_SCRIPTINTERFACE_USEBOOST
#include <boost/regex.hpp>
#include <boost/lexical_cast.hpp>
#endif

#define ERR(a) if (a < 0) return false;

namespace
{
	asIScriptEngine* engine = 0;
	asIScriptContext* ctx = 0;
	Preprocessor::FileSource* file_source = 0;
	Preprocessor::OutStream* error_stream = 0;
	Preprocessor::LineNumberTranslator LNT;
	int errorcode;

#ifdef JM_SCRIPTINTERFACE_USEBOOST

	std::string regex_expression = "^([a-zA-Z._\\/]+)[[:space:]]+\\((\\d+)";
	boost::regex expression(regex_expression);

	bool ParseErrorMessage(std::string& msg, int& num)
	{
		std::string::const_iterator start, end;
		start = msg.begin();
		end = msg.end();

		boost::match_results<std::string::const_iterator> results;
		boost::match_flag_type flags = boost::match_default;
		if (!boost::regex_search(start, end, results, expression, flags)) return false;
		std::string num_str = std::string(results[2].first, results[2].second);
		msg = std::string(results[0].second,end);

		num = boost::lexical_cast<int>(num_str);
		return true;
	}

#endif

	class ErrorTranslator: public asIOutputStream
	{
	public:
		virtual void Write(const char* text)
		{
			//Parse error message...
			if (!error_stream) return;

#ifdef JM_SCRIPTINTERFACE_USEBOOST

			int lnumber;
			std::string msgtext = std::string(text);
			if (ParseErrorMessage(msgtext,lnumber))
			{
				(*error_stream) << LNT.ResolveOriginalFile(lnumber) << " ("
					<< LNT.ResolveOriginalLine(lnumber)	<< msgtext;
			} else {
				(*error_stream) << std::string(text);
			}
		}
#else
			(*error_stream) << std::string(text);
		}
#endif
	};

	ErrorTranslator as_error_stream;

	class ESFileSource: public Preprocessor::FileSource
	{
	private:
		std::string str;
		bool first_time;
	public:
		ESFileSource(const std::string& s) 
			: str(s), first_time(true) {}

		virtual bool LoadFile(const std::string& filename, std::vector<char>& into)
		{
			if (first_time)
			{
				first_time = false;
				char* d_end = &str[str.length()-1];
				++d_end;
				into.insert(into.end(),&str[0],d_end);
				return true;
			} else {
				return file_source ? file_source->LoadFile(filename,into) : false;
			}
		}

	};

	class AngelStdCOut: public asIOutputStream
	{
	public:
		virtual void Write(const char* text) { std::cout << text; }
	};
};

bool ScriptInterface::Initialize(Preprocessor::FileSource* fs,Preprocessor::OutStream* os)
{
	if (!fs || !os) return false;
	engine = asCreateScriptEngine(ANGELSCRIPT_VERSION);
	RegisterStdString(engine);
	ERR(engine->CreateContext(&ctx));

	file_source = fs;
	error_stream = os;

	AngelStdCOut acout;
	AutoBind::Bind(engine,&acout);
	return true;
}

void ScriptInterface::Destroy()
{
	if (ctx) ctx->Release();
	if (engine) engine->Release();
	ctx = 0;
	engine = 0;
	file_source = 0;
	error_stream = 0;
}

void ScriptInterface::DiscardScript(const std::string& str)
{
	engine->Discard(str.c_str());
}

static int FetchFunction(const std::string& script, 
				  const std::string& func)
{
	if (!engine) return -1;
	int m_index = engine->GetModuleIndex(script.c_str());
	if (m_index < 0)
	{
		if (!file_source) return -1;
		if (!error_stream) return -1;
		Preprocessor::VectorOutStream VOS;
		
		bool error = false;
		while (true)
		{
			if (Preprocessor::preprocess(script,*file_source,VOS,*error_stream,&LNT) > 0)
			{
				error = true; break;
			}

			errorcode = engine->AddScriptSection(
				script.c_str(),
				script.c_str(),
				VOS.data(),
				static_cast<int>(VOS.size()),0);

			if (errorcode < 0) { error = true; break; }
			errorcode = engine->Build(script.c_str(),&as_error_stream);
			if (errorcode < 0) { error = true; break; }
			break;
		}
		if (error) engine->Discard(script.c_str());
	}

	int r = engine->GetFunctionIDByName(script.c_str(),func.c_str());
	if (r < 0)
	{
		//emit function-not-found error?
	}
	return r;
}

bool ScriptInterface::ExecuteString(const std::string& script)
{
	ESFileSource EFS = ESFileSource(script);
	Preprocessor::VectorOutStream VOS;
	if (Preprocessor::preprocess("ExecuteString",EFS,VOS,*error_stream) > 0)
	{
		return false;
	}
	std::string preprocessed_script = std::string(VOS.data(),VOS.size());
	int error = engine->ExecuteString(
		0,
		preprocessed_script.c_str(),
		&as_error_stream,
		&ctx,
		asEXECSTRING_USE_MY_CONTEXT);
	return (error >= 0);

}

bool ScriptInterface::ExecuteFunction(
	const std::string& script,
	const std::string& func)
{
	int f_index = FetchFunction(script,func);
	if (f_index < 0) return false;

	ERR(ctx->Prepare(f_index));
	ERR(ctx->Execute());
	return true;
}


AutoBind.h

#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
{
	//Processing and binds everything registered. Can only be called one - it destroyes
	//the list. Optional stream receives signatures of everything bound.
	void Bind(asIScriptEngine*, asIOutputStream *out = 0);
	
	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


AutoBind.cpp

#include "../scriptinterface.h"
#include "AutoBind.h"
#include <list>
#include <cassert>

namespace {

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

	class FunctionBinder: public Binder
	{
	public:
		asUPtr fptr;
		asDWORD call_conv;
		const char* sig;

		FunctionBinder(const char* str, asUPtr fp, asDWORD cc) 
			: sig(str), fptr(fp), call_conv(cc) {}
		virtual void bind(asIScriptEngine* engine)
		{
			assert(engine->RegisterGlobalFunction(sig,fptr,call_conv) >= 0);
		}

		virtual void Emit(asIOutputStream *out)
		{
			out->Write(sig);
			out->Write("\n");
		}
	};

	class TypeBinder: public Binder
	{
	public:
		asDWORD flags;
		int size;
		const char* ident;

		TypeBinder(const char* i, int s, asDWORD t) : ident(i), size(s), flags(t) {}
		virtual void bind(asIScriptEngine* engine)
		{
			assert(engine->RegisterObjectType(ident,size,flags) >= 0);
		}

		virtual void Emit(asIOutputStream *out)
		{
			out->Write("Type ");
			out->Write(ident);
			out->Write("\n");
		}
	};

	class MethodBinder: public Binder
	{
	public:
		const char* obj;
		asUPtr fptr;
		asDWORD call_conv;
		const char* sig;

		MethodBinder(const char* o, const char* s, asUPtr f, asDWORD c) 
			: sig(s), obj(o), fptr(f), call_conv(c) {}

		virtual void bind(asIScriptEngine* engine)
		{
			assert(engine->RegisterObjectMethod(obj, sig, fptr, call_conv) >= 0);
		}

		virtual void Emit(asIOutputStream *out)
		{
			out->Write("Method on type ");
			out->Write(obj);
			out->Write(" : ");
			out->Write(sig);
			out->Write("\n");
		}
	};

	class BehaviorBinder: public Binder
	{
	public:
		asDWORD behave;
		asUPtr fptr;
		asDWORD call_conv;
		const char* sig;

		BehaviorBinder(asDWORD b, const char* s, asUPtr f, asDWORD c) 
			: sig(s), behave(b), fptr(f), call_conv(c) {}

		virtual void bind(asIScriptEngine* engine)
		{
			assert(engine->RegisterGlobalBehaviour(behave, sig, fptr, call_conv) >= 0);
		}
	};

	class TypeBehaviorBinder: public Binder
	{
	public:
		const char* obj;
		asDWORD behave;
		asUPtr fptr;
		asDWORD call_conv;
		const char* sig;

		TypeBehaviorBinder(const char* o, asDWORD b, const char* s, asUPtr f, asDWORD c) 
			: sig(s), obj(o), behave(b), fptr(f), call_conv(c) {}

		virtual void bind(asIScriptEngine* engine)
		{
			assert(engine->RegisterObjectBehaviour(obj, behave, sig, fptr, call_conv) >= 0);
		}
	};


	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, asIOutputStream *out)
{
	while (!GetBinder().empty())
	{
		if (out) GetBinder().front()->Emit(out);
		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 AutoBind::BindType(const char* o, int s, asDWORD f)
{
	PushFrontBinder(new TypeBinder(o,s,f));
}

void AutoBind::BindBehavior(asDWORD b, const char* s, asUPtr f, asDWORD c)
{
	PushBackBinder(new BehaviorBinder(b,s,f,c));
}

void AutoBind::BindTypeBehavior(const char* o, asDWORD b, const char* s, asUPtr f, asDWORD c)
{
	PushBackBinder(new TypeBehaviorBinder(o,b,s,f,c));
}

void AutoBind::BindMethod(const char* o, const char* s, asUPtr f, asDWORD c)
{
	PushBackBinder(new MethodBinder(o,s,f,c));
}


Don't try and compile it; it won't unless you have Boost plus some of my own random headers. Also; it doesn't rely on boost for much. Boost support can be compiled out, and all you lose is the translation of error messages from what AngelScript reports to the original file and line.
Advertisement
Sorry for the late answer.

This looks pretty good. Although, perhaps a bit specialized. There is for example no way to pass parameters to the script functions, nor retrieve the returned value.

Your FetchFunction(), doesn't verify the function signature. I suggest that you use GetFunctionIDByDecl() instead. It will make sure you get at most 1 match, and that it is the exact signature that you want.

Also, your ExecuteFunction() should probably try to cache the function ID, especially if the same function is called multiple times. GetFunctionIDByDecl() is quite expensive.

When calling Execute() or ExecuteString() it is not enough to verify if the return value is negative. There are several positive return values that mean different things. I prefer verifying if the return value is equal to asEXECUTION_FINISHED, which would mean that everything is ok and the script has finished. If it is not equal to that value, other values are verified, e.g. asEXECUTION_EXCEPTION, asEXECUTION_SUSPENDED, etc.

Regards,
Andreas

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: Sorry for the late answer.

It's almost as if everyone went on vacation at once.

Quote: This looks pretty good. Although, perhaps a bit specialized. There is for example no way to pass parameters to the script functions, nor retrieve the returned value.

That's usually how these things start. Like all my other 'satallite libraries', it was part of something else, until I needed it somewhere else too. Now, it gets made more and more generic as I get it to work with the next project without breaking the first.
I've got some ideas for passing the parameters. I'll probably create a class to encapsulate a parameter list, and give it templated functions for pushing parameters on. The class would automate the process of creating and filling a buffer to pass to angelscript. The reason they aren't there now is because it's rather generic. Parameters can only be passed if the application knows the signature of the function it is calling at compile time. In the project this was in, I just assumed every function had the signature void f().

Quote: Your FetchFunction(), doesn't verify the function signature. I suggest that you use GetFunctionIDByDecl() instead. It will make sure you get at most 1 match, and that it is the exact signature that you want.
Also, your ExecuteFunction() should probably try to cache the function ID, especially if the same function is called multiple times. GetFunctionIDByDecl() is quite expensive.

I don't think that would work. I don't have the declaration; just the name. I could assume the declaration is void name(), and thus make sure I get the right function though.

Quote: When calling Execute() or ExecuteString() it is not enough to verify if the return value is negative. There are several positive return values that mean different things. I prefer verifying if the return value is equal to asEXECUTION_FINISHED, which would mean that everything is ok and the script has finished. If it is not equal to that value, other values are verified, e.g. asEXECUTION_EXCEPTION, asEXECUTION_SUSPENDED, etc.

I'd have to supply a method for resuming scripts too. It's really designed to ignore most of angelscripts advanced features.

I just had a brainstorm about how to build and pass parameter lists at runtime, based on strings that list the parameter's values. I also need to give the scripts the ability to call another script function with nothing but the script filename and the func name. I would need somesort of context stack; or the ability to 'commandeer' a context. Basically, since the context is already there, I should be able to prepare it and use it - without disturbing the stuff already on it's stack! Preparing a context that is already in use would have the effect of saving it's current state to it's stack, then executing the new function on that stack. When this latest execution finished, the context would be back in the state it was before prepare was called.
Something like this would work then -
script.ass (Yes I am using that extension. :D)void foo() { CallScriptFunction("script.ass","bar"); }void bar() { }


the applicationcontext* ctx = createcontext();void CallScriptFunction(std::string script, std::string func){  func_id = getthefunctionidsomehow();  ctx->prepare(func_id);  ctx->execute();}int main(){   CallScriptFunction("script.ass","foo");}


This won't work now because 'ctx' will already be executing the second time CallScriptFunction is called.
That's true. I too usually start with code specific to one project and then go from there.

Actually, you do have the declaration. I mean, how would the application call a scripting function if it doesn't know what parameters to pass? The application and scripts must agree upon an interface that will be used, and this interface must be defined at compile time. In your case, you know that the declaration is "void somename()", in other cases it may be "float docalculation(float, float)", etc.

I can't think of any situation where the application must dynamically determine the parameters in order to call the function. How would the application know where to get the parameter values from? And what to do with the return value?

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

I'm thinking specifically of the single case that I actually need - the case where the script wants to call another function in a another script, the same way the application would; with the script filename and the name of the function. This is what I want that context-reuse thing for. It would replace a stack of contexes. Theres no immediate need; there are other ways to solve the same problem that don't require the scripts to need to be able to do this.
Anyway, since angelscript just essentially takes an array of chars for it's function parameters, I can build a parameter list of built-in types from a string like "int:15,float:4,string:\"Hello World!\"" really easy.
As for the application knowing the expected function signature; I can't think of any way to do that generically. The 'execute function' function would have to take some unknown number of parameters. The more I think about it the more I think I should just forget about parameter passing.
How about doing it the same way as AngelScript, with Prepare, SetArgs, and Execute? You could still use your simpler ExecuteFunction() for the cases where no parameters are needed.

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

I don't really want to make it too complicated for scripts to use the interface.

This topic is closed to new replies.

Advertisement