Chapter 15 - Streams and IO
Create and Write
// Create a file called test.txt in the current directory:
using (Stream s = new FileStream ("test.txt", FileMode.Create))
{
Console.WriteLine (s.CanRead); // True
Console.WriteLine (s.CanWrite); // True
Console.WriteLine (s.CanSeek); // True
s.WriteByte (101);
s.WriteByte (102);
byte[] block = { 1, 2, 3, 4, 5 };
s.Write (block, 0, block.Length); // Write block of 5 bytes
Console.WriteLine (s.Length); // 7
Console.WriteLine (s.Position); // 7
s.Position = 0; // Move back to the start
Console.WriteLine (s.ReadByte()); // 101
Console.WriteLine (s.ReadByte()); // 102
// Read from the stream back into the block array:
Console.WriteLine (s.Read (block, 0, block.Length)); // 5
// Assuming the last Read returned 5, we'll be at
// the end of the file, so Read will now return 0:
Console.WriteLine (s.Read (block, 0, block.Length)); // 0
}
Async Demo
using (Stream s = new FileStream ("test.txt", FileMode.Create))
{
byte[] block = { 1, 2, 3, 4, 5 };
await s.WriteAsync (block, 0, block.Length); // Write asychronously
s.Position = 0; // Move back to the start
// Read from the stream back into the block array:
Console.WriteLine (await s.ReadAsync (block, 0, block.Length)); // 5
}
// Clean up
File.Delete ("test.txt");
Named Pipe Server
bool messageMode = true; // Be sure Server and Client set the same
if (messageMode)
{
using var s = new NamedPipeServerStream ("pipedream", PipeDirection.InOut,
1, PipeTransmissionMode.Message);
s.WaitForConnection();
byte[] msg = Encoding.UTF8.GetBytes ("Hello");
s.Write (msg, 0, msg.Length);
Console.WriteLine (Encoding.UTF8.GetString (ReadMessage (s)));
}
else
{
using var s = new NamedPipeServerStream ("pipedream");
Console.WriteLine ("Please start Named Pipe Client.");
s.WaitForConnection();
s.WriteByte (100); // Send the value 100.
Console.WriteLine ("Response from Named Pipe Client.");
Console.WriteLine (s.ReadByte());
}
static byte[] ReadMessage (PipeStream s)
{
MemoryStream ms = new MemoryStream();
byte[] buffer = new byte [0x1000]; // Read in 4 KB blocks
do { ms.Write (buffer, 0, s.Read (buffer, 0, buffer.Length)); }
while (!s.IsMessageComplete);
return ms.ToArray();
}
Named Pipe Client
bool messageMode = true; // Be sure Server and Client set the same
if (messageMode)
{
using var s = new NamedPipeClientStream ("pipedream");
s.Connect();
s.ReadMode = PipeTransmissionMode.Message;
Console.WriteLine (Encoding.UTF8.GetString (ReadMessage (s)));
byte[] msg = Encoding.UTF8.GetBytes ("Hello right back!");
s.Write (msg, 0, msg.Length);
}
else
{
using var s = new NamedPipeClientStream ("pipedream");
s.Connect();
Console.WriteLine (s.ReadByte());
s.WriteByte (200); // Send the value 200 back.
}
static byte[] ReadMessage (PipeStream s)
{
MemoryStream ms = new MemoryStream();
byte[] buffer = new byte [0x1000]; // Read in 4 KB blocks
do { ms.Write (buffer, 0, s.Read (buffer, 0, buffer.Length)); }
while (!s.IsMessageComplete);
return ms.ToArray();
}
Stream Reader and Writer
using (FileStream fs = File.Create ("test.txt"))
using (TextWriter writer = new StreamWriter (fs))
{
var nl = string.Join ("", writer.NewLine.Select (c => $"0x{((int)c).ToString("X2")} "));
Console.WriteLine ($"Newline is {nl} ");
writer.WriteLine ("Line1");
writer.WriteLine ("Line2");
}
using (FileStream fs = File.OpenRead ("test.txt"))
using (TextReader reader = new StreamReader (fs))
{
Console.WriteLine (reader.ReadLine()); // Line1
Console.WriteLine (reader.ReadLine()); // Line2
}
Encoding
Console.WriteLine ("Default UTF-8 encoding");
using (TextWriter w = File.CreateText ("but.txt")) // Use default UTF-8 encoding.
w.WriteLine ("but–"); // Emdash, not the "minus" character
using (Stream s = File.OpenRead ("but.txt"))
for (int b; (b = s.ReadByte()) > -1;)
Console.WriteLine (b);
Console.WriteLine("Unicode a.k.a. UTF-16 encoding");
using (Stream s = File.Create ("but.txt"))
using (TextWriter w = new StreamWriter (s, Encoding.Unicode))
w.WriteLine ("but–");
foreach (byte b in File.ReadAllBytes ("but.txt"))
Console.WriteLine (b);
Compression Streams
using (Stream s = File.Create ("compressed.bin"))
using (Stream ds = new DeflateStream (s, CompressionMode.Compress))
for (byte i = 0; i < 100; i++)
ds.WriteByte (i);
new FileInfo ("compressed.bin").Length.Dump ("Length of compressed file");
using (Stream s = File.OpenRead ("compressed.bin"))
using (Stream ds = new DeflateStream (s, CompressionMode.Decompress))
for (byte i = 0; i < 100; i++)
Console.WriteLine (ds.ReadByte()); // Writes 0 to 99
Compressing Text
string[] words = "The quick brown fox jumps over the lazy dog".Split();
Random rand = new Random(0);
using (Stream s = File.Create ("compressed.bin"))
using (Stream ds = new BrotliStream (s, CompressionMode.Compress))
using (TextWriter w = new StreamWriter (ds))
for (int i = 0; i < 1000; i++)
await w.WriteAsync (words [rand.Next (words.Length)] + " ");
new FileInfo ("compressed.bin").Length.Dump ("Length of compressed file");
using (Stream s = File.OpenRead ("compressed.bin"))
using (Stream ds = new BrotliStream (s, CompressionMode.Decompress))
using (TextReader r = new StreamReader (ds))
Console.Write (await r.ReadToEndAsync()); // Output below:
Compressing in Memory
byte[] data = new byte [1000]; // We can expect a good compression
// ratio from an empty array!
var ms = new MemoryStream();
using (Stream ds = new DeflateStream (ms, CompressionMode.Compress))
ds.Write (data, 0, data.Length);
byte[] compressed = ms.ToArray();
Console.WriteLine (compressed.Length); // 11
// Decompress back to the data array:
ms = new MemoryStream (compressed);
using (Stream ds = new DeflateStream (ms, CompressionMode.Decompress))
for (int i = 0; i < 1000; i += ds.Read (data, i, 1000 - i)) ;
Compressing in Memory - alternative
byte[] data = new byte[1000];
MemoryStream ms = new MemoryStream();
using (Stream ds = new DeflateStream (ms, CompressionMode.Compress, true))
await ds.WriteAsync (data, 0, data.Length);
Console.WriteLine (ms.Length); // 113
ms.Position = 0;
using (Stream ds = new DeflateStream (ms, CompressionMode.Decompress))
for (int i = 0; i < 1000; i += await ds.ReadAsync (data, i, 1000 - i)) ;
GZip Stream
async Task Main()
{
string textfile = "myfile.txt";
File.WriteAllText (textfile, RandomString (4096));
var backup = textfile.Replace (".txt", "-Copy.txt");
File.Copy (textfile, backup);
await GZip (textfile);
await GUnzip (textfile + ".gz", false); // Don't delete it so we can list it for comparison
foreach (var fi in Directory
.EnumerateFiles (Environment.CurrentDirectory, "myfile*")
.Select (f => new FileInfo (f)))
Console.WriteLine ($"{fi.Name} {fi.Length} bytes");
File.Delete (textfile);
File.Delete (textfile + ".gz");
File.Delete (backup);
}
async Task GZip (string sourcefile, bool deleteSource = true)
{
var gzipfile = $"{sourcefile}.gz";
if (File.Exists (gzipfile)) throw new Exception ("Gzip file already exists");
// Compress
using (FileStream inStream = File.Open (sourcefile, FileMode.Open))
using (FileStream outStream = new FileStream (gzipfile, FileMode.CreateNew))
using (GZipStream gzipStream = new GZipStream (outStream, CompressionMode.Compress))
await inStream.CopyToAsync (gzipStream); // Or .CopyTo() for non-async code
if (deleteSource) File.Delete (sourcefile);
}
async Task GUnzip (string gzipfile, bool deleteGzip = true)
{
if (Path.GetExtension (gzipfile) != ".gz") throw new Exception ("Not a gzip file");
var uncompressedFile = gzipfile.Substring (0, gzipfile.Length - 3);
if (File.Exists (uncompressedFile)) throw new Exception ("Destination file already exists");
// Uncompress
using (FileStream uncompressToStream = File.Open (uncompressedFile, FileMode.Create))
using (FileStream zipfileStream = File.Open (gzipfile, FileMode.Open))
using (var unzipStream = new GZipStream (zipfileStream, CompressionMode.Decompress))
await unzipStream.CopyToAsync (uncompressToStream); // Or .CopyTo() for non-async code
if (deleteGzip) File.Delete (gzipfile);
}
private static Random rnd = new Random();
// https://stackoverflow.com/a/1344242/141172
public static string RandomString (int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return new string (Enumerable.Repeat (chars, length)
.Select (s => s [rnd.Next (s.Length)]).ToArray());
}
Zip
void Main()
{
AddFilesToFolder();
var zipPath = Path.Combine(DirectoryToZip, "..", "archive.zip");
ZipFile.CreateFromDirectory (DirectoryToZip, zipPath);
Directory.CreateDirectory(DirectoryToExtractTo);
ZipFile.ExtractToDirectory (zipPath, DirectoryToExtractTo);
Console.WriteLine("Extracted files:");
foreach (var file in Directory.EnumerateFiles(DirectoryToExtractTo))
Console.WriteLine(file);
}
string DirectoryToZip
{
get
{
return RuntimeInformation.IsOSPlatform (OSPlatform.Windows) ?
@".\MyFolder" : "./MyFolder";
}
}
string DirectoryToExtractTo
{
get
{
return RuntimeInformation.IsOSPlatform (OSPlatform.Windows) ?
@".\Extracted" : "./Extracted";
}
}
void AddFilesToFolder()
{
Directory.CreateDirectory (DirectoryToZip);
foreach (var c in new char[] { 'A', 'B', 'C' })
File.WriteAllText (Path.Combine (DirectoryToZip, $"{c}.txt"), $"This is {c}");
}
File Attributes
string filePath = @"test.txt";
File.WriteAllText(filePath, "The quick brown fox.");
FileAttributes fa = File.GetAttributes (filePath);
if ((fa & FileAttributes.ReadOnly) != 0)
{
// Use the exclusive-or operator (^) to toggle the ReadOnly flag
fa ^= FileAttributes.ReadOnly;
File.SetAttributes (filePath, fa);
}
Console.WriteLine (new FileInfo ("test.txt").IsReadOnly);
new FileInfo ("test.txt").IsReadOnly = true;
Console.WriteLine (new FileInfo ("test.txt").IsReadOnly);
new FileInfo ("test.txt").IsReadOnly = false;
Console.WriteLine (new FileInfo ("test.txt").IsReadOnly);
// Now we can delete the file, for instance:
File.Delete (filePath);
CompressFolder
void Main()
{
CreateCompressStructure();
CompressFolder (DirectoryToCompress, true);
CleanupCompressStructure();
}
static void CreateCompressStructure()
{
Directory.CreateDirectory (DirectoryToCompress);
File.WriteAllText (Path.Combine (DirectoryToCompress, "MyFile.txt"), "C# is fun!");
var subfolder = Path.Combine (DirectoryToCompress, "Subfolder");
Directory.CreateDirectory (subfolder);
File.WriteAllText (Path.Combine (subfolder, "FileInSubfolder.txt"), ".NET rocks!");
}
static void CleanupCompressStructure()
{
var subfolder = Path.Combine (DirectoryToCompress, "Subfolder");
File.Delete(Path.Combine (subfolder, "FileInSubfolder.txt"));
Directory.Delete(subfolder);
File.Delete(Path.Combine (DirectoryToCompress, "MyFile.txt"));
Directory.Delete(DirectoryToCompress);
}
static uint CompressFolder (string folder, bool recursive)
{
string path = "Win32_Directory.Name='" + folder + "'";
using (ManagementObject dir = new ManagementObject (path))
using (ManagementBaseObject p = dir.GetMethodParameters ("CompressEx"))
{
p ["Recursive"] = recursive;
using (ManagementBaseObject result = dir.InvokeMethod ("CompressEx",
p, null))
return (uint)result.Properties ["ReturnValue"].Value;
}
}
static string DirectoryToCompress
{
get
{
return RuntimeInformation.IsOSPlatform (OSPlatform.Windows) ?
@"C:\Temp\CompressMe" : "/tmp/CompressMe"; // Requires a path outside LINQPad's current directory
}
}
Enumerate File Security
try
{
File.WriteAllText("sectest.txt", "File for testing security.");
FileSecurity fSecurity = new FileSecurity("sectest.txt", AccessControlSections.Owner |
AccessControlSections.Group |
AccessControlSections.Access);
Console.WriteLine (string.Join (Environment.NewLine,
fSecurity.GetAccessRules (true, true, typeof (NTAccount))
.Cast<AuthorizationRule>()
.Select (r => r.IdentityReference.Value)));
}
catch (PlatformNotSupportedException ex)
{
Console.WriteLine ($"Not supported: {ex.Message}");
}
finally
{
File.Delete ("sectest.txt");
}
Modify Access Control
void ShowSecurity (FileSecurity sec)
{
AuthorizationRuleCollection rules = sec.GetAccessRules (true, true,
typeof (NTAccount));
foreach (FileSystemAccessRule r in rules.Cast<FileSystemAccessRule>()
.OrderBy (rule => rule.IdentityReference.Value))
{
// e.g., MyDomain/Joe
Console.WriteLine ($" {r.IdentityReference.Value}");
// Allow or Deny: e.g., FullControl
Console.WriteLine ($" {r.FileSystemRights}: {r.AccessControlType}");
}
}
var file = "sectest.txt";
File.WriteAllText (file, "File security.");
var sid = new SecurityIdentifier (WellKnownSidType.BuiltinUsersSid, null);
string usersAccount = sid.Translate (typeof (NTAccount)).ToString();
Console.WriteLine ($"User: {usersAccount}");
FileSecurity sec = new FileSecurity (file,
AccessControlSections.Owner |
AccessControlSections.Group |
AccessControlSections.Access);
Console.WriteLine ("AFTER CREATE:");
ShowSecurity(sec);
sec.ModifyAccessRule (AccessControlModification.Add,
new FileSystemAccessRule (usersAccount, FileSystemRights.Write,
AccessControlType.Allow),
out bool modified);
Console.WriteLine ("AFTER MODIFY:");
ShowSecurity (sec);
File Info
void Main()
{
Directory.CreateDirectory (TempDirectory);
FileInfo fi = new FileInfo (Path.Combine(TempDirectory, "FileInfo.txt"));
Console.WriteLine (fi.Exists); // false
using (TextWriter w = fi.CreateText())
w.Write ("Some text");
Console.WriteLine (fi.Exists); // false (still)
fi.Refresh();
Console.WriteLine (fi.Exists); // true
Console.WriteLine (fi.Name); // FileInfo.txt
Console.WriteLine (fi.FullName); // c:\temp\FileInfo.txt (Windows)
// /tmp/FileInfo.txt (Unix)
Console.WriteLine (fi.DirectoryName); // c:\temp (Windows)
// /tmp (Unix)
Console.WriteLine (fi.Directory.Name); // temp
Console.WriteLine (fi.Extension); // .txt
Console.WriteLine (fi.Length); // 9
fi.Encrypt();
fi.Attributes ^= FileAttributes.Hidden; // (Toggle hidden flag)
fi.IsReadOnly = true;
Console.WriteLine (fi.Attributes); // ReadOnly,Archive,Hidden,Encrypted
Console.WriteLine (fi.CreationTime); // 3/09/2019 1:24:05 PM
fi.MoveTo (Path.Combine(TempDirectory, "FileInfoX.txt"));
DirectoryInfo di = fi.Directory;
Console.WriteLine (di.Name); // temp or tmp
Console.WriteLine (di.FullName); // c:\temp or /tmp
Console.WriteLine (di.Parent.FullName); // c:\
di.CreateSubdirectory ("SubFolder");
Cleanup();
}
static string TempDirectory
{
get
{
return RuntimeInformation.IsOSPlatform (OSPlatform.Windows) ?
@"C:\Temp" : "/tmp";
}
}
// Clean up
static void Cleanup()
{
var subfolder = Path.Combine(TempDirectory, "SubFolder");
if (Directory.Exists(subfolder)) Directory.Delete (subfolder);
var fi = new FileInfo (Path.Combine(TempDirectory, "FileInfo.txt"));
if (fi.Exists)
{
fi.Attributes &= ~FileAttributes.Hidden; // (Toggle hidden flag)
fi.IsReadOnly = false;
fi.Delete();
}
var fiX = new FileInfo (Path.Combine(TempDirectory, "FileInfoX.txt"));
if (fiX.Exists)
{
fiX.Attributes &= ~FileAttributes.Hidden; // (Toggle hidden flag)
fiX.IsReadOnly = false;
fiX.Delete();
}
}
Special Folders
foreach (var val in Enum.GetValues(typeof(Environment.SpecialFolder)).Cast<Environment.SpecialFolder>().Distinct().OrderBy(v =>v.ToString()))
{
$"{Environment.GetFolderPath (val)}".Dump(val.ToString());
}
Drive Information
DriveInfo c = new DriveInfo ("C"); // Query the C: drive.
long totalSize = c.TotalSize; // Size in bytes.
long freeBytes = c.TotalFreeSpace; // Ignores disk quotas.
long freeToMe = c.AvailableFreeSpace; // Takes quotas into account.
foreach (DriveInfo d in DriveInfo.GetDrives().OrderBy(d => d.Name)) // All defined drives.
{
Console.WriteLine (d.Name); // C:\
Console.WriteLine (d.DriveType); // Fixed
Console.WriteLine (d.RootDirectory); // C:\
if (d.IsReady) // If the drive is not ready, the following two
// properties will throw exceptions:
{
Console.WriteLine (d.VolumeLabel); // The Sea Drive
Console.WriteLine (d.DriveFormat); // NTFS
}
Console.WriteLine();
}
File Watcher
Watch (GetTestDirectory(), "*.txt", true);
void Watch (string path, string filter, bool includeSubDirs)
{
using (var watcher = new FileSystemWatcher (path, filter))
{
watcher.Created += FileCreatedChangedDeleted;
watcher.Changed += FileCreatedChangedDeleted;
watcher.Deleted += FileCreatedChangedDeleted;
watcher.Renamed += FileRenamed;
watcher.Error += FileError;
watcher.IncludeSubdirectories = includeSubDirs;
watcher.EnableRaisingEvents = true;
Console.WriteLine ("Listening for events - press <enter> to end");
Console.ReadLine();
}
// Disposing the FileSystemWatcher stops further events from firing.
}
void FileCreatedChangedDeleted (object o, FileSystemEventArgs e)
=> Console.WriteLine ("File {0} has been {1}", e.FullPath, e.ChangeType);
void FileRenamed (object o, RenamedEventArgs e)
=> Console.WriteLine ("Renamed: {0}->{1}", e.OldFullPath, e.FullPath);
void FileError (object o, ErrorEventArgs e)
=> Console.WriteLine ("Error: " + e.GetException().Message);
string GetTestDirectory() =>
RuntimeInformation.IsOSPlatform (OSPlatform.Windows)
? @"C:\Temp"
: "/tmp";
Get Volume Information
class SupportsCompressionEncryption
{
const int SupportsCompression = 0x10;
const int SupportsEncryption = 0x20000;
[DllImport ("Kernel32.dll", SetLastError = true)]
extern static bool GetVolumeInformation (string vol, StringBuilder name,
int nameSize, out uint serialNum, out uint maxNameLen, out uint flags,
StringBuilder fileSysName, int fileSysNameSize);
static void Main()
{
string volume = @"C:\";
uint serialNum, maxNameLen, flags;
bool ok = GetVolumeInformation (volume, null, 0, out serialNum,
out maxNameLen, out flags, null, 0);
if (!ok)
throw new Win32Exception();
bool canCompress = (flags & SupportsCompression) != 0;
bool canEncrypt = (flags & SupportsEncryption) != 0;
Console.WriteLine ($"Volume {volume} supports compression: {canCompress}; encryption: {canEncrypt}");
}
}
Memory Mapped
File.WriteAllBytes ("long.bin", new byte [1000000]);
using MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile ("long.bin");
using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();
accessor.Write (500000, (byte)77);
Console.WriteLine (accessor.ReadByte (500000)); // 77
File.WriteAllBytes ("short.bin", new byte [1]);
using MemoryMappedFile mmfSh = MemoryMappedFile.CreateFromFile
("short.bin", FileMode.Create, null, 1000);
using MemoryMappedViewAccessor accessorSh = mmfSh.CreateViewAccessor();
accessorSh.Write (500, (byte)42);
Console.WriteLine (accessorSh.ReadByte (500));
Memory Mapped Named Reader
// This can run in a separate executable:
using MemoryMappedFile mmFile = MemoryMappedFile.OpenExisting ("Demo");
using MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor();
Console.WriteLine (accessor.ReadInt32 (0)); // 12345
Memory Mapped Named Writer
using MemoryMappedFile mmFile = MemoryMappedFile.CreateNew ("Demo", 500);
using MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor();
accessor.Write (0, 12345);
// In LINQPad, manaully stop the query to exit
Console.ReadLine(); // Keep shared memory alive until user hits Enter.
Memory Mapped Reader
void Main()
{
// This can run in a separate executable:
var file = Path.Combine (TempDirectory, "interprocess.bin");
using FileStream fs = new FileStream (file, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
using MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile (fs, null, fs.Length, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true);
using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();
Console.WriteLine (accessor.ReadInt32 (0)); // 12345
}
static string TempDirectory
{
get => RuntimeInformation.IsOSPlatform (OSPlatform.Windows) ?
@"C:\Temp" : "/tmp";
}
Memory Mapped Writer
void Main()
{
var file = Path.Combine(TempDirectory, "interprocess.bin");
File.WriteAllBytes (file, new byte [100]);
using FileStream fs = new FileStream (file, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
using MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile (fs, null, fs.Length, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true);
using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();
accessor.Write (0, 12345);
// In LINQPad, manaully stop the query to exit
Console.ReadLine(); // Keep shared memory alive until user hits Enter.
File.Delete(file);
}
static string TempDirectory
{
get => RuntimeInformation.IsOSPlatform (OSPlatform.Windows) ?
@"C:\Temp" : "/tmp";
}
Memory Mapped Server
using (MemoryMappedFile mmFile = MemoryMappedFile.CreateNew ("Demo", 500))
using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor())
{
accessor.Write (0, 12345);
// In LINQPad, manaully stop the query to exit
Console.ReadLine(); // Keep shared memory alive until user hits Enter.
}
Memory Mapped Unsafe
void Main()
{
File.WriteAllBytes ("unsafe.bin", new byte [100]);
var data = new Data { X = 123, Y = 456 };
using MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile ("unsafe.bin");
using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();
accessor.Write (0, ref data);
accessor.Read (0, out data);
Console.WriteLine (data.X + " " + data.Y); // 123 456
unsafe
{
byte* pointer = null;
try
{
accessor.SafeMemoryMappedViewHandle.AcquirePointer (ref pointer);
int* intPointer = (int*)pointer;
Console.WriteLine (*intPointer); // 123
}
finally
{
if (pointer != null)
accessor.SafeMemoryMappedViewHandle.ReleasePointer();
}
}
}
struct Data { public int X, Y; }