This chapter will show you a simple example how write EScript bindings for simple C++ objects.
We will basically just add a very simple C++ class to PADrend.
For the sake of simplicity we will just add a two dimensional bounding box, which will be described by two Vec2
instances.
This tutorial assumes, that you have a working C++ plugin project according to the tutorial Extending your Plugin with C++
Macros
The binding between C++ and EScript is done by C++ macros which are defined in EScript/EScript.h
:
ES_CONSTRUCTOR(...) // Type constructor
ES_CTOR(...) // Type constructor (short form)
ES_FUNCTION(...) // Function
ES_FUN(...) // Function (short form)
ES_MFUNCTION(...) // Type member function
ES_MFUN(...) // Type member function (short form)
In the following you find an overview of the macros and what they are used for. Afterwards the tutorial continues with a simple example of a binding.
Function definition macros
In order to define a function, you have to use these macros. Each of them comes in two flavors: long and short
The short version just uses the long version, but it modifies the last argument:
LastArg
is transformed to { return EScript::value(LastArg); }
Therefore you use the short version if you just want to do a single statement and return that result, like: new E_Obj()
. If you want to call a void function or if you want to return something else, you can use the comma operator: (someFunction(), nullptr)
This will result in calling someFunction
, but discarding any return value and return the second argument instead. Therefore this is basically a short version for this: { someFunction(); return EScript::value(nullptr); }
The first argument of every function definition macro is the typeObject
. This is an instance of EScript::Type
describing your wrapper type. Therefore this objet is typically aquired through a call to E_MyType::getTypeObject()
. The function will be added to this typeObject.
Parameters
Each definition macro has two parameters min
and max
, describing the minimal and maximal parameter count allowed for this function. If you want to allow an arbitrary number of arguments, just pass -1
as the maximal value. EScript will throw an exception if someone tries to call your function with an invalid amount of arguments.
You can access this arguments via the parameter
variable. This is a variable of the type ParameterValues
and it is basically a list or array to ObjRef
instances.
The amount of given arguments can be accessed by parameter.count()
and each argument by the index operator: parameter[0]
… parameter[n-1]
It is important to note, that the parameter array contains only ObjRef
objects. Therefore you have to cast them to a concrete type in order to use them.
Convert an parameter to:
- a basic type (like
int
,float
,string
, etc.): int value = parameter[i].toInt()
float value = parameter[i].toFloat()
- etc.
- a pointer to an EScript type (or a wrapper type):
E_TargetType* value = parameter[i].toType<E_TargetType>()
- e.g.:
E_Vec3* value = parameter[i].toType<E_Vec3>()
. - any other type:
TargetType value = parameter[i].to<TargetType>(rt)
(rt
refers to the runtime)- e.g.
Vec3* value = parameter[i].to<Vec3*>(rt)
- or
Vec3 value = parameter[i].to<Vec3>(rt)
parameter[i].to<TargetType>(rt)
will try to convert the wrapper objetc to the real object. Make sure to use the correct types, not every wrapper can be converted to a pointer, reference, etc.
If you want to test if a parameter is of a specific type, you have to convert it to a pointer type and check if it is not a nullptr
. This can be done by using toType<E_Type>()
or by using to<Type*>(rt)
. Example:
Vec2* vec = parameter[0].to<Vec2*>(rt);
if(vec != nullptr) {
// do something with this Vec2*
} else {
rt.warn("Parameter must be a Vec2");
}
Constructor
// short version
ES_CTOR(typeObject, min, max, returnExpr)
// long version
ES_CONSTRUCTOR(typeObject, min, max, {
// ...
return ...;
})
The constructor is a special function. It always must return an instance of the wrapper class. So typically you would just return something like:
EScript::create(new MyType());
- or
new E_MyType()
Static class functions
// short version
ES_FUN(typeObject, functionName, min, max, returnExpr)
// long version
ES_FUNCTION(typeObject, functionName, min, max, {
// ...
return ...;
})
With this macro you can add functions directly to the type object. The parameters are similar to the constructor, but this time, you must also define the name of the function as a string. Example:
EScript::Type* typeObject = E_MyType::getTypeObject();
//! Vec2 getXZ(Vec3) this function will extract the x and z value of the given Vec3
ES_FUNCTION(typeObject, "getXZ", 1, 1, {
Vec3 vec = parameter[0].to<Vec3>(rt);
return EScript::create( new Vec2(vec.x(), vec.z()) );
})
You could use this function from EScript like this:
var value = MinSG.MyType.getXZ(new Geometry.Vec3(1,2,3));
outln(value); // Output: Vec2(1 3)
Member functions
// short version
ES_MFUN(typeObject, targetType, functionName, min, max, returnExpr)
ES_MFUNCTION(typeObject, targetType, functionName, min, max, {
// ...
return ...;
})
A member function will have access to the underlying object. Therefore you have to specify the actual type of the real object. For the wrapper class E_MyType
this will typically be just MyType
.
In order to access the concrete object, you have to use the thisObj
pointer. You can also use the wrapper instance with the thisEObj
variable, which is an EScript::ObjPtr
.
Example from the E_Vec3
wrapper class:
//! Number|thisObj Vec3.x([value])
ES_MFUNCTION(typeObject, Vec3, "x", 0, 1, {
if(parameter.count() == 1){
thisObj->setX(parameter[0].to<float>(rt));
return thisEObj;
} else {
return thisObj->getX();
}
})
Conversion macros
The conversion macros are used to convert between the wrapper class and your actual class back and forth.
From a wrapper object to a real object
ES_CONV_EOBJ_TO_OBJ(eSourceType, targetType, expr)
This will convert a eSourceType*
to the type specified by targetType
using the expression given by expr
. The eSourceType
will always be a wrapper class, without any modifiers, like E_Geometry::E_Vec3
. And the targetType
will often be a pointer or reference to your actual type, like Geometry::Vec3*
. For small classes like Vec2
and Vec3
a conversion to the type without modifiers is also useful, but it should be avoided for larger classes. A variable called eObj
is a pointer to your eSourceType
and can be used for your conversion.
Example:
// eObj is a pointer to an E_Geometry object, therefore we first dereference it:
// *eObj is now an E_Geometry object, which is a subclass of EScript::ReferenceObject.
// This class overrides the dereference operator, therefore **eObj will return the wrapped object.
ES_CONV_EOBJ_TO_OBJ(E_Geometry::E_Vec3, Geometry::Vec3&, **eObj)
From a real object to a wrapper object
ES_CONV_OBJ_TO_EOBJ(sourceType, eTargetType, expr)
This will convert a sourceType
to an eTargetType*
by using the expression expr
. The sourceType
is your real object class with some optional modifiers, typically *
or &
. eTargetType
is the name of your wrapper class without any modifiers. A variable called obj
is available inside of your expression and it holds an object of the type given by sourceType
.
Example:
ES_CONV_OBJ_TO_EOBJ(const Geometry::Vec3&, E_Geometry::E_Vec3, new E_Geometry::E_Vec3(obj))
Example: A simple 2D Bounding Box in C++
For now we only want to add a simple class called BoundingBox2D
. Typically you would therefore add a header and a source file for this class, but because it is an extremly simple example, we just put everything in the header file. So we just create a file called BoundingBox2D.h
inside of your source folder, e.g., MyProject/src/BoundingBox2D.h
. This file will hold the actual logic of our simple bounding box:
// prevent multiple includes of this file
#ifndef MY_EXTENSION_BOUNDINGBOX2D_H_
#define MY_EXTENSION_BOUNDINGBOX2D_H_
#include <Geometry/Vec2.h>
// Never pollute the global namespace!
namespace MyProject {
// This class is just a very simple example.
// This basic bounding box is defined by two Geometry::Vec2's, describing the two extreme corners
class BoundingBox2D {
private:
Geometry::Vec2 min;
Geometry::Vec2 max;
public:
// Define some useful constructors
BoundingBox2D() : min(0.0, 0.0), max(0.0, 0.0) {}
BoundingBox2D(const BoundingBox2D& other) : min(other.min), max(other.max) {}
BoundingBox2D(const Geometry::Vec2& pmin, const Geometry::Vec2& pmax) : min(pmin), max(pmax) {}
BoundingBox2D(float minX, float minY, float maxX, float maxY) : min(minX, minY), max(maxX, maxY) {}
// Define some helper methods to get/set all values
float getMinX() { return min.x(); }
void setMinX(float v) { min.setX(v); }
float getMinY() { return min.y(); }
void setMinY(float v) { min.setY(v); }
float getMaxX() { return max.x(); }
void setMaxX(float v) { max.setX(v); }
float getMaxY() { return max.y(); }
void setMaxY(float v) { max.setY(v); }
float getWidth() { return max.x() - min.x(); }
float getHeight() { return max.y() - min.y(); }
float getArea() { return getWidth() * getHeight(); }
Geometry::Vec2 getMin() { return min; }
void setMin(Geometry::Vec2 v) { min = v; }
Geometry::Vec2 getMax() { return max; }
void setMax(Geometry::Vec2 v) { max = v; }
Geometry::Vec2 getCenter() { return (min + max) * 0.5f; }
// Define some additional features
bool contains(Geometry::Vec2 v) {
float x = v.x(), y = v.y();
return x >= min.x() && x <= max.x() && y >= min.y() && y <= max.y();
}
bool intersects(BoundingBox2D* other) {
return std::abs(getMinX() - other->getMinX()) < (getWidth() + other->getWidth()) / 2
&& std::abs(getMinY() - other->getMinY()) < (getHeight() + other->getHeight()) / 2;
}
// (In)equality operators
inline bool operator==(const BoundingBox2D& other) {
return min == other.min && max == other.max;
}
inline bool operator!=(const BoundingBox2D& other) {
return min != other.min || max != other.max;
}
};
}
#endif
As you can see, nothing fancy here. Just a plain, simple class. In order to make it accessible from EScript, we now have to create a corresponding wrapper class.
Creating EScript bindings
Next we add the following two files to our source folder folder:
E_BoundingBox2D.h
E_BoundingBox2D.cpp
The header file is always very similar. You provide a type name, a constructor and a destructor. Furthermore you declare the getTypeObject
and init
functions, which will be implemented in the source file.
// As for all header files, we should prevent multiple includes:
#ifndef E_MY_EXTENSION_BOUNDINGBOX2D_H_
#define E_MY_EXTENSION_BOUNDINGBOX2D_H_
// Include some useful EScript stuff
#include <EScript/EScript.h>
#include <EScript/Objects/ReferenceObject.h>
// include your real class
#include "BoundingBox2D.h"
namespace MyProject {
/*! A binding class must inherit from EScript::ReferenceObject<T> where T is your actual class.
* If your class gets more complex, it might be better to use EScript::ReferenceObject<Util::Reference<BoundingBox2D>> (more on that in the bigger example)
*/
class E_BoundingBox2D : public EScript::ReferenceObject<MyProject::BoundingBox2D> {
//! The human readable name of this type
ES_PROVIDES_TYPE_NAME(BoundingBox2D)
public:
//! returns the Type object for this type
static EScript::Type * getTypeObject();
//! this function is used to initialize this type, should be called from the ELibMinSGExt.cpp file
static void init(EScript::Namespace & lib);
//! most simple constructor, just forward all arguments. This will initialize an instance of your class with that parameters.
template<typename...args> explicit E_BoundingBox2D(args&&... params) : ReferenceObject_t(E_BoundingBox2D::getTypeObject(),std::forward<args>(params)...) {}
//! destructor
virtual ~E_BoundingBox2D() {}
};
}
//! All conversions must be in the public namespace!
//! Convert an EScript object to the real object, typically you just dereference it.
ES_CONV_EOBJ_TO_OBJ(MyProject::E_BoundingBox2D, MyProject::BoundingBox2D&, **eObj)
ES_CONV_EOBJ_TO_OBJ(MyProject::E_BoundingBox2D, MyProject::BoundingBox2D*, &**eObj)
//! Convert real classes to EScript binding class. typically you just create a new instance.
ES_CONV_OBJ_TO_EOBJ(MyProject::BoundingBox2D&, MyProject::E_BoundingBox2D, new MyProject::E_BoundingBox2D(obj))
ES_CONV_OBJ_TO_EOBJ(MyProject::BoundingBox2D*, MyProject::E_BoundingBox2D, new MyProject::E_BoundingBox2D(*obj))
#endif
A very important part of the header file can be found at the end: The conversion macros. Those macros are used to convert between an EScript object and the real object back and forth.
The source file of this header will now look like this:
#include "E_BoundingBox2D.h"
#include <E_Geometry/E_Vec2.h>
namespace MyProject {
//! returns an instance to an EScript::Type
EScript::Type * E_BoundingBox2D::getTypeObject() {
// you want to have this static in order to return always the same instance
// E_BoundingBox2D ---|> Object
static EScript::ERef<EScript::Type> typeObject = new EScript::Type(EScript::Object::getTypeObject());
return typeObject.get();
}
//! initMembers
void E_BoundingBox2D::init(EScript::Namespace& lib) {
EScript::Type* typeObject = getTypeObject();
// first define the class type in EScript
declareConstant(&lib, getClassName(), typeObject);
// now you should define all functions for your new type
// this is done by several macros
//! BoundingBox2D new BoundingBox2D([otherBB | min2D, max2D | minX, minY, maxX, maxY])
ES_CONSTRUCTOR(typeObject, 0, 4,{ // typeObject, minimal parameter count, maximal parameter count, function body
// Depending on the parameter count, we want to use an other constructor
// Here we can return a new BoundingBox2D, because the corresponding macro can convert it to an E_BoundingBox2D
// Of course we could also return a new E_BoundingBox2D directly
switch(parameter.count()) {
// parameterless constructor: BoundingBox2D()
case 0: return EScript::create(new BoundingBox2D());
// as already mentioned we could also do this:
// case 0: return new E_BoundingBox2D();
// Copy constructor: BoundingBox2(BoundingBox2D otherBB)
case 1: return EScript::create(new BoundingBox2D( parameter[0].to<BoundingBox2D&>(rt) ));
// Constructor using the extreme points: BoundingBox2D(Vec2 min, Vec2 max)
case 2: return EScript::create(new BoundingBox2D( parameter[0].to<Geometry::Vec2&>(rt), parameter[1].to<Geometry::Vec2&>(rt) ));
// float constructor: BoundingBox2D(float minX, float minY, float maxX, float maxY)
case 4: return EScript::create(new BoundingBox2D( parameter[0].toFloat(), parameter[1].toFloat(), parameter[2].toFloat(), parameter[3].toFloat() ));
// Something went wrong!
default:
rt.warn("new BoundingBox2D(): Wrong parameter count!");
return EScript::create(new BoundingBox2D());
}
})
// short version of the macro, the function body will be converted to this: return EScript::value( thisObj->getMinX() );
ES_MFUN(typeObject, BoundingBox2D, "getMinX", 0, 0, thisObj->getMinX())
/* This could be also written in a long version:
ES_MFUNCTION(typeObject, BoundingBox2D, "getMinX", 0, 0, {
return thisObj->getMinX();
})
*/
ES_MFUN(typeObject, BoundingBox2D, "setMinX", 1, 1, (thisObj->setMinX(parameter[0].toFloat()), thisEObj) )
/* This could be also written in a long version:
ES_MFUNCTION(typeObject, BoundingBox2D, "setMinX", 1, 1, {
thisObj->setMinX(parameter[0].toFloat());
return thisObj;
})
*/
ES_MFUN(typeObject, BoundingBox2D, "getMinY", 0, 0, thisObj->getMinY())
//ES_MFUN(typeObject, BoundingBox2D, "setMinY", 1, 1, (thisObj->setMinY(parameter[0].toFloat()), thisEObj) )
ES_MFUNCTION(typeObject, BoundingBox2D, "setMinY", 1, 1, {
return EScript::value((thisObj->setMinY(parameter[0].toFloat()), thisEObj));
})
ES_MFUN(typeObject, BoundingBox2D, "getMaxX", 0, 0, thisObj->getMaxX())
ES_MFUN(typeObject, BoundingBox2D, "setMaxX", 1, 1, (thisObj->setMaxX(parameter[0].toFloat()), thisEObj) )
ES_MFUN(typeObject, BoundingBox2D, "getMaxY", 0, 0, thisObj->getMaxY())
ES_MFUN(typeObject, BoundingBox2D, "setMaxY", 1, 1, (thisObj->setMaxY(parameter[0].toFloat()), thisEObj) )
ES_MFUN(typeObject, BoundingBox2D, "getWidth", 0, 0, thisObj->getWidth())
ES_MFUN(typeObject, BoundingBox2D, "getHeight", 0, 0, thisObj->getHeight())
ES_MFUN(typeObject, BoundingBox2D, "getArea", 0, 0, thisObj->getArea())
ES_MFUN(typeObject, BoundingBox2D, "getMin", 0, 0, EScript::create(thisObj->getMin()))
ES_MFUN(typeObject, BoundingBox2D, "setMin", 1, 1, (thisObj->setMin(parameter[0].to<Geometry::Vec2>(rt)), thisEObj) )
ES_MFUN(typeObject, BoundingBox2D, "getMax", 0, 0, EScript::create(thisObj->getMax()))
ES_MFUN(typeObject, BoundingBox2D, "setMax", 1, 1, (thisObj->setMin(parameter[0].to<Geometry::Vec2>(rt)), thisEObj) )
ES_MFUN(typeObject, BoundingBox2D, "getCenter", 0, 0, EScript::create(thisObj->getCenter()))
ES_MFUN(typeObject, BoundingBox2D, "contains", 1, 1, thisObj->contains(parameter[0].to<Geometry::Vec2>(rt)))
ES_MFUN(typeObject, BoundingBox2D, "intersects", 1, 1, thisObj->intersects(parameter[0].to<BoundingBox2D*>(rt)))
}
}
It only consists of two functions, the getTypeObject
and init
functions.
getTypeObject
First of all you will always implement the getTypeObject
function. This function must always return the same EScript type object instance. Therefore you typically just declare it statically. All functions will be later on added to this type object.
init
Next you implement the init function, which is used to actually add all wrapper functions to the type object. Therefore you first get the type object by calling getTypeObject
. Then you have to declare your class as a constant inside of the namespace, otherwise your type object won’t be usable. Then you just continue with adding all kind of functions to your type object.
Overall initialization
Each init function of your wrapper classes must be called from your EScript library initialization function. This is done by modifying your src/ELibMyProject.cpp
file. This file should include all wrapper classes and call their corresponding init functions.
The init function will get your EScript namespace as the parameter. Afterwards you will initialize all of your wrapper classes, by just calling their init functions, with your own namespace as a parameter.
// ...
#include "E_BoundingBox2D.h"
// ...
// Initializes your EScript bindings
void init(EScript::Namespace * lib) {
// initialize EScript objects
E_BoundingBox2D::init(*lib);
// ...
}
The only thing that remains, is to add your new source files to your CMakeLists.txt and compile your library.
# ...
# Add your sources here
add_library(${PROJECT_NAME} SHARED
src/Main.cpp # The main entry point for the EScript library loader
src/ELibMyProject.cpp # Initialize your EScript bindings here
src/E_BoundingBox2D.cpp
)
# ...
After loading your plugin library in EScript, you should be able to create a new 2D Bounding Box in your EScript plugin.
// ...
var bb = new ExampleProject.BoundingBox2D(0,0,2,4);
outln("2D BB Area: ", bb.getArea());
// ...