Skip to main content

Tellusim API Pointers

Pointers in Tellusim behave consistently across all supported programming languages.
Ownership, reference counting, and lazy instantiation work the same way whether you use C++, C#, Rust, Swift, Python, Java, or JavaScript.

Almost all Tellusim API classes are implemented as smart pointers.
This simplifies memory management, reduces the risk of errors, and ensures a consistent API experience across languages.

Each smart pointer uses reference counting with minimal overhead.
On 64-bit platforms, a smart pointer stores three internal pointers, resulting in 24 bytes of memory.

Stack and Heap Allocation​

Tellusim pointers use lazy instantiation, meaning the internal object is not created until it is actually needed (for example, when calling a method that modifies the object).

// Internal Mesh will not be instantiated here (stack allocation size is 24 bytes).
Mesh stack;

// Internal Mesh will not be instantiated here (heap allocation size is 24 bytes).
Mesh *heap = new Mesh();

// Here the actual class object is instantiated and the name is assigned.
stack.setName("Mesh on stack");
heap->setName("Mesh on heap");

// Stack objects will be automatically released at the end of the scope.

// Heap objects must be released manually.
delete heap;
note

The pointer variable requires 24 bytes, not including the heap memory used by the internal object.

Null Pointer​

Each smart pointer type provides a special null pointer that cannot be constructed.
Use it to indicate an invalid or empty object instead of returning a default-constructed object.

// Create pointer from null pointer.
Xml root = Xml::null;

// Invalid null pointer.
TS_LOGPTR(Message, "Root: ", root);
// Root: Valid: 0; Owner: 0; Const: 0; Count: 0; Internal: 0x0
note

Do not return Xml() as a null value.
A default-constructed smart pointer is considered valid and will instantiate its internal object on first access.

Owning vs Non-Owning Pointers​

Smart pointers can be owning or non-owning:

  • Owning pointer: Destroys the internal object when the reference count reaches zero.
  • Non-owning pointer: Keeps the object alive even if the reference count is zero.

Ownership can be manually changed with acquirePtr() or unacquirePtr(), though most ownership management is automatic.

// Root is instantiated because we use a non-default constructor.
Xml root("Root");

// Root pointer is an owner.
TS_LOGPTR(Message, "Root: ", root);
// Root: Valid: 1; Owner: 1; Const: 0; Count: 1; Internal: 0x8d2800010

// Child 0

// Create child object.
Xml child_0("Child 0");

// Child 0 pointer is an owner.
TS_LOGPTR(Message, "Child 0: ", child_0);
// Child 0: Valid: 1; Owner: 1; Const: 0; Count: 1; Internal: 0x8d2800068

// Add child to the root. This removes ownership from the pointer.
root.addChild(child_0);

// Child 0 pointer is not an owner anymore.
TS_LOGPTR(Message, "Child 0: ", child_0);
// Child 0: Valid: 1; Owner: 0; Const: 0; Count: 1; Internal: 0x8d2800068

// Child 1

// Create child object and transfer ownership.
// Constructors with an address argument are treated as parent object.
Xml child_1(&root, "Child 1");

// Child 1 pointer is not an owner because the constructor transfers ownership.
TS_LOGPTR(Message, "Child 1: ", child_1);
// Child 1: Valid: 1; Owner: 0; Const: 0; Count: 1; Internal: 0x8d28000c0

// Remove the child from the root.
// This method transfers ownership back to the pointer.
root.removeChild(child_1);

// Child 1 pointer is now an owner.
TS_LOGPTR(Message, "Child 1: ", child_1);
// Child 1: Valid: 1; Owner: 1; Const: 0; Count: 1; Internal: 0x8d28000c0

Manual Destruction and Reference Counting​

Even if the reference count is non-zero, you can manually destroy an object using destroyPtr().
All other pointers sharing this object become invalid automatically.

// Create root object using default constructor.
Xml root;

// The pointer is considered valid while the internal pointer is null.
// The object will be instantiated on demand.
TS_LOGPTR(Message, "Root: ", root);
// Root: Valid: 1; Owner: 0; Const: 0; Count: 0; Internal: 0x0

// Instantiate root object.
root.setName("Root");

// Reference counter is expected to be 1 here.
TS_LOGPTR(Message, "Root: ", root);
// Root: Valid: 1; Owner: 1; Const: 0; Count: 1; Internal: 0xc35400010

// Create another pointer that shares the same object.
Xml also_root = root;

// Reference counter is expected to be 2 for both pointers.
TS_LOGPTR(Message, "Also: ", also_root);
// Also: Valid: 1; Owner: 1; Const: 0; Count: 2; Internal: 0xcbf400010

// Manually destroy the first pointer.
root.destroyPtr();

// First pointer is invalid (internal object destroyed).
TS_LOGPTR(Message, "Root: ", root);
// Root: Valid: 0; Owner: 0; Const: 0; Count: 0; Internal: 0x0

// Second pointer is also invalid.
TS_LOGPTR(Message, "Also: ", also_root);
// Also: Valid: 0; Owner: 0; Const: 0; Count: 0; Internal: 0x0

// Get the name from invalid pointer.
also_root.getName();
// Assertion failed: (0 && "Xml" "Impl::ref(): is not constructed").

Upcasting and Downcasting​

Upcasting and downcasting create separate smart pointer wrappers.
Reference counting is not shared across pointer types, even if they reference the same internal object.

// Create base texture object.
Texture texture = device.createTexture2D(FormatRGBAu8n, 1024);

// Pointer is expected to be an owner with one reference.
TS_LOGPTR(Message, "Texture: ", texture);
// Texture: Valid: 1; Owner: 1; Const: 0; Count: 1; Internal: 0x72cc8d360

// Upcast texture to an API-specific texture object.
MTLTexture mtl_texture = MTLTexture(texture);

// Upcasted pointer is expected to be non-owning with two references (base + derived).
TS_LOGPTR(Message, "MTLTexture: ", mtl_texture);
// MTLTexture: Valid: 1; Owner: 0; Const: 0; Count: 2; Internal: 0x72cc8d360

// Downcast back to base texture.
Texture base_texture = mtl_texture.getTexture();

// Downcasted pointer is expected to be non-owning with one reference.
TS_LOGPTR(Message, "BaseTexture: ", base_texture);
// BaseTexture: Valid: 1; Owner: 0; Const: 0; Count: 1; Internal: 0x72cc8d360

// Destruction of the original texture will not invalidate upcasted objects.
texture.destroyPtr();

// Original pointer is expected to be destroyed.
TS_LOGPTR(Message, "Texture: ", texture);
// Texture: Valid: 1; Owner: 0; Const: 0; Count: 0; Internal: 0x0

// This is dangerous.
// mtl_texture and base_texture now refer to freed memory (dangling pointers).
TS_LOGPTR(Message, "MTLTexture: ", mtl_texture);
// MTLTexture: Valid: 1; Owner: 0; Const: 0; Count: 2; Internal: 0x72cc8d360

Direct Derived Construction​

You can construct derived objects directly without intermediate pointers:

// Create derived texture object directly from a device.
MTLTexture mtl_texture = MTLTexture(device.createTexture2D(FormatRGBAu8n, 1024));

// Pointer is an owner with two references (base + derived).
TS_LOGPTR(Message, "MTLTexture: ", mtl_texture);
// MTLTexture: Valid: 1; Owner: 1; Const: 0; Count: 2; Internal: 0x71ad09220

Const Methods and Pointers​

Const pointers cannot modify internal objects.
C++ allows removing constness syntactically, but Tellusim enforces const correctness at runtime.

// Create root object.
Xml root("Root");

// Create child object with ownership transferred to root.
Xml child = Xml(&root, "Child");

// Get child from the root using non-const getChild().
child = root.getChild(0u);

// The pointer is non-const.
TS_LOGPTR(Message, "Child: ", child);
// Child: Valid: 1; Owner: 0; Const: 0; Count: 1; Internal: 0x89b884068

// Non-const child can be modified.
child.setName("Child");

// Make a const reference from root.
const Xml &const_root = root;

// Get a const child using the const version of getChild().
const Xml &const_child = const_root.getChild(0u);

// The pointer is const.
TS_LOGPTR(Message, "Const Child: ", const_child);
// Const Child: Valid: 1; Owner: 0; Const: 1; Count: 1; Internal: 0x89b884068

// The following is not valid C++ because const_child is a const reference.
// const_child.setName("Child");

// But we can (unsafely) copy the const pointer into a non-const pointer.
Xml from_const_child = const_child;

// This compiles but will cause a runtime assertion when executed.
from_const_child.setName("Child");
// Assertion failed: (!isConst() && "Xml" "Impl::ref(): constant pointer").
note

Use debug builds to catch and diagnose such invalid operations.

Pointer Methods​

Pointers provide a full set of overloaded operators and can be used in comparison operations and containers like Map.

// Returns a deep copy of the internal object.
// The returned object is completely independent.
TYPE clonePtr() const;

// Clears the pointer.
// The internal object will be destroyed if
// the reference counter is zero and the pointer is the owner.
void clearPtr();

// Manually destroys the internal object, even if the reference counter is non-zero.
// All pointer copies will be invalid after that call.
void destroyPtr();

// Pointer becomes the owner of the internal object.
TYPE &acquirePtr();

// Pointer stops being the owner of the internal object.
TYPE &unacquirePtr();

// Returns true if the pointer is not null or is scheduled to be constructed.
bool isValidPtr() const;

// Returns true if the pointer is an owner.
bool isOwnerPtr() const;

// Returns true if the pointer is a const pointer.
bool isConstPtr() const;

// Returns the number of pointer references.
uint32_t getCountPtr() const;

// Returns the pointer to the internal object (for debug only).
const void *getInternalPtr() const;

Performance​

Accessing smart pointers has minimal overhead.
A test performing 132 million API calls completed in 220 ms - just a few CPU cycles per call.

For maximum efficiency, raw memory access is supported for container classes (MeshAttribute, MeshIndices, Image, and others).

Best Practices​

  • Prefer stack allocation for automatic lifetime management.
  • Do not rely on upcasted or downcasted pointers after the original owner is destroyed.
  • Use debug builds to detect invalid const-to-nonconst conversions.
  • Always respect const-correctness to prevent runtime assertions.