Memory Management
Summary: How Enforce Script's automatic reference counting works, when to use
refversus raw pointers, why reference cycles leak forever, and how theManagedbase class makes weak references safe.
Introduction
Enforce Script uses automatic reference counting (ARC) for memory management -- not garbage collection in the traditional sense. Understanding how ref, autoptr, and raw pointers work is essential for writing stable DayZ mods. Get it wrong and you will either leak memory (your server gradually consumes more RAM until it crashes) or access deleted objects (instant crash with no useful error message). This chapter explains every pointer type, when to use each, and how to avoid the most dangerous pitfall: reference cycles.
The Three Pointer Types
Enforce Script has three ways to hold a reference to an object:
| Pointer Type | Keyword | Keeps Object Alive? | Zeroed on Delete? | Primary Use |
|---|---|---|---|---|
| Raw pointer | (none) | No (weak reference) | Only if class extends Managed | Back-references, observers, caches |
| Strong reference | ref | Yes | Yes | Owned members, collections |
| Auto pointer | autoptr | Yes (strong reference) | Yes | Legacy keyword -- prefer ref |
How ARC Works
Every object has a reference count -- the number of strong references (ref, autoptr, local variables, function arguments) pointing to it. When the count drops to zero, the object is automatically destroyed and its destructor is called.
Weak references (raw pointers) do NOT increase the reference count. They observe the object without keeping it alive.
Several examples in this chapter use this minimal class -- define it once and every snippet below is runnable as-is:
class MyClass : Managed
{
int m_Value;
}Raw Pointers (Weak References)
A raw pointer is any variable declared without ref or autoptr. For class members, this creates a weak reference: it points to the object but does NOT keep it alive.
class Observer
{
PlayerBase m_WatchedPlayer; // Weak reference -- does NOT keep player alive
void Watch(PlayerBase player)
{
m_WatchedPlayer = player;
}
void Report()
{
if (m_WatchedPlayer) // ALWAYS null-check weak references
{
Print("Watching: " + m_WatchedPlayer.GetIdentity().GetName());
}
else
{
Print("Player no longer exists");
}
}
}Managed vs Non-Managed classes
The safety of weak references depends on whether the object's class extends Managed:
- Managed classes (most DayZ gameplay classes): When the object is deleted, all weak references are automatically set to
null. This is safe. - Non-Managed classes (plain
classwithout inheritingManaged): When the object is deleted, weak references become dangling pointers -- they still hold the old memory address. Accessing them causes a crash.
A note on sourcing: Managed itself is declared as an empty marker class in the vanilla script headers (scripts/1_core/proto/enscript.c) -- the auto-nulling of weak references is engine-side behavior described in Bohemia's Enforce Script documentation, not something you can read in the script sources. Treat it as a safety net, not as permission to skip null checks: always null-check weak references before use, whether or not the class is Managed.
// SAFE -- Managed class, weak refs are zeroed
class SafeData : Managed
{
int m_Value;
}
void TestManaged()
{
SafeData data = new SafeData();
SafeData weakRef = data;
delete data;
if (weakRef) // false -- weakRef was automatically set to null
{
Print(weakRef.m_Value); // Never reached
}
}// DANGEROUS -- Non-Managed class, weak refs become dangling
class UnsafeData
{
int m_Value;
}
void TestNonManaged()
{
UnsafeData data = new UnsafeData();
UnsafeData weakRef = data;
delete data;
if (weakRef) // TRUE -- weakRef still holds old address!
{
Print(weakRef.m_Value); // CRASH! Accessing deleted memory
}
}Rule: If you are writing your own script-only classes, always extend
Managedfor safety. Game entities are already covered by inheritance -- you never write: Managedon them yourself (that is why Variables and Types lists entity classes under "do not manually extendManaged"), because they inherit it through the engine hierarchy: the vanilla scripts declareIEntity: Managed(scripts/1_core/proto/enentity.c), and the full chain isEntityAI -> Entity -> ObjectTyped -> Object -> IEntity -> Managed(each link is verifiable inscripts/3_game/entities/). SoEntityAI,ItemBase,PlayerBase, and every other game entity is aManagedclass, and raw pointers to them are auto-nulled when the entity is deleted -- but you still null-check every use.
ref (Strong Reference)
The ref keyword marks a variable as a strong reference. The object stays alive as long as at least one strong reference exists. When the last strong reference is destroyed or overwritten, the object is deleted.
Class members
Use ref for objects that your class owns and is responsible for creating and destroying.
class LNT_Mission : Managed
{
protected string m_Name;
void LNT_Mission(string name)
{
m_Name = name;
}
}
class LNT_MissionConfig : Managed
{
string m_DisplayName;
float m_Reward;
}
class LNT_MissionManager : Managed
{
protected ref array<ref LNT_Mission> m_ActiveMissions;
protected ref map<string, ref LNT_MissionConfig> m_Configs;
void LNT_MissionManager()
{
m_ActiveMissions = new array<ref LNT_Mission>;
m_Configs = new map<string, ref LNT_MissionConfig>;
}
// No destructor needed! When LNT_MissionManager is deleted:
// 1. m_Configs ref is released -> map is deleted -> each LNT_MissionConfig is deleted
// 2. m_ActiveMissions ref is released -> array is deleted -> each LNT_Mission is deleted
}Collections of owned objects
When you store objects in an array or map and want the collection to own them, use ref on both the collection AND the elements:
class SafeZone : Managed
{
protected vector m_Center;
protected float m_Radius;
void SafeZone(vector center, float radius)
{
m_Center = center;
m_Radius = radius;
}
}
class ZoneManager : Managed
{
// The array is owned (ref), and each zone inside is owned (ref)
protected ref array<ref SafeZone> m_Zones;
void ZoneManager()
{
m_Zones = new array<ref SafeZone>;
}
void AddZone(vector center, float radius)
{
ref SafeZone zone = new SafeZone(center, radius);
m_Zones.Insert(zone);
}
}Critical distinction: An array<SafeZone> holds weak references. An array<ref SafeZone> holds strong references. If you use the weak version, objects inserted into the array may be immediately deleted because no strong reference keeps them alive.
// WRONG -- Objects are deleted immediately after insertion!
ref array<MyClass> weakArray = new array<MyClass>;
weakArray.Insert(new MyClass()); // Object created, inserted as weak ref,
// no strong ref exists -> IMMEDIATELY deleted
// CORRECT -- Objects are kept alive by the array
ref array<ref MyClass> strongArray = new array<ref MyClass>;
strongArray.Insert(new MyClass()); // Object lives as long as it's in the arrayautoptr (Legacy Strong Reference)
autoptr is an older keyword that also creates a strong reference. Its historical intent was a strong reference tied to the enclosing scope: when the variable goes out of scope (or the object holding it is destroyed), the reference is released and the object is freed if nothing else holds it. That is exactly the release point ref members and plain local variables already have, so in every situation you will encounter, autoptr behaves the same as a strong reference declared any other way.
void ProcessData()
{
autoptr JsonSerializer serializer = new JsonSerializer();
// Use serializer...
// serializer is released here when the function exits --
// exactly like a plain local variable would be
}The vanilla scripts use autoptr in only a handful of files -- for example, the private member maps in scripts/3_game/analytics/scriptanalytics.c -- and use ref everywhere else.
autoptr vs plain locals
In practice, local variables are already strong references by default in Enforce Script. The autoptr keyword adds nothing:
void Example()
{
// These are functionally equivalent:
MyClass a = new MyClass(); // Local var = strong ref (implicit)
autoptr MyClass b = new MyClass(); // Local var = strong ref (explicit, legacy)
// Both a and b are released when this function exits
}Convention in DayZ modding: Most codebases use
reffor class members and plain declarations for locals. A widely followed community convention is to avoidautoptrentirely and use explicitref-- it says the same thing with one consistent keyword. Follow whichever convention your project establishes, but be consistent.
notnull Parameter Modifier
The notnull modifier on a function parameter declares that null is never a valid argument. How it is enforced is not documented -- the keyword does not appear on Bohemia's Enforce Script syntax page -- so treat it as a contract the caller must honour rather than as a guarantee you can lean on inside the function.
void ProcessPlayer(notnull PlayerBase player)
{
// The caller is responsible for never passing null here
string name = player.GetIdentity().GetName();
Print("Processing: " + name);
}
void CallExample(PlayerBase maybeNull)
{
if (maybeNull)
{
ProcessPlayer(maybeNull); // OK -- we checked first
}
// ProcessPlayer(null); // Never do this -- it violates the parameter's contract
}Use notnull on parameters where null would always be a programming error: it documents the contract in the signature, and vanilla's own script relies on it (array<T>.InsertAll at 1_core/proto/enscript.c:449 dereferences its notnull parameter immediately). It is not a substitute for checking at the call site.
Reference Cycles (MEMORY LEAK WARNING)
A reference cycle occurs when two objects hold strong references (ref) to each other. Neither object can ever be deleted because each one keeps the other alive. This is the most common source of memory leaks in DayZ mods.
The problem
class Parent
{
ref Child m_Child; // Strong reference to Child
}
class Child
{
ref Parent m_Parent; // Strong reference to Parent -- CYCLE!
}
void CreateCycle()
{
ref Parent parent = new Parent();
ref Child child = new Child();
parent.m_Child = child;
child.m_Parent = parent;
// When this function exits:
// - The local 'parent' ref is released, but child.m_Parent still holds parent alive
// - The local 'child' ref is released, but parent.m_Child still holds child alive
// NEITHER object is ever deleted! This is a permanent memory leak.
}The fix: One side must be a raw (weak) reference
Break the cycle by making one side a weak reference. The "child" should hold a weak reference to its "parent". Extend Managed so the child's raw back-pointer is nulled safely if the parent is ever deleted first:
class Parent : Managed
{
ref Child m_Child; // Strong -- parent OWNS the child
}
class Child : Managed
{
Parent m_Parent; // Weak (raw) -- child OBSERVES the parent
}
void NoCycle()
{
ref Parent parent = new Parent();
ref Child child = new Child();
parent.m_Child = child;
child.m_Parent = parent;
// When this function exits:
// - Local 'parent' ref is released -> parent's ref count = 0 -> DELETED
// - Parent destructor releases m_Child -> child's ref count = 0 -> DELETED
// Both objects are properly cleaned up!
}Real-world example: UI panels
A common pattern in DayZ UI code is a panel that holds widgets, where widgets need a reference back to the panel. The panel owns the widgets (strong ref), and widgets observe the panel (weak ref).
class AdminPanel : Managed
{
protected ref array<ref AdminPanelTab> m_Tabs; // Owns the tabs
void AdminPanel()
{
m_Tabs = new array<ref AdminPanelTab>;
}
void AddTab(string name)
{
ref AdminPanelTab tab = new AdminPanelTab(name, this);
m_Tabs.Insert(tab);
}
}
class AdminPanelTab : Managed
{
protected string m_Name;
protected AdminPanel m_Owner; // WEAK -- avoids cycle
void AdminPanelTab(string name, AdminPanel owner)
{
m_Name = name;
m_Owner = owner; // Weak reference back to parent
}
AdminPanel GetOwner()
{
return m_Owner; // May be null if panel was deleted
}
}Reference Counting Lifecycle
Reference Cycle (Memory Leak)
The delete Keyword
You can manually delete an object at any time using delete. This destroys the object immediately, regardless of its reference count. All references (both strong and weak, on Managed classes) are set to null.
void ManualDelete()
{
ref MyClass obj = new MyClass();
ref MyClass anotherRef = obj;
Print(obj != null); // true
Print(anotherRef != null); // true
delete obj;
Print(obj != null); // false
Print(anotherRef != null); // false (also nulled, on Managed classes)
}When to use delete
- When you need to release a resource immediately (not waiting for ARC)
- When cleaning up in a shutdown/destroy method
- When removing objects from the game world (
GetGame().ObjectDelete(obj)for game entities)
When NOT to use delete
- On objects owned by someone else (the owner's
refwill become null unexpectedly) - On objects still in use by other systems (timers, callbacks, UI)
- On engine-managed entities without going through proper channels
Garbage Collection Behavior
Enforce Script does NOT have a traditional garbage collector that periodically scans for unreachable objects. Instead, it uses deterministic reference counting:
- When a strong reference is created (assignment to
ref, local variable, function argument), the object's reference count increases. - When a strong reference goes out of scope or is overwritten, the reference count decreases.
- When the reference count reaches zero, the object is immediately destroyed (destructor is called, memory is freed).
deletebypasses the reference count and destroys the object immediately.
This means:
- Object lifetimes are predictable and deterministic
- There are no "GC pauses" or unpredictable delays
- Reference cycles are NEVER collected -- they are permanent leaks
- Order of destruction is well-defined: objects are destroyed in reverse order of their last reference being released
static Fields Survive a Mission Restart
A mission restart -- a player disconnecting to the main menu and reconnecting, or an admin running #restart -- is not a process restart. The script VM and every class it has loaded stay resident; only the Mission object is torn down and rebuilt. static field initializers run once per game process launch, not once per mission, so any static value your code changed during the previous mission is still sitting there when the next one starts:
class MyLockCounter
{
static int s_Count = 0; // initializer runs ONCE per process
static void Acquire() { s_Count++; if (s_Count == 1) DoTheRealWork(); }
}
// Mission A leaves s_Count at 1 (something forgot to release).
// Player disconnects to the menu and reconnects -- the process never died.
// Mission B: the first Acquire() takes s_Count from 1 to 2, so the "==1" branch
// never runs. The bug appears on the mission AFTER the mistake was made.The result is a bug that surfaces one mission later than its cause, and restarting the game to investigate makes it vanish -- which is exactly why it tends to get filed as "could not reproduce." Any mod-level singleton, counter, cache, or registry with mutable static state needs an explicit Cleanup() called from mission teardown (MissionGameplay.OnMissionFinish() client-side, MissionServer.OnMissionFinish() server-side):
static void Cleanup()
{
s_Count = 0; // reset scalars unconditionally
s_Owners = null; // null every static ref -- a live one is a GC root and leaks
} // its whole object graph into the next missionReset the state unconditionally, not only inside a "recover gracefully" branch that itself bails out when there is no live mission -- that guard is true precisely during teardown, which is exactly when the reset needs to run.
There Is No Object.IsDeleted() -- the Null Check Is the Lifetime Check
Enforce Script has no built-in way to ask "has this object been removed from the world?" It is tempting to write one using IsDamageDestroyed(), since the name sounds close enough:
// WRONG -- this answers a HEALTH question, not a LIFETIME question
bool IsDeleted(EntityAI e) { return e.IsDamageDestroyed(); }IsDamageDestroyed() is true for a corpse or a wrecked vehicle -- an object that still very much exists in the world. A guard written as if (!thing.IsDeleted()) Delete(thing); actually reads as "delete this only while it is still alive," which is backwards from what most callers intend, and both methods return an ordinary bool -- nothing about the mistake is visible at the call site or from the compiler.
When the engine actually removes an entity, every raw (non-ref) reference to it goes null -- that null is the only honest "is this still around" signal script has:
static void DespawnAndClean(EntityAI e)
{
if (!e) return; // already gone -- this IS the lifetime check
GetGame().ObjectDelete(e);
}Never pair IsDeleted()-style checks with IsAlive() on the same object expecting them to mean different things -- on a corpse, both answer the identical underlying fact (IsAlive() is itself defined as !IsDamageDestroyed()), so one of the branches becomes silently unreachable.
Real-World Example: Proper Manager Class
Here is a complete example showing proper memory management patterns for a typical DayZ mod manager:
class MyZoneManager : Managed
{
// Singleton instance -- the only strong ref keeping this alive
private static ref MyZoneManager s_Instance;
// Owned collections -- manager is responsible for these
protected ref array<ref MyZone> m_Zones;
protected ref map<string, ref MyZoneConfig> m_Configs;
// Weak reference to external system -- we don't own this
protected PlayerBase m_LastEditor;
void MyZoneManager()
{
m_Zones = new array<ref MyZone>;
m_Configs = new map<string, ref MyZoneConfig>;
}
void ~MyZoneManager()
{
// Explicit cleanup (optional -- ARC handles it, but good practice)
m_Zones.Clear();
m_Configs.Clear();
m_LastEditor = null;
Print("[MyZoneManager] Destroyed");
}
static MyZoneManager GetInstance()
{
if (!s_Instance)
{
s_Instance = new MyZoneManager();
}
return s_Instance;
}
static void DestroyInstance()
{
s_Instance = null; // Releases the strong ref, triggers destructor
}
void CreateZone(string name, vector center, float radius, PlayerBase editor)
{
ref MyZoneConfig config = new MyZoneConfig(name, center, radius);
m_Configs.Set(name, config);
ref MyZone zone = new MyZone(config);
m_Zones.Insert(zone);
m_LastEditor = editor; // Weak reference -- we don't own the player
}
void RemoveZone(int index)
{
if (!m_Zones.IsValidIndex(index))
return;
MyZone zone = m_Zones.Get(index);
string name = zone.GetName();
m_Zones.RemoveOrdered(index); // Strong ref released, zone may be deleted
m_Configs.Remove(name); // Config ref released, config deleted
}
MyZone FindZoneAtPosition(vector pos)
{
foreach (MyZone zone : m_Zones)
{
if (zone.ContainsPosition(pos))
return zone; // Return weak reference to caller
}
return null;
}
}
class MyZone : Managed
{
protected string m_Name;
protected vector m_Center;
protected float m_Radius;
protected MyZoneConfig m_Config; // Weak -- config is owned by manager
void MyZone(MyZoneConfig config)
{
m_Config = config; // Weak reference
m_Name = config.GetName();
m_Center = config.GetCenter();
m_Radius = config.GetRadius();
}
string GetName() { return m_Name; }
bool ContainsPosition(vector pos)
{
return vector.Distance(m_Center, pos) <= m_Radius;
}
}
class MyZoneConfig : Managed
{
protected string m_Name;
protected vector m_Center;
protected float m_Radius;
void MyZoneConfig(string name, vector center, float radius)
{
m_Name = name;
m_Center = center;
m_Radius = radius;
}
string GetName() { return m_Name; }
vector GetCenter() { return m_Center; }
float GetRadius() { return m_Radius; }
}Memory ownership diagram for this example
MyZoneManager (singleton, owned by static s_Instance)
|
|-- ref array<ref MyZone> m_Zones [STRONG -> STRONG elements]
| |
| +-- MyZone
| |-- MyZoneConfig m_Config [WEAK -- owned by m_Configs]
|
|-- ref map<string, ref MyZoneConfig> m_Configs [STRONG -> STRONG elements]
| |
| +-- MyZoneConfig [OWNED here]
|
+-- PlayerBase m_LastEditor [WEAK -- owned by engine]When DestroyInstance() is called:
s_Instanceis set to null, releasing the strong referenceMyZoneManagerdestructor runsm_Zonesis released -> array is deleted -> eachMyZoneis deletedm_Configsis released -> map is deleted -> eachMyZoneConfigis deletedm_LastEditoris a weak reference, nothing to clean up- All memory is freed. No leaks.
Best Practices
- Use
reffor class members your class creates and owns; use raw pointers (no keyword) for back-references and external observations. - Always extend
Managedfor pure-script classes -- it ensures weak references are zeroed on delete, preventing dangling pointer crashes. - Break reference cycles by making the child hold a raw pointer to its parent: parent owns child (
ref), child observes parent (raw). - Use
array<ref MyClass>when the collection owns its elements;array<MyClass>holds weak references that will not keep objects alive. - Prefer ARC-driven cleanup over manual
delete-- let the lastrefrelease trigger the destructor naturally.
Ownership Patterns in Practice
| Pattern | Where you see it | Detail |
|---|---|---|
Parent ref + child raw back-pointer | UI code everywhere | A menu owns its tabs and handlers with ref; each tab holds a raw pointer back to its parent to avoid a cycle. Vanilla's ScriptedWidgetEventHandler is itself declared : Managed (scripts/1_core/proto/enwidgets.c), so raw back-pointers null out safely when the handler is deleted |
static ref singleton + shutdown nulling | Framework manager classes | Setting s_Instance = null in a static shutdown method releases the last strong reference and triggers the whole destructor chain (see Singletons) |
ref array<ref T> for owned collections | Vanilla scripts throughout | Both the array and its elements are ref -- e.g. private ref array<ref CallQueueContext> m_commands; in scripts/4_world/classes/contextmenu.c |
| Raw pointers for engine entities (players, items) | Any script referencing world objects | Entity lifetime belongs to the engine: entities are spawned by the game world and destroyed via GetGame().ObjectDelete(). Scripts hold raw pointers, and because entities inherit Managed through IEntity (scripts/1_core/proto/enentity.c), those pointers are nulled when the entity despawns -- but you still null-check every use |
Theory vs Practice
| Concept | Theory | Reality |
|---|---|---|
autoptr for local variables | Should auto-delete at scope exit | Locals are already implicitly strong references, so autoptr adds nothing; vanilla uses it in only a handful of files and most mod codebases avoid it entirely in favor of ref |
| ARC handles all cleanup | Objects freed when refcount hits zero | Reference cycles are never collected -- they leak permanently until server restart |
delete for immediate cleanup | Destroys the object right away | Can null out references held by other systems unexpectedly -- prefer letting ARC handle it |
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
Two objects with ref to each other | Reference cycle, permanent memory leak | One side must be a raw (weak) reference |
array<MyClass> instead of array<ref MyClass> | Elements are weak references, objects may be deleted immediately | Use array<ref MyClass> for owned elements |
| Accessing a raw pointer after the object was deleted | Crash (dangling pointer on non-Managed classes) | Extend Managed and always null-check weak references |
| Not checking weak references for null | Crash when the referenced object has been deleted | Always: if (weakRef) { weakRef.DoThing(); } |
Using delete on objects owned by another system | The owner's ref becomes null unexpectedly | Let the owner release the object through ARC |
Storing ref to engine entities (players, items) | Can fight with engine lifetime management | Use raw pointers for engine entities |
Forgetting ref on class member collections | Collection is a weak reference, may be collected | Always: protected ref array<...> m_List; |
Circular parent-child with ref on both sides | Classic cycle; neither parent nor child is ever freed | Parent owns child (ref), child observes parent (raw) |
Decision Guide: Which Pointer Type?
Is this a class member that this class CREATES and OWNS?
-> YES: Use ref
-> NO: Is this a back-reference or external observation?
-> YES: Use raw pointer (no keyword), always null-check
-> NO: Is this a local variable in a function?
-> YES: A plain declaration is fine (locals are implicitly strong)
-> The legacy autoptr keyword adds nothing here -- skip it
Storing objects in a collection (array/map)?
-> Objects OWNED by the collection: array<ref MyClass>
-> Objects OBSERVED by the collection: array<MyClass>
Function parameter that must never be null?
-> Use notnull modifierQuick Reference
// Raw pointer (weak reference for class members)
MyClass m_Observer; // Does NOT keep object alive
// Set to null on delete (Managed only)
// Strong reference (keeps object alive)
ref MyClass m_Owned; // Object lives until ref is released
ref array<ref MyClass> m_List; // Array AND elements are strongly held
// Auto pointer (legacy strong reference -- prefer ref)
autoptr MyClass local; // Released when scope exits, same as a plain local
// notnull (contract: never pass null; enforcement undocumented)
void Func(notnull MyClass obj); // Null-check at the call site
// Manual delete (immediate, bypasses ARC)
delete obj; // Destroys immediately, nulls all refs (Managed)
// Break reference cycles: one side must be weak
class Parent { ref Child m_Child; } // Strong -- parent owns child
class Child { Parent m_Parent; } // Weak -- child observes parent