I/O, Files, and Streams

A code-first guide to streams, files, directories, readers, writers, watchers, compression, hashing, and storage.

System.IONative resourcesPlatform-qualified

The I/O model in one view

Sharp Runtime’s I/O surface is a native C++ implementation of a practical subset of System.IO. The familiar names are useful when porting, but the storage underneath is ordinary C++: streams are polymorphic objects, files are reached through std::fstream and std::filesystem, and ownership is explicit. There is no managed finalization pass to rescue an object whose lifetime was designed incorrectly.

The core IO component owns byte streams, text and binary readers and writers, filesystem helpers, metadata objects, positional descriptor I/O, drive information, unmanaged-memory access, and the Linux file watcher. Compression, ZIP, hashing, storage-path selection, and isolated storage are separate physical components so consumers can select only the surface they need.

NeedCMake targetWhat it addsImportant dependency
Streams, files, paths, readers, writers, watcherSharpRuntime::IOThe core System::IO surfacePublic: Core.Base, Uri; private: TimeZone
GZip and Deflate streamsSharpRuntime::IO.CompressionCompression streams and optionsPrivate ZLIB; public Buffers, Core.Base, IO
ZIP archivesSharpRuntime::IO.Compression.ZipZipArchive, entries, modes, levelsPrivate vendored miniz
Non-cryptographic hashesSharpRuntime::IO.HashingAdler, CRC, xxHash and the incremental base APIPublic Core.Base, IO
Application-scoped storageSharpRuntime::IO.IsolatedStorageStore-relative file and directory operationsPrivate Storage path provider
Select the narrow target

Request the component that owns the public header you include. CMake enables its dependency closure. A consumer of IO.Compression, for example, does not need to list IO again.

Surface map

The core component is broad enough that choosing a type by name alone is not always sufficient. This map separates ownership, buffering, representation, and platform concerns.

FamilyRepresentative typesUse it forRead before porting
Byte streamsStream, MemoryStream, FileStream, BufferedStream, UnmanagedMemoryStreamSequential or seekable byte I/O behind one virtual interfaceCapability defaults, close behavior, and ownership differ by concrete stream
Binary recordsBinaryReader, BinaryWriterLittle-endian primitives, 7-bit lengths, UTF-8 stringsThey borrow a raw Stream*; the stream must outlive the wrapper
TextTextReader, TextWriter, StreamReader, StreamWriter, StringReader, StringWriterLines and native stringsStreamReader is a byte-to-character reader, not a general encoding decoder
Whole-file helpersFileRead, write, append, copy, move, delete, and existence checksText helpers operate on native string bytes and do not offer encoding/BOM options
Directories and pathsDirectory, PathEager enumeration, creation, deletion, movement, normalization, and path partsThe implemented overload set is smaller than .NET’s and Windows root semantics are reduced
Metadata objectsFileSystemInfo, FileInfo, DirectoryInfo, DriveInfoObject-shaped metadata and operationsSymbolic-link APIs, several enumeration overloads, and full drive classification are absent
Positional I/ORandomAccessRead or write at a file offset without changing a shared positionIt borrows a native descriptor and throws on Emscripten
Filesystem eventsFileSystemWatcherSingle-directory create, delete, change, and rename events on LinuxNo recursive watch and no Windows/macOS backend
Raw native memoryUnmanagedMemoryStream, UnmanagedMemoryAccessorExpose caller-owned memory through stream or primitive-access APIsThe caller owns the buffer and must keep it alive

Streams are capability contracts

Stream supplies a common byte-oriented protocol, not a promise that every operation works. Derived types must implement Read, Close, and Length. The base implementation rejects writing and resizing, derives single-byte reads from Read, derives seeking from Position and Length, and makes Flush a no-op.

Capability properties are virtual defaults rather than abstract members. The base claims readable, not writable, and not seekable. A custom stream that overrides Write but forgets CanWrite can write in practice while readers of its contract reject it. Conversely, an unreadable custom stream that inherits the default CanRead=true is accepted by reader constructors and fails later. Implementers must override every capability whose answer differs from the base, including changes caused by closing the stream.

Read operations may return fewer bytes than requested and return zero at end-of-stream. A caller that needs a complete record must loop or use a reader method whose contract promises an exact count. Write behavior depends on the concrete stream. RandomAccess::Write, for example, retries native short writes until the requested range is complete; the abstract Stream::Write contract does not turn every subclass into an exact-write transport automatically.

See Streams, readers, and writers for the capability matrix, ownership rules, disposal behavior, binary format, and text-encoding boundaries.

Filesystem operations are eager and native

File and Directory are static convenience surfaces. FileInfo and DirectoryInfo retain an absolute path and expose instance-shaped operations. Both styles ultimately use the host filesystem. They do not virtualize naming, permissions, link resolution, sharing, case sensitivity, timestamp resolution, or atomicity into one universal .NET-like filesystem.

Current directory enumeration returns vectors. There are no streaming Enumerate* forms, and the core directory helpers do not expose recursive SearchOption or EnumerationOptions overloads. Large or untrusted directory trees therefore need an application-specific traversal rather than assuming the complete .NET enumeration surface exists.

FileSystemInfo translates path-resolution failures into Sharp Runtime exceptions instead of allowing std::filesystem::filesystem_error to escape its constructor. On POSIX, creation time is approximated with inode metadata-change time where portable birth-time data is unavailable. That distinction matters for backup, synchronization, and audit software.

See Files, directories, paths, and metadata for the exact helper inventory, deletion and movement rules, positional I/O, timestamp caveats, and platform differences.

Ownership and lifetime

The I/O API uses several ownership styles. Confusing them is a more serious bug than choosing the wrong convenience method.

ObjectBacking resourceOwnership ruleInvalidation risk
FileStreamIts std::fstreamOwns and closes it; destructor is the RAII boundaryPublic operations that require the file throw after Close
MemoryStreamInternal std::vector<bytecs>Owns a copy of constructor bytesA reference from GetBuffer can be invalidated by later growth
BufferedStreamRaw Stream*Borrowed by default; optionally closes it when constructed as ownerThe wrapped stream must outlive a non-owning wrapper
Reader/writer wrappersRaw Stream*Borrow it; leaveOpen controls whether close/destruction closes the streamThe pointer dangles if the stream dies first
UnmanagedMemoryStreamRaw byte bufferNever frees itThe buffer must outlive the stream and must not move
RandomAccessNative integer descriptorNever opens, closes, duplicates, or extends its lifetimeClosing or reusing the descriptor concurrently is the caller’s bug
Close and ownership are separate questions

leaveOpen=true means the wrapper does not close the underlying stream. At the selected source revision, the text wrappers also remain usable after their own Close; that is a documented compatibility gap, not a license to rely on the behavior. Design code so wrappers are not reused after close.

Text and binary representations

BinaryReader and BinaryWriter define an explicit little-endian record format. Their string format is UTF-8 bytes preceded by a 7-bit encoded byte length. Character reads decode UTF-8 into charcs UTF-16 code units, retaining a low surrogate when a supplementary scalar crosses a caller-supplied boundary. This is a specific binary protocol, not host-endian serialization of arbitrary C++ objects.

The text wrappers are narrower. StreamReader reads individual bytes as Latin-1/ASCII-like characters and does not detect a BOM or decode multibyte UTF-8. StreamWriter writes the bytes already present in the native std::string. If an application needs validated Unicode transcoding, it should use the Text component’s encoding APIs before or after the I/O operation.

Errors are part of the contract

The implementation increasingly translates native failures into System-shaped exceptions at public doors: invalid ranges use argument exceptions, a closed stream uses ObjectDisposedException, access mismatches use NotSupportedException, and filesystem failures use the relevant IOException family. This does not erase every host difference. Permissions, sharing, missing parents, symlinks, special files, full disks, and unavailable platform services can still produce platform-specific messages and timing.

Existence checks intentionally answer false for empty paths and native lookup errors rather than throwing. Mutating operations are not silent: for example, File::Delete rejects a directory instead of letting std::filesystem::remove remove an empty one, and non-overwriting moves reject an existing destination.

Platform boundaries

  • Core file, directory, path, stream, and drive surfaces build on the standard library, but their visible behavior still follows the host filesystem.
  • RandomAccess uses Win32 overlapped file operations on Windows and pread/pwrite/fsync on POSIX. It throws PlatformNotSupportedException on Emscripten.
  • FileSystemWatcher has a real Linux/inotify backend only. Enabling it elsewhere throws.
  • Path separators and text-writer line endings are selected at compile time. The Path model does not reproduce the complete Windows drive/UNC grammar.
  • Android and Emscripten storage roots belong to the separate Storage component and require application integration; see Platforms and portability.

The physical component split is also a semantic boundary. IO.Compression provides GZip and raw Deflate stream adapters backed privately by ZLIB. IO.Compression.Zip owns ZIP archive behavior and its private miniz implementation. IO.Hashing contains non-cryptographic checksum and hash algorithms; it is not a substitute for the cryptographic component. IO.IsolatedStorage interprets names relative to an application storage root and now rejects ordinary lexical and resolved-symlink escapes, but it is not an operating-system security sandbox and retains documented check-then-use and direct-constructor boundaries.

Continue into the subsystem