2005/02/11, Andreas Jönsson
Before registering a class with AngelScript you need to decide how the script engine should interact with it. Basically there are two questions that needs to be answered:
If you answered no on the first question, then you'll want to register the object with a size of 0. This is useful if you wish to register an object type for interacting with singletons, or you wish to have special control over how the objects are created.
If you answered yes to the second question, then you'll need to register the addref and release behaviours for the object type. These will be used to control how many references are held by the script.
If you answered no to both questions then the script can only interact with the object if it is registered as a global property, or if a registered function or method returns a reference to the object.
If you answered yes to the first question, then you need to define the size of the object type, and perhaps even register the constructor and destructor behaviours for initializing and cleaning up the objects.
It is also possible to override how the memory is allocated for an object type, but this will not be described in this article. This can be useful if the object type is allocated from for example a memory pool, or if it is allocated from a DLL which uses another memory heap than the main application.
The first method that must be called is RegisterObjectType(), which will give the name for which the class will be identified with in the scripts.
int RegisterObjectType(const char *name, int byteSize, asDWORD flags);
The name can be either a new identifier naming the new type, or it can be the name of an already existing datatype followed by [] (array brackets), e.g. "MyClass" or "int[]". The second case means that the application wants to override the default array implementation for that specific type.
The second parameter is as the name suggests the size of the class in bytes, so normally you would use the sizeof() operator to determine this value. It is however valid to register an object type with the size of 0, which then means that the scripts may not declare local objects of that type. The scripts can still hold handles to the object type, or use application registered global objects of that type.
The third flag is perhaps the most important one, if this flag isn't correctly set it is quite possible that you will find yourself searching for bugs that corrupt the stack frame, or other horrible bugs. This flag tells AngelScript about the C++ properties of the object type, i.e. how C++ is handling the objects in situations like returns, etc.
There are three main types:
asOBJ_CLASS | The object type is a C++ class, structure, or union. |
asOBJ_PRIMITIVE | The object type is a C++ primitive, such as int, void *, MyClass *. The only primitives that aren't included by this flag is float and double. |
asOBJ_FLOAT | The object type is in reality a float or a double. |
For the asOBJ_CLASS flag there is also three important modifiers that should be combined with | (binary or):
asOBJ_CLASS_CONSTRUCTOR | The class has a defined constructor. |
asOBJ_CLASS_DESTRUCTOR | The class has a defined destructor. |
asOBJ_CLASS_ASSIGNMENT | The class has an overloaded assignment operator. |
For the modifiers there is also a shorter form, example: asOBJ_CLASS_CDA. You can find these shorter forms in angelscript.h.
It is extremely important that this flag is set correctly as it is the only way AngelScript has to know how the C++ application handles the objects, for example if the object is returned in memory, in the general registers, or in the floating point registers.
After the object type has been registered, AngelScript needs to know how an object should be moved around in memory. The important behaviours are the way an object is constructed, destroyed, and copied from one location to another.
If no constructor is registered then AngelScript will not do anything to initialize new objects of the registered type. This is fine for structures and unions that don't make any presumtion on the value of the members. But for a class that needs to allocate resources at construction it could be big problem. If the class declares virtual functions it is also important to register the constructor as the virtual function table must be initialized for the class at allocation.
If no destructor is registered then AngelScript again does nothing when the object is destroyed, e.g. when a variable of the type goes out of scope. If the object allocates resources that must be freed upon destruction then it is necessary to register the destructor. The exception is if the addref/release behaviours are registered, since the library will then expect the application to destroy the object in the release method, thus it never calls the destructor.
If no copy behaviour, or assignment behaviour as it is also called, is registered, then AngelScript simply copies the memory byte for byte. If the class needs special behaviour when being copied, for example if a new resource must be allocated for the target object, then it is again necessary to register the assignment behaviour.
All object behaviours are registered with the following method:
int RegisterObjectBehaviour(const char *datatype, asDWORD behaviour, const char *declaration, asUPtr funcPointer, asDWORD callConv);
The first parameter, datatype, should be the same string that was used in RegisterObjectType(), e.g: "MyClass", or "int[]".
behaviour should be one of the available behaviour constants declared in angelscript.h (see the reference manual). The most important behaviours are the following:
asBEHAVE_CONSTRUCT | The constructor behaviour. |
asBEHAVE_DESTRUCT | The destructor behaviour. |
asBEHAVE_ASSIGNMENT | The assignment or copy behaviour. |
asBEHAVE_ADDREF | The addref behaviour. |
asBEHAVE_RELEASE | The release behaviour. |
The declaration parameter, is used to show the parameter types and return type for the behaviour functions. The constructor and destructor should have the return type void, and no parameters. It is actually possible to register constructors that take parameters, but the default constructor must be registered without any parameters. The assignment should return a reference to the datatype in question and take a reference to the datatype, preferably a constant reference. It is also possible to register assignment behaviours that take other types in the parameter, but the default behaviour must use a reference to it's own type.
Suggested declaration strings:
asBEHAVE_CONSTRUCT | "void f()" |
asBEHAVE_DESTRUCT | "void f()" |
asBEHAVE_ASSIGNMENT | "obj &f(obj &in)" |
asBEHAVE_ADDREF | "void f()" |
asBEHAVE_RELEASE | "void f()" |
The function name used in the declaration doesn't really matter as it will not be stored by the engine. For the assignment the return value should be a reference to the actual object. The parameter type can be any type that AngelScript understand. If it is a reference to an object of the same type as the actual object, then the operator will also be used when copying the object in the VM, e.g. when passing the object by value to a function.
The fourth parameter is the actual function pointer. It is possible to register class methods directly or global C++ functions that take an extra parameter for the object pointer. It is not possible to register a class' constructor and destructor directly as C++ doesn't allow taking the pointer to these methods. For these two it is therefor necessary to write a couple of wrapper functions. My suggestion is to do it like this:
void Constructor(MyClass *obj) { new(obj) MyClass(); } void Destructor(MyClass *obj) { obj->~MyClass(); }
The placement new operator, new(void*), is declared in new.h in the standard C++ library.
To get the function pointer to the function or method you wish to register you should use the following macros:
asFUNCTION(function) | Get the function pointer of a global function. |
asFUNCTIONPR(function, p, r) | Get the function pointer of an overloaded function. p is the parameter list and r is the return type. |
asMETHOD(class, method) | Get the method pointer of a class method. |
asMETHODPR(class, method, p, r) | Get the method pointer of an overloaded class method. |
Make sure you don't use a virtual method for the constructor behaviour as one of it's task is to initialize the virtual function table pointer. AngelScript doesn't support class methods for classes with virtual inheritance. If you need to use such a class you will have to write wrapper functions for the methods you wish to register. Classes with multiple inheritance is supported though.
The final parameter to RegisterObjectBehaviour() tells AngelScript what calling convention that the function is using. For registered object behaviours the supported calling conventions are:
asCALL_THISCALL | Class method with fixed number of parameters. |
asCALL_CDECL_OBJLAST | Global cdecl function that takes the pointer to the object as the last parameter, after all declared parameters. |
asCALL_CDECL_OBJFIRST | Global cdecl function that takes the object pointer as the first parameter before any declared parameter. |
asCALL_GENERIC | Global function implementing the generic calling convention. |
The other object behaviours found in the reference manual are registered in a similar manner as described above.
Now that AngelScript knows how to properly handle the registered object type you can continue to add the interface that the script writer will have to access the object's internals such as it's properties and methods.
int RegisterObjectProperty(const char *obj, const char *declaration, int byteOffset);
The first parameter is again the name of the object type.
The seond parameter is the declaration of the property, with its datatype and name, e.g. "int value". If you want to make the property read-only simply declare it with the const modifier.
The third parameter is the byteoffset of the property in the class structure. Normally you would determine this offset by using the macro offsetof(class, property) that is declared in stddef.h.
int RegisterObjectMethod(const char *obj, const char *declaration, asUPtr funcPointer, asDWORD callConv)
This method is used to register object methods that can be called from the script. The parameters are pretty much the same as for RegisterObjectBehaviour(), except that there is no behaviour flag, and the function name used in the declaration is used by the scripts to call the method.
Sometimes it can be useful to allow the scripts to manipulate objects through operators, i.e. using + to concatenate two strings. These overloaded operator behaviours are registered with either RegisterObjectBehaviour() or RegisterGlobalBehaviour(). Binary operators are generally registered with RegisterGlobalBehaviour(), whereas unary operator are registered with RegisterObjectBehaviour(). The exception is the assignment operators that are binary but are registered with RegisterObjectBehaviour().
int RegisterGlobalBehaviour(asDWORD behaviour, const char *declaration, asUPtr funcPointer, asDWORD callConv)
The first parameter is the behaviour flag, as found in the reference manual.
The second is the function signature, where just as for RegisterObjectBehaviour() the function name doesn't matter. Observe that it is not possible to override the operators for the built-in primitives in AngelScript, at least one of the parameters must be a registered object type.
The function pointer is obtained with the same macros as earlier.
The last parameter has a different set of calling conventions available though, as we are now registering a global function.
asCALL_CDECL | Global cdecl function, that can have variable number of parameters. |
asCALL_STDCALL | Global stdcall function with fixed number of parameters. |
asCALL_GENERIC | Global function using the generic calling convention. |
Although it is possible to register a global function that takes a variable number of parameters, AngelScript will always send a fixed number of arguments as it doesn't support variable number of arguments in functions.