Windows Fiber Local Storage: Cleanup Callbacks, Slot Ownership, and Index Retirement
Design Windows FLS ownership around callback lifetime, per-fiber slots, checked allocation, explicit cleanup, and process-wide index retirement.
Windows fiber local storage, or FLS, gives each fiber a separate slot for a process-local index. Its optional cleanup callback is useful for scratch buffers and runtime state whose lifetime follows execution contexts. The difficult part is not obtaining a pointer. It is defining who owns the pointed allocation, when a slot becomes empty, and when the process may retire an index without racing its users.
An FLS slot is not a garbage collector, a module reference, or a global shutdown barrier. A correct design combines the documented callback triggers with an explicit application lifecycle. This article focuses on that cleanup contract rather than repeating the mechanics of ordinary thread-local storage.
Separate the index from each stored value
FlsAlloc creates an index usable by fibers in the current process. Each fiber has its own slot at that index, initially zero. The index itself is shared infrastructure; the value retrieved by FlsGetValue belongs to the calling fiber’s slot.
The allocation failure sentinel is FLS_OUT_OF_INDEXES, not zero. Index zero can be a valid allocation. On failure, capture GetLastError before unrelated operations overwrite diagnostic state. Do not publish a failed allocation to callers or use a default-initialized integer as evidence that initialization succeeded.
Microsoft’s fiber documentation distinguishes FLS from TLS when an application switches fibers on one thread. FLS follows the fiber. TLS follows the thread running that fiber. In code without fiber switching, FLS can still provide per-thread-like storage and its cleanup callback, but that does not justify assuming all applications have the same execution-context model.
An index has no portable meaning in another process. It should not be written into an IPC message or persisted as a durable resource identifier. A subsystem should expose operations using its owned index, not invite arbitrary callers to reuse raw numeric values after retirement.
Make the allocation family part of ownership
The callback receives the stored value. Windows does not infer whether that pointer came from new, a process heap, a pool, or a borrowed object. The cleanup routine must match the allocation family and must only destroy resources the slot actually owns.
The following Windows C++ fragment uses the process heap for a small scratch object. It assumes a valid, live index supplied by an already initialized subsystem. It intentionally avoids retrieval ambiguity by installing a fresh object only into a slot the caller has established is empty.
#include <windows.h>
struct Scratch {
unsigned char bytes[256];
};
VOID CALLBACK DestroyScratch(PVOID value)
{
if (value != nullptr)
HeapFree(GetProcessHeap(), 0, value);
}
DWORD InstallIntoEmptySlot(DWORD liveIndex)
{
auto* scratch = static_cast<Scratch*>(
HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(Scratch)));
if (scratch == nullptr)
return ERROR_NOT_ENOUGH_MEMORY;
if (!FlsSetValue(liveIndex, scratch)) {
DWORD error = GetLastError();
HeapFree(GetProcessHeap(), 0, scratch);
return error;
}
return ERROR_SUCCESS;
}
// Subsystem initialization, before admitting users:
// DWORD index = FlsAlloc(DestroyScratch);
// if (index == FLS_OUT_OF_INDEXES) { /* record GetLastError() */ }
The caller must not call this installer twice for an occupied slot. Replacing the pointer would lose the application’s ownership path to the old object unless it explicitly retains and frees it. The example is a contract fragment, not a complete lazy-initialization library. It needs lifecycle synchronization and an empty-slot protocol around it.
FlsSetValue has its own success result. If installation fails, the allocation remains application-owned and must be released. A pointer allocation succeeding does not prove that the slot accepted it. Microsoft documents invalid-parameter and out-of-memory errors for setting the value; preserve the actual error rather than reducing every failure to a generic allocation problem.
Do not confuse a null value with proven retrieval success
FlsGetValue returns the stored pointer on success and null on failure. An empty slot also contains a null value. The pointer alone therefore does not prove why retrieval returned null.
Keep index validity and subsystem state explicit. The retrieval documentation does not provide the same detailed last-error success semantics developers may remember from other APIs, so do not import a TLS-specific error-clearing assumption without checking the FLS contract for the target environment. During debugging, a null result after shutdown should trigger investigation of lifecycle validity, not automatic recreation of state using a possibly retired index.
Return meaningful states from an application wrapper: uninitialized subsystem, retired subsystem, empty valid slot, present object, and operational failure. Collapsing them into one nullable pointer makes shutdown bugs resemble harmless first use. It also encourages code to allocate new data while the component is trying to stop.
Callback triggers define cleanup opportunities
Microsoft documents callback invocation when an in-use slot encounters fiber deletion, thread exit, or index freeing. The callback type has no return status. This is an appropriate place for bounded resource cleanup, not the only place to discover whether a critical persistence operation succeeded.
For index freeing specifically, FlsFree invokes the associated callback for fibers whose corresponding slots contain non-null values. A cleanup routine that frees heap objects must not be combined with a separate path that frees the same object while leaving its pointer in the slot. Explicit early destruction should clear the current slot successfully before releasing the allocation, so a later callback does not receive a dangling owner pointer.
If clearing fails, stop and diagnose the index or lifecycle error. Freeing the object anyway can leave stale state behind. If an application transfers an allocation elsewhere, clearing the slot and transferring ownership must form one coherent operation from the application’s perspective.
Keep cleanup independent of assumed callback order, user-interface thread affinity, or other fibers’ still-live state. None of those assumptions is needed for a self-contained heap buffer. A callback that reaches into a scheduler, logger, or unloaded library creates a much larger shutdown dependency graph.
Retiring an index affects the entire process
FlsFree is not a request to clear only the calling fiber’s value. It releases the index across instances of FLS in the current process and makes the index available for reuse. This is a subsystem-wide transition.
A safe application-level retirement protocol first stops admitting new users, then waits for existing users to leave the component’s protected operations, and only then frees the index. Record failure from FlsFree and do not announce completed retirement when the API reports failure. The required synchronization belongs to the surrounding component; FLS does not automatically implement that protocol.
Avoid saving a raw index in long-lived tasks that can outlive its owner. Reuse can turn a stale number into another component’s live index, which is worse than a clean failure. A generation-bearing application handle or a checked subsystem reference can prevent callers from confusing an old lifecycle with the new one.
Callback code must remain loaded while Windows can invoke it. In a DLL, teardown must follow the documented DLL lifecycle constraints; the FlsFree documentation specifically discusses calls during DLL_PROCESS_DETACH. A generic plugin-unload recipe cannot be derived from a heap-buffer example. Treat module lifetime and the loader’s rules as separate integration work, and avoid promising safe unload merely because the current slot is empty.
Test lifecycle behavior, not just pointer isolation
Build a Windows-native fixture that creates multiple fibers, installs independently identifiable objects, and verifies that each fiber sees its own value. Track callback invocation using controlled test instrumentation, then exercise fiber deletion, thread exit, and explicit index retirement separately.
Include an explicit early-clear case and prove the object is destroyed exactly once. Include installation failure handling in a wrapper that can inject API failures. Test that new users are rejected after retirement begins and that stale application handles cannot allocate into a replacement lifecycle.
Run these checks on the supported Windows versions and architectures. The fragment above was source-reviewed against Microsoft documentation, not executed on a Windows host during authoring. Callback cleanup is production-ready only when the application verifies its own admission, ownership, and teardown protocol in addition to the API contract.
Related:
- Windows Thread-Local Storage: TLS Slots, Ownership, and Fiber Boundaries
- Windows One-Time Initialization: Safe Lazy Setup with InitOnceExecuteOnce
Sources: