UnigineScript
The Language
Core Library
Engine Library
Node-Related Classes
GUI-Related Classes
Plugins Library
High-Level Systems
Samples
C++ API
API Reference
Integration Samples
Usage Examples
C++ Plugins
Content Creation
Materials
Unigine Material Library
Tutorials

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.
      Scripts are written in UnigineScript language and allow to create user classes.
    • 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.
  • 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.

Notice
A node can be handled by any of the scripts (world, system or editor one), not matter whom it belongs to. The ownership matters when deleting it.

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.

Notice
Ownership is automatically reassigned in the following cases. That is why is required to use class_remove() for created bodies, shapes or joints to release the script ownership.

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:
    Source code (UnigineScript)
    Node clone = node.clone();
    class_append(clone);
    delete clone;
    
    Here, clone() returns a new node, pointer to which is orphaned. To prevent the memory leak, a script takes ownership and safely deletes it.
  • 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.
    Source code (UnigineScript)
    image = new Image();
    class_manage(image);
    image = 0;
    
    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.
  • class_release() removes all references to the external class including its pointers. It removes even the smallest memory leaks.
    Source code (UnigineScript)
    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.
    Source code (UnigineScript)
    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.
    Source code (UnigineScript)
    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.
    Source code (UnigineScript)
    Node node = engine.world.loadNode("my.node");
    node.addChild(class_remove(new NodeDummy()));
    node_append(node);
    
    Here, the script takes ownership of both MyNode and its child NodeChild.
  • 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.
    Source code (UnigineScript)
    Node node = engine.world.loadNode("my.node");
    node.addChild(class_remove(new NodeDummy()));
    node_delete(node_append(node));
    
    Here, both the node and its child will be deleted.
  • 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.
    Source code (UnigineScript)
    ObjectMesh mesh = new ObjectMesh("samples/common/meshes.statue.mesh");
    node_append(node_clone(mesh));
    
    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.

    Functions for non-hierarchical nodes:

  • Node node_cast(Node node) allows to safely convert the given base node to its derived type.
    Source code (UnigineScript)
    ObjectMesh mesh = node_cast(node);
    mesh.getNumSurfaces();
    
    After downcasting, the node can call member functions of ObjectMesh class.

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.
    Source code (UnigineScript)
    ObjectMesh mesh = new ObjectMesh("samples/common/meshes/statue.mesh");
    engine.editor.addNode(node_remove(mesh));
    
    After the script releases the ownership of the mesh object established via new operator, it can be handled by Engine Editor.
  • engine.editor.removeNode() deletes the node that is owned by the Engine Editor.
    Source code (UnigineScript)
    ObjectMesh mesh = new ObjectMesh("samples/common/meshes/statue.mesh");
    engine.editor.addNode(node_remove(mesh));
    engine.editor.removeNode(mesh);
    
    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.
  • 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).
    Source code (UnigineScript)
    ObjectMesh mesh = new ObjectMesh("samples/common/meshes/statue.mesh");
    engine.editor.addNode(node_remove(mesh));
    engine.editor.releaseNode(mesh);
    node_delete(node_append(mesh));
    
    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.

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:

  1. Create an object by the script and pass it to be received by the extern C++ function:
  2. Create a smart pointer by the extern C++ function, which will allocate a new native Unigine class, and pass it to the script:
  3. 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.

  1. 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.
    Source code (C++)
    //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;
    }
    
  2. 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.
    Source code (UnigineScript)
    //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.

  1. 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.
    Source code (C++)
    //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;
    }
    
  2. 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.
    Source code (UnigineScript)
    //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.

  1. 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.
    Source code (C++)
    //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:

    Source code (C++)
    //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();
    }
    ...
    
  2. 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.
    Source code (UnigineScript)
    //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.

  1. 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.
    Source code (C++)
    //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;
    }
    
  2. 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.
    Source code (UnigineScript)
    //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.

  1. 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().
    Source code (C++)
    //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:

    Source code (C++)
    //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;
    }
    ...
    
  2. 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.
    Source code (UnigineScript)
    //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:

Setting one of the script environments is required, when unsafe conversions between external class variables and script ones take place.

Notice
When calling these functions, the corresponding script should already be loaded.

Source code (C++)
//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
...
The smart pointer will be automatically released when the reference count reaches zero.
Last update: 2017-07-03