Memory Management
This article dwells on pointers management and communication between the Unigine scripts, C++ part of Unigine engine (native C++ classes and the Engine Editor) and extern C++ classes.
See Also
Communication between Internal Modules and External C++ Part
Allocating memory, keeping track of those allocations and freeing memory blocks when they are no longer needed is mainly the task of managing pointers. Unigine consists of several internal modules each of which can establish the ownership of the pointer, meaning that it would be responsible for deallocating the memory when the reference is no longer needed. By that, the object is accessible by all internal modules. Ownership can also be released and transferred for another module to take responsibility and delete the unnecessary pointer at the right time. If failing to set only one owner that deallocates the object, memory leaks or even application crashes are imminent.
The situation with external C++ modules, which extend Unigine functionality with extern C++ classes, is different. 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 internal modules. Conversely, custom C++ modules use an underlying system-provided allocator, which has a separate memory pool. This means that ownership of the pointer should not be transfered between internal and external modules, because memory allocated by an external C++ module, can never be freed by one of the Unigine internal modules and vice versa.
To sum it up, objects can be created and managed by the following modules:
- Unigine runtime:
- Scripts. These are:
- World script that contains world logic.
- Editor script that wraps editor functionality and editor GUI.
- System script that does the system housekeeping and controls the main menu (system) GUI.
- Native C++ classes. These are low-lever built-in objects, that are actually created on C++ side when the script declares them, such as: node, mesh, body, image etc.
- Engine Editor. It also a C++ part of Unigine engine. When the world is initialized, Engine Editor loads nodes from the world file and creates an array of pointers to them. Engine Editor nodes also appear on the Nodes panel as hierarchal list and can be adjusted in the virtual world in real-time.
- Scripts. These are:
- External module:
- API C++ classes is an external C++ module, which is accessed through C++ API. It should not manage pointers to the object created on the Unigine side, since it cannot allocate or free such pointee. However, it is possible to declare Unigine native C++ objects using smart pointers Unigine::Ptr, such as Unigine::ImagePtr, Unigine::FilePtr or Unigine::Node Class.
Managing Pointers
The basic principles of manging pointers are as follows:
- Whoever creates an object via a new operator will automatically become the owner of the pointer. Afterwards, the object can be deleted with a delete operator.
- A pointer should be owned only by one module to avoid double-freeing.
- When transferring the ownership of the pointer, it must be released from the previous owner and assigned to a new one. (The order of these operations is of no importance).
- Never leave orphan or released pointers without an assigned owner.
Orphan (unowned) object pointers are the result of calling the following functions:
- All clone() functions in the library return new cloned objects, pointers to which are not owned by any of the modules.
- engine.world.loadNode loads a node with an orphan pointer.
- 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
- an 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 or joint.setBody1)
- an body was specified in the constructor, for example, new JointFixed(body0,body1)
Script Ownership
Scripts can handle ownership of the pointer using the following system functions:
- class_append() assigns ownership of the object to the current script module. All appended objects will be automatically deleted on the script shutdown; or they can be deleted using delete operator. For example:
Here, clone() returns a new node, pointer to which is orphaned. To prevent the memory leak, a script takes ownership and safely deletes it.
Node clone = node.clone(); class_append(clone); delete clone;
- class_manage() indicates that reference count should be performed for the object. When the number of references that point to the object reaches zero, the memory previously allocated for it is automatically deleted, thus freeing the developer from carefully managing the lifetime of pointed-to objects. Before calling this function, the object should naturally 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.
image = new Image(); class_manage(image); image = 0;
- class_release() removes all references to the external class including its pointers. It removes even the smallest memory leaks.
NodeDummy node = new NodeDummy(); node = class_release(node); // node is deleted
- class_remove() releases the ownership of the pointer. It needs to be reassigned to any module (before or after it has been released) not to become orphaned. For example, it can be passed the Engine Editor to appear on the Nodes panel to be adjusted in real-time.
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 object of a given type into another type. Take notice, that the pointer conversions are unsafe and allow to specify any type, because neither the pointee nor the pointer type itself is checked.
Node node = engine.world.getNode(id); ObjectMesh mesh = class_cast("ObjectMesh",node); string name = mesh.getMeshName();
There is also a group of functions that allow to safely handle hierarchical nodes together with all of their children. They can be found in 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 MyNode and its child NodeChild.
Node node = engine.world.loadNode("my.node"); node.addChild(class_remove(new NodeDummy())); node_append(node);
- Node node_remove(Node node) releases the script ownership of the given node and its children. (Do not forget to set another owner!)
- 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.
Node node = engine.world.loadNode("my.node"); node.addChild(class_remove(new NodeDummy())); node_delete(node_append(node));
- Node node_clone(Node node) clones the node. 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. Like all other clone() functions, created pointer is orphaned and it ownership is to be passed to some module.
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.
ObjectMesh mesh = new ObjectMesh("samples/common/meshes.statue.mesh"); node_append(node_clone(mesh));
Functions for non-hierarchical nodes:
- Node node_cast(Node node) allows to safely convert the given base node to its derived type.
After downcasting, the node can call member functions of ObjectMesh class.
ObjectMesh mesh = node_cast(node); mesh.getNumSurfaces();
Engine Editor Ownership
As it was said, the Engine Editor owns the nodes loaded from the world file. When the world is unloaded, the nodes will be automatically deleted, thus freeing allocated memory.
- engine.editor.addNode() establishes Engine Editor ownership of the node. Before that, the node should be released of other owners.
After the script releases the ownership of the mesh object established via new operator, it can be handled by Engine Editor.
ObjectMesh mesh = new ObjectMesh("samples/common/meshes/statue.mesh"); engine.editor.addNode(node_remove(mesh));
- engine.editor.removeNode() deletes the node that is owned by the Engine Editor.
Here, a new mesh, created via a new operator and owned by the script, is released of script ownership. After that, it becomes orphaned and the ownership is transfered to the Engine Editor, which safely deletes it.
ObjectMesh mesh = new ObjectMesh("samples/common/meshes/statue.mesh"); engine.editor.addNode(node_remove(mesh)); engine.editor.removeNode(mesh);
- engine.editor.releaseNode() allows to release the ownership of the node held by the Engine Editor and transfer it to some other module (for example, the script).
Here, the node ownership is passed from the script to the Engine Editor, which in its turn releases it. After that, the node is free to be owned by the script again.
ObjectMesh mesh = new ObjectMesh("samples/common/meshes/statue.mesh"); engine.editor.addNode(node_remove(mesh)); engine.editor.releaseNode(mesh); node_delete(node_append(mesh));
Extern C++ Module Ownership
Extern class can access the Unigine native object created by some internal module using the C++ API; however, it can never create or delete it. For more details, see the C++ API Usage Examples articles.
There are different variants to create and handle ownership of the object:
- Create an object by the script and pass it to be received by the extern C++ function:
- Create a smart pointer by the extern C++ function, which will allocate a new native Unigine class, and pass it to the script:
- Create a smart pointer to the new allocated Unigine object on the C++ side, while the function is called inside the C++ part.
Receive Object as Node
The fisrst variant is the following: a node that is created and handled by the world script is passed to the extern function as a smart pointer.
- Create a custom function on the C++ side, to which a NodePtr smart pointer will be passed. Then a function need to be registered with Unigine interpreter, so that the script could call it in its runtime.
//main.cpp #include <Unigine.h> #include <UnigineInterpreter.h> /* */ using namespace Unigine; /* */ // 1. Create an extern function that receives a script node as 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 Engine *engine = Engine::init(UNIGINE_VERSION,argc,argv); // 4. Enter the engine main loop engine->main(); // 5. Shut down the engine Engine::shutdown(); 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 delete operator to destroy the node.
//world script (myworld.cpp) int init() { // 1. Create a new dummy object and cast it to the Node type, so it could be passed to the extern C++ function as NodePtr Node node = class_cast("Node",new ObjectDummy()); // 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 Object of Specific Type
This variant is similar to the previous one, except that it is valid for the following objects:
It allows to create a specific object by the world script, pass it to an extern function and use its methods exposed through a C++ API.- Create a custom function on the C++ side, which will receive a NodePtr smart pointer. An object as it is cannot be passed between the script and C++ part — only a node pointer can. After that, the object is cast to its specific type using create() function and calls its member function. The extern function should be registered to be called by the script.
//main.cpp #include <Unigine.h> #include <UnigineObject.h> #include <UnigineInterpreter.h> /* */ using namespace Unigine; /* */ // 1.0. Create an extern function that receives a script object as a smart pointer NodePtr void my_object_update(NodePtr node,float time) { // 1.1. Cast the given object from the NodePtr type to the ObjectMeshDynamicPtr smart pointer type ObjectMeshDynamicPtr object = ObjectMeshDynamic::create(node); // 1.2. Call the member function of the ObjectMeshDynamic object->updateSurfaces(); } /* */ 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 Engine *engine = Engine::init(UNIGINE_VERSION,argc,argv); // 4. Enter the engine main loop engine->main(); // 5. Shut down the engine Engine::shutdown(); return 0; }
- On the script side, create an object and cast it to the node type. After that, the registered extern C++ function call receive it, when called by the script. Ownership remains by the script, so it can delete the object.
//world script (myworld.cpp) int init() { // 1. Create a new ObjectMeshDynamic Object object = new ObjectMeshDynamic(); // 1.1. Call the member function object.setMaterial("mesh_base","*"); // 2. Cast the object into a node type for the extern function to recieve it Node node = class_cast("Node",object); // 3. Call the registered extern C++ function my_object_update(node,engine.game.getTime()); // 4. The script can delete the object, as it was the one that allocated it delete node; return 1; }
Receive Object as Variable
The third variant is to create an object in the script (for example, the image) and pass it to the extern C++ function as a variable. The extern 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 extern function should be registered to be called by the script.
//main.cpp #include <Unigine.h> #include <UnigineImage.h> #include <UnigineInterpreter.h> /* */ using namespace Unigine; /* */ // 1. Create an extern function that recieves a variable void my_image_get(const Variable &v) { // 1.1. Cast the recieved variable to the ImagePtr type ImagePtr image = v.getImage(); // 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 Engine *engine = Engine::init(UNIGINE_VERSION,argc,argv); // 4. Enter the engine main loop engine->main(); // 5. Shut down the engine Engine::shutdown(); return 0; }
There also exists another way to cast the variable into the image pointer type using VariableToType function:
//main.cpp ... // 1. The same extern function void my_image_get(const Variable &v) { // 1.1. Another way of casting the variable value into ImagePtr: ImagePtr image = VariableToType<ImagePtr>(v).value; return image->getFormatName(); } ...
- On the script side, the image needs to be created and simply passed to the extern function. The image will be converted into the ImagePtr smart pointer automatically.
//world script (myworld.cpp) 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 extern function and simply pass the image to it my_image_get; // 3. The script can delete the image explicitly, if necessary delete image; return 1; }
Create and Pass Object as Smart Pointer
Smart pointers allow the extern function not only to recieve the objects created by the script, but also create instances of a native Unigine C++ classes, which are allocated in the Unigine memory pool.
Here, a script calls an extern function that creates a new image using ImgPtr.
- First of all, it is necessary to define ImgPtr as a global variable. Otherwise, if it is a local variable defined inside the function block, the image will not be visible outside the function scope. Though it can be passed to the script, the pointer will be dangling, i.e. it will not point to the valid image object.
Then a custom function calls API function create() and is registered with the Unigine interpreter.//main.cpp #include <Unigine.h> #include <UnigineImage.h> #include <UnigineInterpreter.h> /* */ using namespace Unigine; /* */ // 0. ImagePtr must be defined as a global variable ImagePtr image; // 1. Create an extern function that returns an ImagePtr smart pointer ImagePtr my_image_create_0() { // 1.1. Create an image in the Unigine memory pool through ImagePtr smart pointer image = Image::create(); // 1.2. Specify parameters of the image and fill it with black color image->create2D(128,128,Image::FORMAT_RG8); 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 Engine *engine = Engine::init(UNIGINE_VERSION,argc,argv); // 4. Enter the engine main loop engine->main(); // 5. Shut down the engine Engine::shutdown(); return 0; }
- Call the registered extern function. The image it returns can be simply passed to the script, as conversion from ImgPtr into the image will be done automatically. Remember, the script cannot delete the image, as the ownership belongs to the extern function. The smart pointer will be automatically released when the reference count reaches zero.
//world script (myworld.cpp) int init() { // 1. Call the extern function in the script my_image_create_0(); // 2. The script will automatically handle ImgPtr returned by the extern function as a simple image image.getFormatName(); return 1; }
Create and Pass Object as Variable
This variant is similar to the previous one, except that the extern function creates a new image object as a variable. The script still calls the extern function.
- ImgPtr should be defined as a global variable, just like in the previous case. Then the custom extern function can create an ImgPtr smart pointer, which will allocate a native Unigine image, and set it to the variable using the dedicated function setImage().
//main.cpp #include <Unigine.h> #include <UnigineImage.h> #include <UnigineInterpreter.h> /* */ using namespace Unigine; /* */ // 0. ImagePtr must be defined as a global variable ImagePtr image; // 1. Create an extern function that returns a variable Variable my_image_create_1() { // 1.1. Create an image in the Unigine memory pool image = Image::create(); // 1.2. Specify parameters of the image and fill it with black color image->create2D(128,128,Image::FORMAT_RG8); // 1.3. Define the variable Variable v; // 1.4. Set the image smart pointer to the variable v.setImage(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 Engine *engine = Engine::init(UNIGINE_VERSION,argc,argv); // 4. Enter the engine main loop engine->main(); // 5. Shut down the engine Engine::shutdown(); return 0; }
Another way to set the ImgPtr smart pointer to the variable is to use TypeToVariable function:
//main.cpp ... // 1. The same extern function Variable my_image_create_1() { image = Image::create(); image->create2D(128,128,Image::FORMAT_RG8); // 1.3. Another way of setting the image smart pointer to the variable: Variable v = TypeToVariable<ImagePtr>(image).value; return v; } ...
- A script calls the registered extern 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 extern function. The smart pointer will be automatically released when the reference count reaches zero.
//world script (myworld.cpp) int init() { // 1. Call the extern function in the script my_image_create_1(); // 2. The scipt automatically handles the returned ImgPrt as a simple image image.getFormatName(); return 1; }
Create Variable in External Function
The last variant is to create a native Unigine Image object as a variable on the C++ side. However, it is not the script that calls the external function, therefore the context for the created variable should be set manually for the interpreter. It is done with the help of the following functions:
- Unigine::Engine::getWorldInterpreter()
- Unigine::Engine::getSystemInterpreter()
- Unigine::Engine::getWorldInterpreter()
//main.cpp
#include <Unigine.h>
#include <UnigineImage.h>
#include <UnigineInterpreter.h>
/*
*/
using namespace Unigine;
/*
*/
// 0. ImagePtr must be defined as a global variable
ImagePtr image;
// 1. Create an external function that returns a variable
Variable my_image_create_1() {
// 1.1. Create an image in the Unigine memory pool
image = Image::create();
// 1.2. Specify parameters of the image and fill it with black color
image->create2D(128,128,Image::FORMAT_RG8);
// 1.3. Define the variable
Variable v;
// 1.4. Set the world script context, because setImage() requires a script environment
Engine *engine = Engine::get();
Interpreter *world = (Interpreter*)engine->getWorldInterpreter();
// 1.5. Set the image smart pointer to the variable
v.setImage(world,image);
return v;
}
...
// Use the external function somewhere later
...