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;
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
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").
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.