pinSimplifying the C++/Angelscript Binding Process

Overload Journal #95 - February 2010 + Programming Topics   Author: Stuart Golodetz
Many systems provide a scripting language. Stuart Golodetz shows how to make it easier.

Although it may seem like there's more work involved, there are sometimes significant advantages to be gained by writing your program in more than one language. The specific example I want to highlight is in the field of game development, where, if not ubiquitous, it is certainly commonplace for games to have their artificial intelligence code written in a scripting language rather than in a compiled language like C++. Why is this? The two most significant reasons are that (a) artificial intelligence coding in particular involves a lot of tweaking and tuning - tasks for which compiled languages are not ideally suited - and (b) artificial intelligence code ties in to the game design in a particularly fundamental way, so it is often written by the game designers, who may or may not be experienced programmers. If a game designer wants to implement a simple new feature, and that involves asking an already busy programmer to put it in for them, and a two-day turn-around, then the game as a whole will suffer. A further reason for some games is that the developers want their game to be 'modable', i.e. they want to make it easy for people to modify their game once it's released - whilst many mod writers are experienced programmers, it's a lot easier for people if they just have to modify a script rather than trying to build the entire system.

Incorporating a scripting language into a game is relatively straightforward, but the binding process (i.e. the means by which scripts are allowed to call C++ code, and vice-versa) is often a bit fiddly. This article is about a way I came up with to make the process of adding bindings between C++ and the scripting language I chose to use for my game - AngelScript - a bit less painful. I don't plan to talk about the overall use of AngelScript too much, but the library documentation [AngelScript] is pretty good, and there are tutorials available on the web if you're interested.

Before we start, I'd like to add the disclaimer that comparisons in this article between the original way of doing things in AngelScript and the way I'd prefer are not especially intended as a criticism of the former - AngelScript is more sophisticated as an underlying library than the simple wrapper I'm going to build here really allows for, so in some cases the complexity when using it is a necessary evil. The wrapper I'll describe was originally designed for the specific purpose of making the bits of AngelScript which I found most relevant easier to use - a general solution would take far more work.

Registering C++ functions with AngelScript

One of the first things you usually want to do when you're getting a scripting language up and running in your game is to let your scripts call an in-game function. (For instance, your scripts might need to be able to test line-of-sight between two points in the game world.) To register a function such as int f(int) in AngelScript, you have a pointer to a script engine (of type asIScriptEngine *), and make a call which looks like:

      engine->RegisterGlobalFunction("int f(int)",  
         asFUNCTION(f), asCALL_CDECL);  

This seems fairly simple, except for the fact that you have to explicitly specify the AngelScript type of the function as a string, "int f(int)", which is messy (it gets particularly annoying when the function name is longer or the type is more complicated). Ideally, it would be nicer to transform this into something like:

      myengine->register_global_function(f, "f");  

The trick to doing this lies in C++'s mechanisms for automatic type deduction. In this instance, it is possible to write, for example, the code in Listing 1:

    template <typename F>  
    void ASXEngine::register_global_function(F f,  
       const std::string& name)  
    {  
      register_global_function<F>(f, name,  
         ASXTypeString<F>(name)());  
    }  
    template <typename F>  
    void ASXEngine::register_global_function(F f,  
       const std::string& name,  
       const std::string& decl)  
    {  
      int result = m_engine->RegisterGlobalFunction(  
         decl.c_str(), asFUNCTION(f), asCALL_CDECL);  
      if(result < 0) throw ASXException(  
        "Global function " + name + " could not be  
        registered");  
    }
Listing 1

Here we get the compiler to deduce the type of the function we're trying to bind for us, then construct the appropriate AngelScript type string using template specialization. All the real work happens in the ASXTypeString template. The base template looks like Listing 2.

    struct ASXSimpleTypeString  
    {  
      std::string name, prefix, suffix;  
      explicit ASXSimpleTypeString(  
         const std::string& name_) : name(name_) {}  
 
      std::string operator()() const  
      {  
        std::ostringstream os;  
        if(prefix != "") os << prefix << ' ';  
        os << type();  
        if(suffix != "") os << ' ' << suffix;  
        if(name != "") os << ' ' << name;  
        return os.str();  
      }  
      virtual std::string type() const = 0;  
      ASXSimpleTypeString& as_param()  
      {  
        return *this;  
      }  
    };  
 
    template <typename T> struct ASXTypeString : ASXSimpleTypeString  
    {  
      explicit ASXTypeString(  
         const std::string& name_ = "")   
         : ASXSimpleTypeString(name_) {}  
      std::string type() const {  
         return T::type_string(); }  
    };
Listing 2

This is then specialized for (a) simple built-in types like bool, double, int, etc.; (b) const types; (c) pointer and reference types; (d) function types; and (e) function pointer types (see Listing 3).

    // (a)  
    // e.g. int  
    template <>  
    struct ASXTypeString<int> : ASXSimpleTypeString  
    {  
      explicit ASXTypeString(  
         const std::string& name_ = "")  
      :  ASXSimpleTypeString(name_)  
      {}  
      std::string type() const { return "int"; }  
    };  

    // (b)  
    template <typename T>  
    struct ASXTypeString<const T> : ASXTypeString<T>  
    {  
      explicit ASXTypeString(  
         const std::string& name_ = "")  
      : ASXTypeString<T>(name_)  
      {  
        this->prefix = "const ";  
      }  
      ASXTypeString& as_param() { return *this; }  
    };  

    // (c)  
    template <typename T>  
    struct ASXTypeString<T*> : ASXTypeString<T>  
    {  
      explicit ASXTypeString(  
         const std::string& name_ = "")  
      :  ASXTypeString<T>(name_)  
      {  
        this->suffix = "@";  
      }  
    };  
 
    template <typename T>  
    struct ASXTypeString<T&> : ASXTypeString<T>  
    {  
      explicit ASXTypeString(  
         const std::string& name_ = "")  
      :  ASXTypeString<T>(name_)  
      {  
        this->suffix = "&";  
      }  
      ASXTypeString& as_param()  
      {  
        this->suffix = "& out";  
        return *this;  
      }  
    };  
 
    template <typename T>  
    struct ASXTypeString<const T&> : ASXTypeString<T>  
    {  
      explicit ASXTypeString(  
         const std::string& name_ = "")  
      :  ASXTypeString<T>(name_)  
      {  
        this->prefix = "const";  
        this->suffix = "&";  
      }  
      ASXTypeString& as_param()  
      {  
        this->suffix = "& in";  
        return *this;  
      }  
    };  

    // (d)  
    // e.g. 1 argument  
    template <typename R, typename Arg0>  
    struct ASXTypeString<R (Arg0)>  
    {  
      std::string name;  
      explicit ASXTypeString(  
         const std::string& name_)  
      :  name(name_)  
      {}  
      std::string operator()() const  
      {  
        std::ostringstream os;  
        os << ASXTypeString<R>()() << ' ' <<   
           name << '(';  
        os << ASXTypeString<Arg0>().as_param()();  
        os << ')';  
        return os.str();  
      }  
    };  
 
    // (e)  
    // e.g. 2 arguments  
    template <typename R, typename Arg0,  
       typename Arg1>  
    struct ASXTypeString<R (*)(Arg0,Arg1)>   
       : ASXTypeString<R (Arg0,Arg1)>  
    {  
      explicit ASXTypeString(  
         const std::string& name_)  
      :  ASXTypeString<R (Arg0,Arg1)>(name_)  
      {}  
    };
Listing 3

Note the need to refer to prefix and suffix from ASXTypeString<T> as this->prefix and this->suffix in the above code. This is because ASXTypeString<T> is a dependent base class, and non-dependent names are not looked up in dependent base classes in standard C++ [Vandevoorde]. Using this-> makes the names dependent and causes their lookup to be delayed until the time the template is actually instantiated.

Until C++0x becomes widely implemented, it will be necessary to write specializations like this out long-hand, i.e. a specialization for functions with no arguments, 1 argument, 2 arguments, etc. This works, but is extremely tedious - variadic templates will ultimately make this a lot easier.

The type string for a function like int f(int) is built up in pieces. At the top level, the operator() for an ASXTypeString<intint)> is invoked. This invokes the operator() of an ASXTypeString<int> to get the string "int" for the return type of the function. It also invokes the operator() of an ASXTypeString<int> to get the type string for the argument in this instance, but calls as_param() on it first, because things like references translate into different AngelScript types depending on whether they appear as parameters or return types of functions. For example, a T reference appearing as a return type should be translated as "T&", whereas one appearing as a parameter should be translated as "T& out". This comes down to the specifics of AngelScript syntax, which are only mildly interesting for the purposes of this article - the key thing is that it's possible (and in this case necessary) to vary the translation depending on where the type actually appears.

Calling script functions from C++

So far, this sort of technique is mildly interesting at best. It saves us a bit of typing, but nothing more. It becomes more interesting at the point where we want to call script functions from C++. The normal AngelScript way of doing this for our function intf(int) is something like Listing 4.

    int funcID   
       = module->GetFunctionIdByDecl("int f(int)");  
    asIScriptContext *context   
       = engine->CreateContext();  
    context->Prepare(funcID);  
    int arg = 23;  
    context->SetArgDWord(0, arg);  
    context->Execute();  
    int result = context->GetReturnDWord();  
    context->Release();
Listing 4

This works, but it's a lot of hassle just to call a script function. Ideally we'd prefer something like this, where we don't have to specify the full declaration of the AngelScript function, or manually set arguments and retrieve return values:

      ASXFunction<int(int)> f   
         = mymodule->get_global_function("f", f);  
      int arg = 23;  
      int result = f(arg);  

The get_global_function() method used above is fairly easy to write. It is also possible to provide an extended version which allows us to still pass in the full declaration of the function. This is useful because there is actually more than one possible way to translate some C++ function types into AngelScript, and we may sometimes wish to explicitly override the default generated by ASXTypeString (see Listing 5).

    template <typename F>  
    ASXFunction<F> ASXModule::get_global_function(  
       const std::string& name,  
       const ASXFunction<F>&) const  
    {  
      std::string decl = ASXTypeString<F>(name)();  
      int funcID   
         = m_module->GetFunctionIdByDecl(  
         decl.c_str());  
      if(funcID < 0) throw ASXException(  
         "Could not find function with declaration "  
         + decl);  
      asIScriptContext *context  
         = m_module->GetEngine()->CreateContext();  
      return ASXFunction<F>(ASXContext(context,  
         funcID));  
    }  
 
    template <typename F>  
    ASXFunction<F> ASXModule::get_global_function_ex(  
       const std::string& decl,  
       const ASXFunction<F>&) const  
    {  
      int funcID   
         = m_module->GetFunctionIdByDecl(  
         decl.c_str());  
      if(funcID < 0) throw ASXException("Could not  
         find function with declaration " + decl);  
      asIScriptContext *context   
         = m_module->GetEngine()->CreateContext();  
      return ASXFunction<F>(ASXContext(context,  
         funcID));  
    }
Listing 5

The trick here is to use a dummy parameter to the function, allowing the variable in which the return value is to be stored to be passed in as an argument and its type to be automatically deduced. We've already seen the definition of the ASXTypeString template - the rest of the work is done by the ASXContext class and ASXFunction template. The former is essentially a simple wrapper around asIScriptContext (Listing 6).

    struct ASXContextReleaser  
    {  
      void operator()(asIScriptContext *context)  
      {  
        context->Release();  
      }  
    };  
 
    ASXContext::ASXContext(asIScriptContext *context,  
       int funcID)  
    :  m_context(context, ASXContextReleaser()),  
       m_funcID(funcID)  
    {
      m_context->SetExceptionCallback(  
         asMETHOD(ASXContext, exception_callback),  
         this, asCALL_THISCALL);  
    }  
 
    asIScriptContext *ASXContext::operator->() const  
    {  
      return m_context.get();  
    }  
 
    int ASXContext::execute()  
    {  
      return m_context->Execute();  
    }  
 
    int ASXContext::prepare()  
    {  
      return m_context->Prepare(m_funcID);  
    }  
 
    void ASXContext::exception_callback(  
       asIScriptContext *context)  
    {  
      int col;  
      int row   
         = context->GetExceptionLineNumber(&col);  
 
      std::cout  << "A script exception occurred: "  
         << context->GetExceptionString() << " at  
         position (" << row << ',' << col << ')'  
         << std::endl;  
    }
Listing 6

The ASXFunction template and its specializations are more interesting (Listing 7).

    template <typename F> class ASXFunction;  

    // e.g. 2 arguments  
    template <typename R, typename Arg0,  
       typename Arg1>  
    class ASXFunction<R (Arg0,Arg1)>  
    {  
    private:  
      ASXContext m_context;  
 
    public:  
      explicit ASXFunction(const ASXContext& context)  
      :  m_context(context)  
      {}  
 
      R operator()(Arg0 value0, Arg1 value1)  
      {  
        int err = m_context.prepare();  
        if(err < 0) throw ASXException(  
           "Error preparing script function  
           context");  
 
        ASXSetArgValue<Arg0>()(m_context, 0, value0);  
        ASXSetArgValue<Arg1>()(m_context, 1, value1);  
 
        err = m_context.execute();  
        if(err < 0) throw ASXException(  
           "Error executing script function");  
 
        return ASXGetReturnValue<R>()(m_context);  
      }  
    };
Listing 7

The idea here is to wrap the context preparation, argument setting, context execution and value returning together so that we can call script functions without having to worry about the intricate details each time. This is complicated by the fact that the method of setting an argument/retrieving a return value when using AngelScript depends fundamentally on the type of the argument. For this reason, both are implemented as templates in the above (argument setting is handled by ASXSetArgValue, and return value retrieval by ASXGetReturnValue). These templates are implemented by writing specializations for the different types we might want to pass in/return (see Listing 8).

    template <typename T> struct ASXSetArgValue  
    {  
      void operator()(const ASXContext& context,  
         int arg, T& value) const  
      {  
        context->SetArgObject(arg, &value);  
      }  
    };  
 
    template <typename T> struct ASXSetArgValue<T*>  
    {  
      void operator()(const ASXContext& context,  
         int arg, T *value) const  
      {  
        context->SetArgObject(arg, value);  
      }  
    };  
 
    template <> struct ASXSetArgValue<double>  
    {  
      void operator()(const ASXContext& context,  
         int arg, double value) const  
      {  
        context->SetArgDouble(arg, value);  
      }  
    };  
    template <> struct ASXSetArgValue<int>  
    {  
      void operator()(const ASXContext& context,  
         int arg, int value) const  
      {
        context->SetArgDWord(arg, value);  
      }  
    };  
 
    template <typename T> struct ASXGetReturnValue  
    {  
      T operator()(const ASXContext& context) const  
      {  
        return *static_cast<T*>(  
           context->GetReturnObject());  
      }  
    };  
 
    template <typename T> struct  
       ASXGetReturnValue<T*>  
    {  
      T *operator()(const ASXContext& context) const  
      {  
        return static_cast<T*>(  
           context->GetReturnObject());  
      }  
    };  
 
    template <> struct ASXGetReturnValue<double>  
    {  
      double operator()(  
         const ASXContext& context) const  
      {  
        return context->GetReturnDouble();  
      }  
    };  
 
    template <> struct ASXGetReturnValue<int>  
    {  
      int operator()(const ASXContext& context) const  
      {  
        return context->GetReturnDWord();  
      }  
    };  
 
    template <> struct ASXGetReturnValue<void>  
    {  
      void operator()(const ASXContext&) const  
      {}  
    };
Listing 8

With these templates in place, we can then easily acquire a handle to script functions and call them in the usual C++ fashion.

Conclusion

Investing the time to write wrappers like these up-front makes using a scripting language in your program really easy. Of course, there's plenty more we could do - in particular, AngelScript allows you to register C++ types to be used in scripts, and there's a fair amount of work associated with wrapping those sensibly (anyone who is interested is very welcome to email me for the code). The basic idea is much the same, however (and indeed similar ideas can be applied when you're wrapping other scripting languages).

The take-home lessons from this article as a whole are two-fold: firstly, there can be good reasons for developing your program in more than one language, particularly if you need to make it easy for team members who are potentially less experienced to customise functionality without going through you to do it; secondly, it doesn't have to be a particularly painful process. If you think your current project could benefit from scripting, but you're put off because it seems hard to integrate into your existing code, I'd encourage you to take another look.

References

[AngelScript] http://www.angelcode.com/angelscript/sdk/docs/manual/index.html

[Vandevoorde] David Vandevoorde and Nicolai M Josuttis, C++ Templates: The Complete Guide, pp.136-9.

Overload Journal #95 - February 2010 + Programming Topics