Handling Ownership When Using Scripts
This article dwells on objects management and communication between the UNIGINE scripts, external C++ classes and C++ part of UNIGINE Engine (internal C++ classes).
See Also#
UNIGINE Ownership System#
UNIGINE has its own optimized memory allocator for faster and more efficient memory management. It works within a particular preallocated memory pool, which is shared by all of the UNIGINE modules. Custom C++ modules, which extend UNIGINE functionality with external C++ classes, use an underlying system-provided allocator, which has a separate memory pool.
Allocating memory, keeping track of those allocations and freeing memory blocks when they are no longer needed is mainly the task of managing objects. UNIGINE consists of several modules (including C++ modules mentioned above) each of which can establish the ownership of an object, meaning that it would be responsible for deallocating the memory when the object is no longer needed. By that, the object is accessible by all these modules. Ownership can also be released and transferred for another module to take responsibility and delete the unnecessary object at the right time. If failing to set only one owner that deallocates the object, memory leaks are imminent.
Internal C++ classes. Instances of these classes are low-level built-in objects, that are actually created on the C++ side when a script or a C++ module declares them, such as: mesh, body, image and so on. Such internal instances can be owned by one of the following modules:
- Scripts. These are:
- World script that contains world logic.
- Editor script that wraps editor functionality and editor GUI.
- System script that does system housekeeping and controls the system menu GUI.
An internal instance can be handled by any of the scripts (world, system or editor one), no matter whom it belongs to. The ownership matters when deleting it. - API C++ classes are wrapper classes that hide implementation of internal C++ classes and store pointers to instances of these internal classes (internal instances, in other words).
By default, ownership of such instances is handled by the Engine.
API C++ classes can be owners of the internal instances if these instances are created on the C++ side of the user application via the create() function.As instances of C++ API classes only store pointers to instances of internal C++ classes, they cannot be created and deleted via the standard new / delete operators. So they should be declared as smart pointers (Unigine::Ptr) that allow you to automatically manage their lifetime.
There is also Engine World, which is also a C++ part of UNIGINE Engine (do not confuse with the world script!). All the instances created via the new operator or the create() function are automatically added to Engine World. Engine World is responsible for loading a world with all its nodes, for managing a spatial tree and handling collisions and intersections of nodes. However, it doesn't take ownership of internal instances.
Handling Ownership in the Main Loop#
Pointers to nodes (C++/C# API) created in the Main Loop are automatically handled by the Engine preventing unexpected memory leaks and crashes. Every node created via any of the following ways is added to the current world and is deleted automatically on the world shutdown (or Engine shutdown if the corresponding lifetime management policy was selected):
// creating a new node
NodeDummyPtr nodeDummy = NodeDummy::create();
NodeReferencePtr nodeReference = NodeReference::create("object.node");
ObjectMeshStaticPtr mesh = ObjectMeshStatic::create("data/template_assets/template_material_ball/material_ball.mesh");
// loading a node
NodePtr node = World::loadNode("object.node");
// cloning a node
node->clone();
Node removal should be performed via the following code:
node.deleteLater();
Managing Ownership#
The basic principles of managing ownership of internal instances are the following:
- Whoever creates an instance of an internal C++ class will automatically become the owner of this instance. Afterwards, the module that owns the internal instance can delete it. For example:
- If you create an instance on the script side of the application via the new operator, the script will become the owner and you will be able to delete the instance by using the delete operator.
- If you create an instance on the C++ side via the create() function, you will be able to delete it via the deleteLater() function (non-ownership object), or it will be deleted as its reference counter reaches zero(ownership objects).
- When transferring ownership of an instance, it must be released from the previous owner and assigned to a new one. The order of these operations is of no importance.
- If a new body was assigned to an object via
- body.setObject()
- object.setBody()
- an object was specified in the constructor, for example, new BodyRigid(object)
- If a new shape was assigned to a body via
- body.addShape()
- shape.setBody()
- a body was specified in the constructor, for example, new ShapeSphere(body,radius)
- If a new joint was assigned to a body via
- body.addJoint()
- joint.setBody0() orjoint.setBody1()
- a body was specified in the constructor, for example, new JointFixed(body0,body1)
Script Ownership#
By default, any instance of an internal C++ class (a node, a mesh, an image and so on) created in a script is owned by this script:
NodeDummy node_1 = new NodeDummy(); // the script owns node_1
NodeDummy node_2 = new NodeDummy(); // the script owns node_2
// after adding node_2 as a child to node_1
// node_1 WILL become an owner of node_2
node_1.addChild(node_2);
// node_1 will be deleted together with node_2 as the latter is its child
delete node_1;
ObjectDummy object = new ObjectDummy(); // the script owns the object
BodyRigid body = new BodyRigid(object); // both the script and the object own the body
// remove script ownership for the body
class_remove(body);
// both the object and its body will be deleted
delete object;
Handling Ownership of Internal Instances#
Scripts can handle ownership of an instance of an internal C++ class using the following system functions:
- class_append() assigns ownership of the internal instance to the current script module. All appended instances will be automatically deleted on the script shutdown; or they can be deleted using the delete operator. For example: Here, clone() returns a new node, which is orphaned. To prevent the memory leak, a script takes ownership and safely deletes it.
// clone an existing node. The cloned node will be orphaned Node clone = node.clone(); // set script ownership for the node class_append(clone); // delete the cloned node later if necessary delete clone;
- class_manage() indicates that reference counting should be performed for the internal instance. When the number of references that point to the instance reaches 0, the memory previously allocated for it is automatically deleted, thus freeing the developer from carefully managing the lifetime of pointed-to instances. Before calling this function, the internal instance should be appended to the script.
Here, the image is automatically owned by the script, because it was created using a new operator. It will be deleted, because there are no references left to it.
// create an image that will be automatically owned by the script Image image = new Image(); // enable reference counting for the image class_manage(image); // the image will be deleted when there are no references left to it image = 0;
- class_release() removes all references to the instance of the internal C++ class. It removes even the smallest memory leaks.
// create a new node that will be automatically owned by the script NodeDummy node = new NodeDummy(); // delete the reference to the node node = class_release(node);
- class_remove() releases the ownership of the instance.
Body body = class_remove(new BodyRigid(object)); // bodies are automatically managed by objects they are assigned to ShapeSphere shape = class_remove(new ShapeSphere(body,radius)); // shapes are automatically managed by bodies they are assigned to JointFixed joint = class_remove(new JointFixed(body,body0)); // joints are automatically managed by bodies they are assigned to
- class_cast() converts the pointer to an internal instance of a given type into another type.
When converting into the other type a new internal instance isn't constructed, so you cannot delete it via the delete operator.Conversions of the pointer are unsafe and allow specifying any type, because neither the pointer nor its type is checked.If you try to delete the mesh declared in the example above via the delete operator, you will get an error. However, it you delete the node, the mesh will also be deleted.
// get a node Node node = engine.world.getNode(id); // cast the node to the ObjectMeshStatic type ObjectMeshStatic mesh = class_cast("ObjectMeshStatic",node); // call a member function of the class to which the node has been casted string name = mesh.getMeshName();
Handling Ownership of Hierarchical Nodes#
There is also a group of functions that allow you to safely handle hierarchical nodes together with all of their children. They can be found in the data/core/unigine.h file of UNIGINE SDK.
- Node node_append(Node node) registers script ownership of the given node and its children. Here, the script takes ownership of both my node and its child node (NodeDummy).
// load a node. The node will be automatically owned by the script Node node = engine.world.loadNode("my.node"); // ... // release script ownership of the instance // add an orphan child node to the loaded node node.addChild(class_remove(new NodeDummy())); // set script ownership for both the parent and the child nodes node_append(node);
The situation presented in the example is created intentionally in order to show that node_append() sets script ownership for the whole node hierarchy. So, for example, you don't need to call class_remove() before adding a child to a node in your application. - Node node_remove(Node node) releases script ownership of the given node and its children and casts them to their types.
// create a static mesh that will be automatically owned by the script ObjectMeshStatic mesh = new ObjectMeshStatic("samples/common/meshes/statue.mesh"); // release script ownership node_remove(mesh);
- void node_delete(Node node) deletes the parent node together with its children. Before calling this function, the node should be appended for the script to take ownership.
Here, both the node and its child will be deleted.
// load a node. The node will be automatically owned by the script Node node = engine.world.loadNode("my.node"); // ... // release script ownership of the node // add an orphan child node to the loaded node node.addChild(class_remove(new NodeDummy())); // set script ownership for the node and its child node_append(node); // perform something here // delete the node and its child node_delete(node);
The situation presented in the example is created intentionally in order to show that node_delete() deletes the whole node hierarchy. So, for example, you don't need to call class_remove() before adding a child to a node in your application. - Node node_clone(Node node) clones the node. Herewith, a new instance of the internal C++ class is created. Like for all other clone() functions, the created instance is orphaned and its ownership is to be passed to some module. For example, this function is useful when the node is an object with a body and joints. A usual clone() function does not recreate connected joints.
Here, the first mesh object is automatically owned by the script as it is created using new operator, while the copied one should be appended manually.
// create a node that is automatically owned by the script ObjectMeshStatic mesh = new ObjectMeshStatic("samples/common/meshes/statue.mesh"); // set script ownership for the cloned node node_append(node_clone(mesh));
Functions for non-hierarchical nodes:
- Node node_cast(Node node) allows you to safely convert the given base node to its derived type. When converting into the other type a new internal instance isn't constructed, so you cannot delete it via the delete operator.After downcasting, the node can call member functions of the ObjectMeshStatic class. If you try to delete the mesh declared in the example above via the delete operator, you will get an error as there is no such internal instance. However, if you delete the node, the mesh will also be deleted.
// get a node that will be automatically owned by the script Node node = engine.world.getNode(id); // cast the node to ObjectMeshStatic ObjectMeshStatic mesh = node_cast(node); // call a member function of the ObjectMeshStatic class int num_targets = mesh.getNumSurfaceTargets(0);
Pass Ownership Between Scripts#
Ownership of an internal instance can be passed between the scripts: world, system and editor ones. For the scripts to interact, use the following functions:
- One of the engine.world.call() to call functions of the world script.
- One of the engine.system.call() to call functions of the system script.
- One of the engine.editor.call() to call functions of the editor script.
For example, you can create an internal instance in the world script and then pass its ownership to the system script:
- Create an instance in the world script and release world script ownership of this object, so that the system script can grab its ownership. Then pass the object to the system script function via the engine.system.call() function.
int init() { // 1. Create a new dummy object. The internal instance to which the dummy object points will be created automatically ObjectDummy dummy = new ObjectDummy(); // 2. Release world script ownership of this object class_remove(dummy); // 3. Pass the object to the system script function engine.system.call("receive_dummy_ownership",dummy); return 1; }
- In the system script function, set the system script ownership for the received orphan instance. Now this instance can be deleted using the delete operator; otherwise, the instance will be automatically deleted on the system script shut down.
void receive_dummy_ownership(ObjectDummy dummy) { // 1. Set system script ownership of the dummy object constructed in the world script class_append(dummy); // 2. The system script can delete the internal instance to which the dummy object points. Or it will be deleted on the system script shut down delete dummy; }
C++ Ownership#
There are different variants to create and handle ownership of the internal instance:
- Create an instance of an internal C++ class by the script and pass it to be received by the function defined on the C++ side:
- Create a smart pointer to an instance of the C++ API class and pass it to the script:
Receive Instance as Smart Pointer#
The first option is the following: a node that is created and handled by the world script is passed to the C++ function that receives a smart pointer.
- Create a custom function on the C++ side, which will receive a NodePtr smart pointer. Then a function needs to be registered with the UNIGINE interpreter, so that the script could call it at its run time.
//main.cpp #include <UnigineEngine.h> #include <UnigineInterpreter.h> #include <UnigineInterface.h> /* */ using namespace Unigine; /* */ // 1. Create an external function that receives a smart pointer NodePtr void my_node_set(NodePtr node) { // 1.1. Call the node member function exposed through the C++ API node->setTransform(translate(vec3(1.0f,2.0f,3.0f))); } /* */ int main(int argc,char **argv) { // 2. Register the function for export into UNIGINE Interpreter::addExternFunction("my_node_set",MakeExternFunction(&my_node_set)); // 3. Initialize the engine. It will be shut down automatically EnginePtr engine(argc,argv); // 4. Enter the engine main loop engine->main(); return 0; }
- Create a node in the world script and call the registered C++ function. The ownership of the node will belong to the script, so only it could call the delete operator to destroy the node.
// world script (myworld.usc) int init() { // 1. Create a new dummy node, so it could be passed to the external C++ function Node node = new NodeDummy(); // 2. Call the registered C++ function my_node_set(node); // 3. Delete the node if no longer needed; otherwise, the node will be automatically deleted by the script shutdown delete node; return 1; }
Receive a Released Instance as Smart Pointer#
In this case, a node with released script ownership is passed to the C++ function that receives it as a smart pointer, so that it could be deleted in accordance with the reference counter.
- Create a custom function on the C++ side, which will receive an ImagePtr smart pointer. Then a function needs to be registered with UNIGINE interpreter, so that the script could call at in its run time.
#include <UnigineEngine.h> #include <UnigineInterpreter.h> #include <UnigineInterface.h> /* */ using namespace Unigine; /* */ // 1. Create an external function that receives a script image as a smart pointer ImagePtr void my_image_set(ImagePtr image) { // 1.1. Grab ownership of the received pointer image->setOwner(true); // 1.2. Call the image member function exposed through the C++ API image->create2D(256,256,IMAGE_FORMAT_R8); // 1.3. Delete the image if necessary (setting the reference counter to zero) image = nullptr; } /* */ int main(int argc,char **argv) { // 2. Register the function for export into UNIGINE Interpreter::addExternFunction("my_image_set",MakeExternFunction(&my_image_set)); // 3. Initialize the engine. It will be shut down automatically EnginePtr engine(argc,argv); // 4. Enter the engine main loop engine->main(); return 0; }
- Create an image in the world script, release script ownership and call the registered C++ function. The ownership of the image will no longer belong to the script, so you don't need to call delete operator to destroy the image.
int init() { // 1. Create a new image, so it could be passed to the C++ function Image image = new Image(); // 2. Release script ownership so that it can be grabbed by the C++ function class_remove(image); // 3. Call the registered C++ function my_image_set(image); return 1; }
Receive Instance of Specific Type#
This variant is similar to the first one: a node that is created and handled by the world script is passed to a C++ function that receives it as a smart pointer of a specific type. Here, "specific type" means a C++ API class that is wrapped with a smart pointer (for example, ObjectMeshStaticPtr, DecalDefferedMeshPtr and so on).
- Create a custom function on the C++ side, which will receive an ObjectMeshDynamicPtr smart pointer. The external function should be registered to be called by the script.
//main.cpp #include <UnigineEngine.h> #include <UnigineInterpreter.h> #include <UnigineObjectMeshDynamic.h> #include <UnigineInterface.h> #include <UnigineLog.h> /* */ using namespace Unigine; /* */ // 1.0. Create an external function that receives a smart pointer ObjectMeshDynamicPtr void my_object_update(ObjectMeshDynamicPtr object,float time) { // 1.1. Call the member function of the ObjectMeshDynamic object->updateSurfaceBegin(0); object->updateSurfaceEnd(0); Log::message("Surface indices was updated\n"); } /* */ int main(int argc,char **argv) { // 2. Register the function for export into UNIGINE Interpreter::addExternFunction("my_object_update",MakeExternFunction(&my_object_update)); // 3. Initialize the engine. It will be shut down automatically EnginePtr engine(argc,argv); // 4. Enter the engine main loop engine->main(); return 0; }
- On the script side, create an object and pass it to the external function. After that, the registered external C++ function call receive it, when called by the script. Ownership remains by the script, so it can delete the object.
// world script (myworld.usc) int init() { // 1. Create a new ObjectMeshDynamic Object object = new ObjectMeshDynamic(); // 1.1. Call the member function object.setMaterialPath("materials/mesh_base","*"); // 3. Call the registered external C++ function my_object_update(object,engine.game.getTime()); // 4. The script can delete the object, as it was the one that allocated it delete object; return 1; }
Receive Instance as Variable#
The third variant is to create an internal instance in the script (for example, the image) and pass it to the C++ function that receives a variable. The external function is called by the script thereafter.
- Create a custom function on the C++ side, which will receive a variable. The variable is cast to the ImagePtr smart pointer type using the dedicated function getImage(). After that, the C++ function should be registered to be called by the script.
You should set the script runtime when calling the getImage() function: pass the pointer to the current interpreter via the Unigine::Interpreter::get() function.
//main.cpp #include <UnigineEngine.h> #include <UnigineInterpreter.h> #include <UnigineImage.h> #include <UnigineInterface.h> /* */ using namespace Unigine; /* */ // 1. Create an external function that receives a variable const char* my_image_get(const Variable &v) { // 1.1. Cast the received variable to the ImagePtr type ImagePtr image = v.getImage(Interpreter::get()); // 1.2. Call the image member functions exposed through C++ API return image->getFormatName(); } /* */ int main(int argc,char **argv) { // 2. Register the function for export into UNIGINE Interpreter::addExternFunction("my_image_get",MakeExternFunction(&my_image_get)); // 3. Initialize the engine. It will be shut down automatically EnginePtr engine(argc,argv); // 4. Enter the engine main loop engine->main(); return 0; }
There also exists another way to cast the variable into the image pointer type using the VariableToType() function:
//main.cpp ... // 1. The same external function const char* my_image_get(const Variable &v) { // 1.1. Another way of casting the variable value into ImagePtr: ImagePtr image = VariableToType<ImagePtr>(Interpreter::get(),v).value; return image->getFormatName(); } ...
- On the script side, the image needs to be created and simply passed to the C++ function. The image will be converted into the ImagePtr smart pointer automatically.
//world script (myworld.usc) int init() { // 1. Create a new image Image image = new Image(); // 1.1. Specify parameters of the image and fill it with black color image.create2D(256,256,IMAGE_FORMAT_R8); // 2. Call the registered external function and simply pass the image to it my_image_get(image); // 3. The script can delete the image explicitly, if necessary delete image; return 1; }
Create and Pass Instance as Smart Pointer#
Smart pointers allow the C++ function not only to receive instances of internal C++ classes created by the script, but also create such instances.
Here, a script calls an external function that creates a new image using ImagePtr.
- In the external C++ function, declare ImagePtr, call API function create() and pass the pointer to the script. However, if you just pass the pointer, it will be dangling, i.e. it will not point to the valid image object as it isn't visible outside the external function scope. To avoid it, you should pass ownership of the pointer to the script via calling setOwner(false). Then a custom function is registered to be called on the script side.
//main.cpp #include <UnigineEngine.h> #include <UnigineInterpreter.h> #include <UnigineImage.h> #include <UnigineInterface.h> /* */ using namespace Unigine; /* */ // 1. Create an external function that returns an ImagePtr smart pointer ImagePtr my_image_create_0() { // 1.1. Declare an image smart pointer ImagePtr image; // 1.2. Create an image in the UNIGINE memory pool through the ImagePtr smart pointer image = Image::create(); // 1.3. Specify parameters of the image and fill it with black color image->create2D(128,128,Image::FORMAT_RG8); // 1.4. Pass ownership of the pointer to the script image->setOwner(false); return image; } /* */ int main(int argc,char **argv) { // 2. Register the function for export into UNIGINE Interpreter::addExternFunction("my_image_create_0",MakeExternFunction(&my_image_create_0)); // 3. Initialize the engine. It will be shut down automatically EnginePtr engine(argc,argv); // 4. Enter the engine main loop engine->main(); return 0; }
- Call the registered C++ function. The image it returns can be simply passed to the script, as conversion from the ImagePtr into the Image will be done automatically. As ownership of the image has been passed from the external function to the script, it can be deleted by the script any time (or it will be automatically deleted on engine shut down).
//world script (myworld.usc) int init() { // 1. Call the external function in the script Image image = my_image_create_0(); // 2. Set the script ownership for the image class_append(image); // 3. The script will automatically handle the ImagePtr returned by the C++ function as a simple image log.message("%s\n",image.getFormatName()); // 4. Delete the image if necessary delete image; return 1; }
Create and Pass Instance as Variable#
In this variant, the external function creates a new image as a variable and then the script calls this external function.
- In the C++ function, declare an ImagePtr, call the API function create() to create a smart pointer, which will allocate an internal UNIGINE image, and set it to the variable using the dedicated function setImage().
You should set the script runtime when calling the setImage() function: pass the pointer to the current interpreter via the Unigine::Interpreter::get() function.
//main.cpp #include <UnigineEngine.h> #include <UnigineInterpreter.h> #include <UnigineImage.h> #include <UnigineInterface.h> /* */ using namespace Unigine; /* */ // 1. Create an external function that returns a variable Variable my_image_create_1() { // 1.1. Declare an image smart pointer ImagePtr image; // 1.2. Create an image in the UNIGINE memory pool image = Image::create(); // 1.3. Specify parameters of the image and fill it with black color image->create2D(128,128,Image::FORMAT_RG8); // 1.4. Define the variable Variable v; // 1.5. Set the image smart pointer to the variable v.setImage(Interpreter::get(),image); return v; } /* */ int main(int argc,char **argv) { // 2. Register the function for export into UNIGINE Interpreter::addExternFunction("my_image_create_1",MakeExternFunction(&my_image_create_1)); // 3. Initialize the engine. It will be shut down automatically EnginePtr engine(argc,argv); // 4. Enter the engine main loop engine->main(); return 0; }
Another way to set the ImagePtr smart pointer to the variable is to use the TypeToVariable construction:
//main.cpp ... // The same external function Variable my_image_create_1() { image = Image::create(); image->create2D(128,128,Image::FORMAT_RG8); // Another way of setting the image smart pointer to the variable: Variable v = TypeToVariable<ImagePtr>(Interpreter::get(),image).value; return v; } ...
Only core classes (like Node, Object, etc.) instances can be converted to variables via TypeToVariable. In other cases, you need to cast a class instance to the core class first. - A script calls the registered C++ function. After that, it handles the returned variable as a simple image, because conversion is done automatically. Remember, the script cannot delete the image, as the ownership belongs to the external function. The smart pointer will be automatically released when the reference count reaches zero.
//world script (myworld.usc) int init() { // 1. Call the external function in the script Image image = my_image_create_1(); // 2. The script automatically handles the returned ImagePrt as a simple image log.message("%s\n",image.getFormatName()); return 1; }
Manage Script Ownership from C++ Side#
If an instance has been created in the script and then passed as a variable to the C++ function, you can handle script ownership from this function by using the following methods of the Unigine::Variable class:
- class_append()
- class_manage()
- class_remove()
- class_release()
These functions receive a pointer to the required context, which allows you to correctly set ownership of the required script. To get the right context, use the following functions:
- Unigine::Engine::getWorldInterpreter() to manage world script ownership of the pointer.
- Unigine::Engine::getSystemInterpreter() to manage system script ownership of the pointer.
- Unigine::Engine::getEditorInterpreter() to manage editor script ownership of the pointer.
Setting the context is required as we perform conversion between external class variables and script ones.
For example:
- Create a custom function on the C++ side, which will receive a variable. Release script ownership of the received variable.
#include <UnigineEngine.h> #include <UnigineInterpreter.h> #include <UnigineImage.h> #include <UnigineInterface.h> /* */ using namespace Unigine; /* */ // 1. Create an external function that receives a variable const char* change_owner(const Variable &v) { // 1.1 Release script ownership of the received variable Engine *engine = Engine::get(); v.removeExternClass(engine->getWorldInterpreter()); // the world script context should be set, as removeExternClass() requires a script environment // 1.2. Cast the received variable to the ImagePtr type ImagePtr image = v.getImage(Interpreter::get()); // 1.3. Call the image member functions exposed through C++ API return image->getFormatName(); } /* */ int main(int argc,char **argv) { // 2. Register the function for export into UNIGINE Interpreter::addExternFunction("change_owner",MakeExternFunction(&change_owner)); // 3. Initialize the engine. It will be shut down automatically EnginePtr engine(argc,argv); // 4. Enter the engine main loop engine->main(); return 0; }
- On the script side, create an image and pass it to the external C++ function. As the script ownership is released in the external function, there is no need to delete the image here.
int init() { // 1. Create a new image Image image = new Image(); // 1.1. Specify parameters of the image and fill it with black color image.create2D(256,256,IMAGE_FORMAT_R8); // 2. Call the registered external function and simply pass the image to it log.message("%s\n",change_owner(image)); }