An overview of AngelScript

2004/10/31, Andreas Jönsson

This article gives a quick overview of what is possible to do with the AngelScript library. I've left out any error checking to make the code more readable.

Compiling a script

With no configuration AngelScript is capable of compiling and executing simple scripts with global variables and functions. The language has the normal control statements if, while, for, etc. Variables, both global and local, can use the ordinary C++ types int, float, double, etc.

// AngelScript code

float global_var = 0;

void DoSomething()
{
  ++global_var;

  if( global_var > 10 )
    global_var = 0;
}

These scripts are compiled into byte code, that the virtual machine understands, with just a few lines of code.

AngelScript doesn't load the script files directly from disk, instead the application should pass the already loaded script file as a character string.

// C++ code

// Load the script
FILE *f = fopen("myscriptfile.as", "rb");
int length = _filelength(_fileno(f));
char *script = new char[length];
fread(script, length, 1, f);
fclose(f);

// Compile the script
engine->AddScriptSection("module", "section", script, length);
engine->Build("module");

It is possible to add several script sections, e.g. if you have more than one source that you wish to combine into one script. All of these script sections will use the same namespace.

Scripts can be compiled into different modules, where each module has its own namespace. This is especially useful when you have several scripts that use the same interface, e.g. different AI entities. Modules are compiled individually and can be exchanged without affecting the other modules in the engine.

Dynamic linking between modules

The modules can also be linked dynamically with each other after compilation, which would allow modules to call functions from other modules. The links can also be rebound while running the script, making it possible to give the script writer the ability to change script behaviour at will.

In order to dynamic linking between modules the script writer has to declare the functions he wishes to use and from which modules they should be imported. The application can then enumerate and dynamically bind these functions with the correct modules, or prevent the linking if the script writer is trying to do something that is not allowed.

// AngelScript code

// Import a function from another module
import void ImportedFunction() from "OtherModule";

// Call the imported function normally
void Test()
{
  ImportedFunction();
}
// C++ code

// Load the scripts into the modules
...

// Build all modules
engine->Build("Module");
engine->Build("OtherModule");

// Allow the first module to link functions from the other
engine->BindAllImportedFunctions("Module");

BindAllImportedFunctions() is a method that does all the binding in the most direct way. It simply finds the module and function requested and binds them. If the application has needs to translate function names, or module names, or otherwise control the linking there are other methods available for enumerating requested imports and manually bind them.

If the script engine tries to execute a function that has not been bound, a script exception will be thrown that can be detected by the application.

Executing a script function

Script functions are executed in separate contexts, allowing the application to maintain several script functions in memory at the same time. The contexts can be suspended and resumed at a later time, allowing for a semi multi-threaded execution of the script functions.

Note: The AngelScript library is not truly multi-threaded so you shouldn't have more than one context executing at the same time in different threads. This will be corrected for a future version.

// C++ code

// Do some preparation before execution
asIScriptContext *context = 0;
engine->CreateContext(&context);
int functionID = engine->GetFunctionIDByDecl("module", "void DoSomething()");

// Execute the script function
context->Prepare(functionID);
context->Execute();

// Release the context when finished with it
context->Release();

Observe how the execution is divided in three parts, the initial preparation, the execution, and the final clean up. For better performance, contexts and function IDs should be prepared this way outside of any inner loops. The Prepare() call resets the internal states of the context and allocates space for the stack.

The context interface also has methods for passing arguments to the script function and retrieving any return value.

Note: The context stack grows dynamically as needed so there is no need to guess the needed stack size. It is possible to set the maximum stack size if that is desireable.

Executing a simple script statement

Sometimes an application need to execute only a simple statement, defined at run-time by the user. One example would be a console where the user enters commands to configure the application. AngelScript supports this with the method ExecuteString(). This method executes the string using the currently compiled script code.

// C++ code

// Execute some simple statements
engine->ExecuteString("module", "DoSomething()");

More than one statement can be executed sequencially by separating them with a ;. In fact, the statements will be wrapped inside a function block so you are allowed to use conditional statements, loops, and even declare variables in the statements.

Accessing variables in the host application

The application can allow scripts to access internal variables directly simply by registering them. The variables can be registered with or without write access.

// C++ code

int counter = 5;
float time = 0;
engine->RegisterGlobalProperty("int counter", &counter);
engine->RegisterGlobalProperty("const float time", &time);

Registering a variable as constant only means the script can't change it, the host application is still allowed to alter it after registration and the scripts will see the changes.

// AngelScript code

void DoSomething()
{
  if( time > 1 )
    counter += 2;
  else
    counter++;
}

Calling functions in the host application

The application can also register functions with the engine. These functions are normal global C++ functions, there is no need to write any proxy functions.

// C++ code

#include <stdio.h>
#include <math.h>

void Print(float value);
{
  printf("%g", value);
}

engine->RegisterGlobalFunction("void Print(float)", asFUNCTION(Print), asCALL_CDECL);
engine->RegisterGlobalFunction("float sin(float)", asFUNCTION(sinf), asCALL_CDECL);
// AngelScript code

void DoSomething()
{
  float val = sin(3.141592f);

  Print(val);
}

You may also register function overloads, i.e. more than one function with the same name, but with different parameters.

Registering new types

Sometimes it can be a good idea to use a data structure or a class to group data together or to protect data. AngelScript supports this by allowing the application to register an object type, with properties and methods, i.e. its interface.

// C++ code

class CObject
{
public:
  int property;
};

engine->RegisterObjectType("object", sizeof(CObject), asOBJ_CLASS);
engine->RegisterObjectProperty("object", "int property", offsetof(CObject, property));

If the application registers the object type with a size of 0, it prevents the script from allocating this type of object on the stack.

// AngelScript code

void DoSomething()
{
  object o;

  o.property = 5;
}

Note: When registering the object type, pay special attention to how C++ handles returns with the type, sometimes objects are returned in the registers and other times they are copied to a memory location determined by the calling function. Generally objects that have a size of 8 bytes or less are returned in registers. But there are some exceptions that AngelScript isn't able to detect by itself. The last flag to RegisterObjectType() determines the the true type of the object, which can be asOBJ_CLASS, asOBJ_PRIMITIVE, or asOBJ_FLOAT. For the class type, there are also flags to tell AngelScript if the class has a registered constructor, destructor, or assignment operator.

Constructors and destructors

Some classes may need special treatment when allocated and deallocated, e.g. classes that allocates memory, or classes with virtual functions that need to initialize the virtual function table. If you want to allow scripts to allocate objects of these types on the stack you have to register the constructor and destructor behaviour for them, and perhaps also the assignment behaviour.

The constructor will be called each time a local variable of the type comes into scope. The destructor will be called when the variable goes out of scope. It is not possible to pass any parameters to the constructor. The assignment operator will be called every time an object is copied from one place in memory to another.

Unfortunately it is not possible to take the address of a constructor, nor a destructor, in C++ so you'll have to write special functions for these purposes. For simple classes that don't have any virtual functions, or virtual base classes you could just write an initialization function, but for the complex classes you have to call the class' constructor. The best way to do this is to call the new operator, passing the memory address as argument. The new operator will call the constructor for us using that memory address.

// C++ code
#include <new.h>

class CObject
{
  CObject() {}

  virtual void VFunction() {}
};

void Constructor(CObject *o)
{
  // Call the placement new operator, 
  // which in turn will call the constructor
  new(o) CObject();
}

// We must tell AngelScript that the C++ class has a constructor as it affects 
// calling conventions. If the class has a defined destructor or assignment  
// operator AngelScript must be told about those as well.
engine->RegisterObjectType("object", sizeof(CObject), asOBJ_CLASS | asOBJ_CLASS_CONSTRUCTOR);

// Register the constructor function with asCALL_CDECL_OBJLAST 
// as AngelScript will call it as if it was a class method
engine->RegisterObjectBehaviour("object", asBEHAVE_CONSTRUCT, "void f()", asFUNCTION(Constructor), asCALL_CDECL_OBJLAST);

The destructor is also easy to register as it is possible to call the destructor directly, even though it is not possible to take its address.

// C++ code

void Destructor(CObject &o)
{
  o.~CObject();
}

engine->RegisterObjectBehaviour("object", asBEHAVE_DESTRUCT, "void f()", asFUNCTION(Destructor), asCALL_CDECL_OBJLAST);

The assignment operator takes one argument, the object to be copied into the current. Both the current object and the object that is being copied are guaranteed to be correctly initialized with the constructor. If the assignment operator isn't registered, then AngelScript simply copies the object byte for byte.

// C++ code

CObject &Copy(CObject &other, CObject &self)
{
  // Do the necessary copy operation, that assures that both objects are correctly working afterwards.
  // This is especially necessary if the objects have pointers that shouldn't be shared between them.
  self = other;

  return self;
}

engine->RegisterObjectBehaviour("object", asBEHAVE_ASSIGNMENT, "object &f(const object &)", asFUNCTION(Copy), asCALL_CDECL_OBJLAST);

Observe how the constructor, destructor, and assignment behaviour were registered as global functions. asCALL_CDECL_OBJLAST tells AngelScript to send the object pointer to the function in addition to any declared parameters. We could have registered member functions instead as we will see in the next section.

Object methods

Just as it is possible to register global functions for use in the script, it is possible to register object methods. The methods is registered much the same way.

// C++ code

float CObject::Sum(float a, float b)
{
  return a + b; 
}

engine->RegisterObjectMethod("object", "float Sum(float, float)", asMETHOD(CObject, Sum), asCALL_THISCALL);
// AngelScript code

void Test()
{
  object o;

  float sum = o.Sum(1.0f, 2.0f);
}

Currently AngelScript cannot handle class methods where virtual inheritance is involved. If you want to be able to call such a method from within AngelScript you'll have to wrap the call in another function or method. Example:

// C++ code

class CObject : virtual CVirtualBase
{
  virtual float Sum(float a, float b);
};

float CObject_Sum(float a, float b, CObject &o)
{
  // Call the virtual method
  return o.Sum(a, b);
}

engine->RegisterObjectMethod("object", "float Sum(float, float)", asFUNCTION(CObject_Sum), asCALL_CDECL_OBJLAST);

Overloading operators

Should you like to register a new data type that the script writers can use in expressions, for example vectors that can be added and multiplied, you can do so by registering operator overloads for the registered object type.

// C++ code

class CVector
{
  float x, y;
};

CVector operator+(CVector &a, CVector &b)
{
  CVector c;
  c.x = a.x + b.x;
  c.y = a.y + b.y;

  return c; 
}

engine->RegisterGlobalBehaviour(asBEHAVE_ADD, "vector f(vector &, vector &)", asFUNCTIONP(operator+, (CVector &, CVector &)), asCALL_CDECL);
// AngelScript code

void Test()
{
  vector a, vector b;

  vector c = a + b;
}

Most of the operators can be overloaded, including all dual operators and assignment operators.

Arrays

AngelScript has native support for arrays, which allow the script writer to use arrays with full freedom. But to register application functions that accept arrays as arguments or return arrays, the array type must first be registered so that AngelScript and the application is using the same object type.

An array object is registered the same way an object is registered, with the difference that the object type should include the array type modifier [].

// C++ code

// Register std::vector<int>
engine->RegisterObjectType("int[]", sizeof(vector<int>), asOBJ_CLASS_CDA);
engine->RegisterObjectBehaviour("int[]", asBEHAVE_CONSTRUCT, "void f()", asFUNCTIONP(ConstructIntArray, (vector<int> *)), asCALL_CDECL_OBJLAST);
engine->RegisterObjectBehaviour("int[]", asBEHAVE_CONSTRUCT, "void f(int)", asFUNCTIONP(ConstructIntArray, (int, vector<int> *)), asCALL_CDECL_OBJLAST);
engine->RegisterObjectBehaviour("int[]", asBEHAVE_DESTRUCT, "void f()", asFUNCTION(DestructIntArray), asCALL_CDECL_OBJLAST);
engine->RegisterObjectBehaviour("int[]", asBEHAVE_ASSIGNMENT, "int[] &f(int[]&)", asMETHODP(vector<int>, operator=, (const std::vector<int> &)), asCALL_THISCALL);
engine->RegisterObjectBehaviour("int[]", asBEHAVE_INDEX, "int &f(int)", asMETHODP(vector<int>, operator[], (int)), asCALL_THISCALL);
engine->RegisterObjectMethod("int[]", "int length()", asMETHOD(vector<int>, size), asCALL_THISCALL);

// Constructor functions
void ConstructIntArray(vector<int> *self)
{
  new(self) vector<int>();
}

void ConstructIntArray(int length, vector<int> *self)
{
  new(self) vector<int>(length);
}

void DestructIntArray(vector<int> *self)
{
  self->~vector();
}
// AngelScript code

void Test()
{
  int[] a(2);

  a[0] = 123;
  a[1] = a[0];
}

Exception handling

The above example with the array isn't very secure, as no check is made to see if the script is trying to access memory outside of the array. This is a common bug that hackers frequently use to gain access to systems. In order to protect your application (and the users' systems) you should add bounds checking. In order to let the user know why his code doesn't work it is a good idea to throw an exception in case of out-of-bounds access.

// C++ code

int *ArrayIndex(int n, vector &o)
{
  if( n < 0 || n >= o.size() )
  {
    asIScriptContext *ctx = asGetActiveContext();
    if( ctx )
      ctx->SetException("Out of range");

    return 0; 
  }

  return &o.buffer[n]; 
}

When an exception like this occurs the context stops the execution and calls the registered destructor on all objects currently on the stack. Afterwards, the application can access the exception information.

// C++ code

int r = context->Execute();
if( r == asEXECUTION_EXCEPTION )
{
  printf("The script raised an exception.\n");
  printf("func: %s\n", engine->GetFunctionDeclaration(context->GetExceptionFunction()));
  printf("line: %d\n", context->GetExceptionLine());
  printf("desc: %s\n", context->GetExceptionString());
}

Note: A future version of AngelScript may allow catching and treating exceptions in the script.