File I/O

Reflex reads and writes files at two levels:

  • the whole file in one call, with File::Open and File::Save
  • streaming through a System::FileHandle, with the File:: helpers

Start at the top. Drop down to a handle only when you need more control.

  • Whole file. Configs, assets, documents that fit comfortably in memory. No handle to manage, no cleanup.
  • Streaming. Large files, line-by-line parsing, header sniffing, seeking. FileHandle::Create plus ReadBytes, ReadLine, ReadValue.

To make the file calls concrete, we will build a small waveform. Point it at an audio file and it draws the shape of the sound: peaks where the signal is loud, flat stretches where it is quiet.


Open

Open the source audio and decode it into samples. File::Open reads the whole file into a Data::Archive, a byte buffer, and an app-side decoder turns those bytes into one channel of Float32 samples in the range [-1, 1]. That is the equivalent of Web Audio decodeAudioData followed by getChannelData.

auto bytes = File::Open(path);
if (!bytes) return false;                     // missing or unreadable

Array<Float32> samples;
UInt           rate = 0;
LoadWav(bytes.GetData(), bytes.GetSize(), samples, rate);   // WAV bytes -> Float32 [-1,1]
  • File::Open does not throw. On a missing or unreadable file it returns an empty Data::Archive, which is falsy, so if (!bytes) is the whole guard.
  • The bytes come out through GetData() and GetSize(). What decodes them is yours: LoadWav here parses uncompressed WAV and returns false on bytes it cannot read; swap in your own codec for MP3, FLAC, or OGG. The WaveformViewer demo can also synthesise its samples, so it ships with no external file.
  • File::Open takes a WString. Paths often arrive as a WString::View, so wrap them at the call: File::Open(WString(view)).

That same truthiness gives you a compact form when you use the bytes right away, binding and testing in one step:

if (auto request = File::Open(request_path))
{
    return Data::DecodePropertySet(Data::kJsonFormat, request);
}
// else: file absent or unreadable, fall back

One caveat. A zero-byte file is also empty, so empty means no bytes, not strictly an error. To tell an empty file from a missing one, gate on File::Exists(path) first.


Save

With samples in hand, persist them. Build a Data::PropertySet, the tree Reflex uses for any structured data, then encode it as JSON and hand the bytes to File::Save in one call.

Data::PropertySet json;
auto keymap = Data::AcquireKeyMap(json);

auto rate_key   = Data::RegisterKey(keymap, "sample_rate");
auto sample_key = Data::RegisterKey(keymap, "sample");

Data::SetInt64(json, rate_key, m_sample_rate);

auto channel = Data::AcquirePropertySetArray(json, Data::RegisterKey(keymap, "samples"));

for (UInt i = 0; i < m_samples.GetSize(); ++i)
{
	auto node = Data::AddPropertySet(channel);
	Data::SetFloat32(node, sample_key, m_samples[i]);
}

File::Save(WString(path), Data::EncodePropertySet(Data::kJsonFormat, json));
  • Keys are compile-time hashed 32-bit integers. Register them once against the key map, then reuse the handles.
  • Data::EncodePropertySet(Data::kJsonFormat, json) turns the tree into bytes; File::Save writes them. The save call is the same one you would use for a raw buffer, only the source differs.
  • For a long recording you would usually save the raw sample bytes with File::Save directly. JSON earns its place when you want the file human readable or carrying metadata like the sample rate.

Read

Reading is the mirror image. File::Open feeds its bytes straight into Data::DecodePropertySet, and you pull the values back out by key.

auto json = Data::DecodePropertySet(Data::kJsonFormat, File::Open(WString(path)));

m_sample_rate = UInt(Data::GetInt64(json, "sample_rate"));

auto channel = Data::GetPropertySetArray(json, "samples");

m_samples.Clear();

for (auto& i : channel)
{
	m_samples.Push(Data::GetFloat32(*i, "sample"));
}
  • File::Open nests inside the decode call, so a missing file decodes to an empty set and the getters return safe defaults. No cast checks, no exceptions.
  • GetPropertySetArray is safe to iterate even when the key is absent, so a malformed file simply comes back as no samples.

The symmetry

bytes you built Array<UInt8> File::Save disk file File::Open bytes you receive Data::Archive write is build then Save, read is Open then check GetSize()

Whatever the format, the shape is the same. Save is build then File::Save; read is File::Open then decode. JSON rides on top of that byte round-trip, it does not replace it.


Draw

A waveform has far more samples than the view has pixels, so first reduce each column of samples to a min and a max, then paint one bar per column.

Reduce. This is the browser computePeaks, one interleaved min, max pair per column:

void ComputePeaks(const Float32* samples, UInt count, UInt spp, Array<Float32>& peaks)
{
	peaks.Clear();

	for (UInt i = 0; i < count; i += spp)
	{
		Float32 mn =  1.0f;
		Float32 mx = -1.0f;

		UInt end = Min(i + spp, count);

		for (UInt j = i; j < end; ++j)
		{
			Float32 v = samples[j];
			if (v < mn) mn = v;
			if (v > mx) mx = v;
		}

		peaks.Push(mn);      // interleaved: one (min, max) pair per column
		peaks.Push(mx);
	}
}

Then draw. Reflex assembles the canvas once through GLX::SetColourCanvas, exactly where the browser looped over ctx.fillRect:

GLX::SetColourCanvas(*this, {}, [this](GLX::ColourCanvasContext& ctx)
{
	UInt    columns = m_peaks.GetSize() / 2;
	Float32 mid     = ctx.size.h * 0.5f;
	Float32 step    = ctx.size.w / Float32(columns);

	const GLX::Colour wave = { 0.20f, 0.80f, 0.75f, 1.0f };   // teal

	for (UInt x = 0; x < columns; ++x)
	{
		Float32 mn = m_peaks[x * 2];
		Float32 mx = m_peaks[x * 2 + 1];

		Float32 top    = mid + mn * mid;         // mn <= 0, so the bar starts above centre
		Float32 height = (mx - mn) * mid;

		GLX::AddRectFill(ctx.output, wave, { { Float32(x) * step, top }, { step, height } });
	}
});
  • Each column becomes one filled bar, starting above the centre line and spanning the full min to max swing.
  • This is the only step that draws rather than moves bytes. It is here so the round-trip has a visible result.

Paths

Absolute Windows paths need a leading slash, and forward slashes or escaped backslashes:

"/C:/Users/Name/file.txt"      OK
"/C:\\Users\\Name\\file.txt"   OK
"/C:\Users\Name\file.txt"      FAILS (unescaped backslashes)

Files bundled with your app use the :res: prefix and read through the same call:

Data::Archive style = File::Open(L":res:MyApp/styles.glx");

Stream with a FileHandle

For incremental reading, create a handle:

auto fh = System::FileHandle::Create(L"/C:/data/log.txt", System::FileHandle::kModeRead);

kModeRead is the default, so it can be omitted. The other modes are kModeOverwrite and kModeAppend, for writing through the same handle API.

Create is [[nodiscard]], so check the returned handle. A null ref (the FileHandle::null sentinel) means the open failed. From there every call reports its own result rather than throwing:

CallFailure signal
Read(ptr, max)bytes read; short or 0 is EOF or error
Write(ptr, size)bytes written
Truncate(), Flush(commit)false on failure
File::ReadLine(fh, out)false at EOF or failure
File::Save, Copy, Delete, Rename, MakeDirectoryfalse on failure

Bytes

Data::Archive all   = File::ReadBytes(fh);          // everything from the current position
Data::Archive chunk = File::ReadBytes(fh, 256);     // next 256 bytes
Data::Archive ahead = File::Peek(fh, 4);            // read 4 bytes WITHOUT advancing
UInt64        left  = File::GetRemainder(fh);       // bytes between position and end

Peek leaves the read position untouched. Reach for it when you need to sniff a file header (magic bytes) before deciding how to parse the rest.

Lines

CString aline;
while (File::ReadLine(fh, aline)) { /* aline decoded as ASCII */ }

WString wline;
while (File::ReadLine(fh, wline)) { /* wline decoded from UTF-8 */ }

Both overloads return false when there is nothing left, so they drive a while loop cleanly. Pick the WString overload for any text that may contain non-ASCII characters; it decodes UTF-8 for you. File::WriteLine mirrors both for the write direction and appends the newline itself.

Typed values

UInt32 magic = File::ReadValue<UInt32>(fh);   // reads sizeof(UInt32) bytes

Header h;
if (File::ReadValue(fh, h)) { /* h filled, false on a short read */ }

Only raw-packable types compile; the header enforces this with a static assert. File::WriteValue mirrors it for writing.

Position

UInt64 size = fh->GetSize();
fh->SetPosition(0);            // rewind
UInt64 pos = fh->GetPosition();

Where to go next