Storage
Implementation status: The Storage namespace is implemented. Headers are present and the API is functional for file I/O on all supported platforms.
Overview
The Microsoft::Xna::Framework::Storage namespace provides a platform-independent abstraction for save-game and user-data persistence. On the original Xbox 360, StorageDevice represented a physical memory unit or hard drive, and device selection was presented to the player as a system UI. On PC, the selector returned immediately with the local user profile directory.
CNA preserves the full XNA 4.0 API shape, including the Begin/End asynchronous pattern, but all operations complete synchronously on current platforms. The StorageContainer maps directly to a directory on the local filesystem, and StorageDevice represents that location. The async wrappers exist purely for source-level XNA compatibility and return immediately.
Namespace members at a glance
| Type | Description | Status |
|---|---|---|
StorageDevice |
Represents a storage location (local disk). Obtained via the async selector. | Implemented |
StorageContainer |
Opened from a StorageDevice; provides file and directory access within a named save scope. |
Implemented |
StorageDeviceNotConnectedException |
Thrown when a previously valid StorageDevice is no longer available. |
Implemented |
StorageDevice
StorageDevice is the entry point into the storage system. On PC (and in CNA on all platforms) it represents the local filesystem. To obtain a device, use the static Begin/End async selector pattern. On a PC target the selector returns immediately with a device backed by the current user's profile directory.
Device selection (Begin/End pattern)
| Method | Description |
|---|---|
StorageDevice::BeginShowSelector(callback, state) |
Starts the (synchronous on CNA) device-selection operation. callback is an AsyncCallback invoked when the result is ready; state is an arbitrary user object passed through to the callback. |
StorageDevice::EndShowSelector(result) |
Retrieves the selected StorageDevice from the IAsyncResult returned by BeginShowSelector. Returns nullptr if the user cancelled selection (not applicable on CNA, where a device is always available). |
Opening a container
| Method | Description |
|---|---|
device.BeginOpenContainer(name, callback, state) |
Begins opening a StorageContainer with the given display name. The container maps to a subdirectory within the device root. |
device.EndOpenContainer(result) |
Retrieves the opened StorageContainer from the IAsyncResult. The caller is responsible for closing the container when done. |
Platform storage paths
| Platform | Root path |
|---|---|
| Linux / macOS | ~/.local/share/<AppName>/ |
| Windows | %APPDATA%\<AppName>\ |
| Android | getFilesDir() / <AppName> / (internal app storage) |
| Web (Emscripten) | Emscripten persistent FS mount (limited; requires explicit flush) |
On the Web target, writes to the Emscripten persistent filesystem are buffered in memory. Call EM_ASM(FS.syncfs(false, function(){})) after writing to flush data to IndexedDB. Failing to sync means data is lost when the tab closes.
StorageContainer
StorageContainer provides file and directory access scoped to a named save context within a device. It implements the XNA 4.0 API almost entirely. All paths passed to its methods are relative to the container root directory.
File operations
| Method | Returns | Description |
|---|---|---|
OpenFile(path, mode) |
Stream |
Opens or creates a file at path using the given FileMode (Create, Open, OpenOrCreate, Append, Truncate). Returns a Stream for reading or writing. |
FileExists(path) |
bool |
Returns true if the file exists within the container. |
DeleteFile(path) |
void |
Deletes the file at path. Throws if the file does not exist. |
GetFileNames() |
std::vector<std::string> |
Returns the names of all files in the container root (non-recursive). |
GetFileNames(pattern) |
std::vector<std::string> |
Returns file names matching a wildcard pattern (e.g. "*.sav"). |
Directory operations
| Method | Returns | Description |
|---|---|---|
CreateDirectory(path) |
void |
Creates a subdirectory within the container (creates intermediate directories as needed). |
DirectoryExists(path) |
bool |
Returns true if the directory exists within the container. |
GetDirectoryNames() |
std::vector<std::string> |
Returns the names of all subdirectories in the container root (non-recursive). |
GetDirectoryNames(pattern) |
std::vector<std::string> |
Returns directory names matching a wildcard pattern. |
StorageDeviceNotConnectedException
StorageDeviceNotConnectedException is thrown when a StorageDevice that was previously valid is no longer accessible — for example, if a memory unit was ejected on Xbox 360. In CNA, because storage always maps to the local filesystem, this exception is raised only in exceptional circumstances such as the backing directory being removed by an external process after the device was opened.
It derives from ExternalException and carries a human-readable message. Catch it wherever you call BeginOpenContainer or perform file operations if your application needs to handle a missing device gracefully.
try {
auto result = device.BeginOpenContainer("SaveData", nullptr, nullptr);
auto container = device.EndOpenContainer(result);
// ... use container ...
container->Dispose();
} catch (const StorageDeviceNotConnectedException& ex) {
// Storage unavailable — prompt the user to reconnect or select another device
ShowErrorDialog(ex.what());
}
IAsyncResult pattern
XNA 4.0 used the standard .NET IAsyncResult Begin/End async pattern so that storage operations could show system UI (device selector, container namer) without blocking the game loop. The pattern works as follows:
- Call the
Begin*method, supplying an optionalAsyncCallbackand state object. It returns anIAsyncResult. - In the callback (or by polling
IAsyncResult::IsCompleted), call the matchingEnd*method to retrieve the result.
In CNA, all storage operations complete synchronously: the Begin* call performs the operation immediately and the returned IAsyncResult has IsCompleted == true before the callback is even invoked. The pattern is preserved verbatim so that XNA save-game code ports without changes.
Code examples
Saving a file
// Obtain a StorageDevice
StorageDevice* device = nullptr;
StorageDevice::BeginShowSelector(
[](IAsyncResult* result) {
device = StorageDevice::EndShowSelector(result);
}, nullptr);
// Open a container named after your save slot
auto openResult = device->BeginOpenContainer("Slot1", nullptr, nullptr);
auto container = device->EndOpenContainer(openResult);
// Write a save file
if (!container->DirectoryExists("profile"))
container->CreateDirectory("profile");
auto stream = container->OpenFile("profile/save.dat", FileMode::Create);
stream->Write(saveData.data(), 0, static_cast<int>(saveData.size()));
stream->Close();
container->Dispose();
Loading a file
auto openResult = device->BeginOpenContainer("Slot1", nullptr, nullptr);
auto container = device->EndOpenContainer(openResult);
auto stream = container->OpenFile("profile/save.dat", FileMode::Open);
std::vector<uint8_t> buffer(stream->Length());
stream->Read(buffer.data(), 0, static_cast<int>(buffer.size()));
stream->Close();
container->Dispose();
// Deserialise from buffer ...
LoadGameState(buffer);
Checking file existence before loading
auto openResult = device->BeginOpenContainer("Slot1", nullptr, nullptr);
auto container = device->EndOpenContainer(openResult);
if (container->FileExists("profile/save.dat")) {
auto stream = container->OpenFile("profile/save.dat", FileMode::Open);
// ... read and deserialise ...
stream->Close();
} else {
// No save file found — start a new game
StartNewGame();
}
container->Dispose();
Always call container->Dispose() when you are finished with a StorageContainer. On CNA this releases the directory handle; on Xbox 360 it was required to flush and unlock the device. Keeping containers open longer than necessary is discouraged.