// dnlib: See LICENSE.txt for more info
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Threading;
using Microsoft.Win32.SafeHandles;
namespace dnlib.IO {
///
/// Creates s that read memory mapped data
///
sealed unsafe class MemoryMappedDataReaderFactory : DataReaderFactory {
///
/// The filename or null if the data is not from a file
///
public override string Filename => filename;
///
/// Gets the total length of the data
///
public override uint Length => length;
///
/// Raised when all cached s created by this instance must be recreated
///
public override event EventHandler DataReaderInvalidated;
DataStream stream;
uint length;
string filename;
GCHandle gcHandle;
byte[] dataAry;
IntPtr data;
OSType osType;
long origDataLength;
MemoryMappedDataReaderFactory(string filename) {
osType = OSType.Unknown;
this.filename = filename;
}
~MemoryMappedDataReaderFactory() {
Dispose(false);
}
///
/// Creates a data reader
///
/// Offset of data
/// Length of data
///
public override DataReader CreateReader(uint offset, uint length) => CreateReader(stream, offset, length);
///
/// Cleans up and frees all allocated memory
///
public override void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
internal void SetLength(uint length) => this.length = length;
enum OSType : byte {
Unknown,
Windows,
Unix,
}
[Serializable]
sealed class MemoryMappedIONotSupportedException : IOException {
public MemoryMappedIONotSupportedException(string s) : base(s) { }
public MemoryMappedIONotSupportedException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}
static class Windows {
const uint GENERIC_READ = 0x80000000;
const uint FILE_SHARE_READ = 0x00000001;
const uint OPEN_EXISTING = 3;
const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
const uint PAGE_READONLY = 0x02;
const uint SEC_IMAGE = 0x1000000;
const uint SECTION_MAP_READ = 0x0004;
const uint FILE_MAP_READ = SECTION_MAP_READ;
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)]
static extern SafeFileHandle CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile);
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
static extern SafeFileHandle CreateFileMapping(SafeFileHandle hFile, IntPtr lpAttributes, uint flProtect, uint dwMaximumSizeHigh, uint dwMaximumSizeLow, string lpName);
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
static extern IntPtr MapViewOfFile(SafeFileHandle hFileMappingObject, uint dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffsetLow, UIntPtr dwNumberOfBytesToMap);
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool UnmapViewOfFile(IntPtr lpBaseAddress);
[DllImport("kernel32", CharSet = CharSet.Auto, SetLastError = true)]
static extern uint GetFileSize(SafeFileHandle hFile, out uint lpFileSizeHigh);
const uint INVALID_FILE_SIZE = 0xFFFFFFFF;
const int NO_ERROR = 0;
public static void Mmap(MemoryMappedDataReaderFactory creator, bool mapAsImage) {
using (var fileHandle = CreateFile(creator.filename, GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero)) {
if (fileHandle.IsInvalid)
throw new IOException($"Could not open file {creator.filename} for reading. Error: {Marshal.GetLastWin32Error():X8}");
uint sizeLo = GetFileSize(fileHandle, out uint sizeHi);
int hr;
if (sizeLo == INVALID_FILE_SIZE && (hr = Marshal.GetLastWin32Error()) != NO_ERROR)
throw new IOException($"Could not get file size. File: {creator.filename}, error: {hr:X8}");
var fileSize = ((long)sizeHi << 32) | sizeLo;
using (var fileMapping = CreateFileMapping(fileHandle, IntPtr.Zero, PAGE_READONLY | (mapAsImage ? SEC_IMAGE : 0), 0, 0, null)) {
if (fileMapping.IsInvalid)
throw new MemoryMappedIONotSupportedException($"Could not create a file mapping object. File: {creator.filename}, error: {Marshal.GetLastWin32Error():X8}");
creator.data = MapViewOfFile(fileMapping, FILE_MAP_READ, 0, 0, UIntPtr.Zero);
if (creator.data == IntPtr.Zero)
throw new MemoryMappedIONotSupportedException($"Could not map file {creator.filename}. Error: {Marshal.GetLastWin32Error():X8}");
creator.length = (uint)fileSize;
creator.osType = OSType.Windows;
creator.stream = DataStreamFactory.Create((byte*)creator.data);
}
}
}
public static void Dispose(IntPtr addr) {
if (addr != IntPtr.Zero)
UnmapViewOfFile(addr);
}
}
static class Unix {
// Can't use SafeFileHandle. Seems like a bug in mono. You'll get
// "_wapi_handle_unref_full: Attempting to unref unused handle 0xYYY" when Dispose() is called.
[DllImport("libc")]
static extern int open(string pathname, int flags);
const int O_RDONLY = 0;
[DllImport("libc")]
static extern int close(int fd);
[DllImport("libc", EntryPoint = "lseek", SetLastError = true)]
static extern int lseek32(int fd, int offset, int whence);
[DllImport("libc", EntryPoint = "lseek", SetLastError = true)]
static extern long lseek64(int fd, long offset, int whence);
const int SEEK_END = 2;
[DllImport("libc", EntryPoint = "mmap", SetLastError = true)]
static extern IntPtr mmap32(IntPtr addr, IntPtr length, int prot, int flags, int fd, int offset);
[DllImport("libc", EntryPoint = "mmap", SetLastError = true)]
static extern IntPtr mmap64(IntPtr addr, IntPtr length, int prot, int flags, int fd, long offset);
const int PROT_READ = 1;
const int MAP_PRIVATE = 0x02;
[DllImport("libc")]
static extern int munmap(IntPtr addr, IntPtr length);
public static void Mmap(MemoryMappedDataReaderFactory creator, bool mapAsImage) {
int fd = open(creator.filename, O_RDONLY);
try {
if (fd < 0)
throw new IOException($"Could not open file {creator.filename} for reading. Error: {fd}");
long size;
IntPtr data;
if (IntPtr.Size == 4) {
size = lseek32(fd, 0, SEEK_END);
if (size == -1)
throw new MemoryMappedIONotSupportedException($"Could not get length of {creator.filename} (lseek failed): {Marshal.GetLastWin32Error()}");
data = mmap32(IntPtr.Zero, (IntPtr)size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == new IntPtr(-1) || data == IntPtr.Zero)
throw new MemoryMappedIONotSupportedException($"Could not map file {creator.filename}. Error: {Marshal.GetLastWin32Error()}");
}
else {
size = lseek64(fd, 0, SEEK_END);
if (size == -1)
throw new MemoryMappedIONotSupportedException($"Could not get length of {creator.filename} (lseek failed): {Marshal.GetLastWin32Error()}");
data = mmap64(IntPtr.Zero, (IntPtr)size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == new IntPtr(-1) || data == IntPtr.Zero)
throw new MemoryMappedIONotSupportedException($"Could not map file {creator.filename}. Error: {Marshal.GetLastWin32Error()}");
}
creator.data = data;
creator.length = (uint)size;
creator.origDataLength = size;
creator.osType = OSType.Unix;
creator.stream = DataStreamFactory.Create((byte*)creator.data);
}
finally {
if (fd >= 0)
close(fd);
}
}
public static void Dispose(IntPtr addr, long size) {
if (addr != IntPtr.Zero)
munmap(addr, new IntPtr(size));
}
}
static volatile bool canTryWindows = true;
static volatile bool canTryUnix = true;
internal static MemoryMappedDataReaderFactory CreateWindows(string filename, bool mapAsImage) {
if (!canTryWindows)
return null;
var creator = new MemoryMappedDataReaderFactory(GetFullPath(filename));
try {
Windows.Mmap(creator, mapAsImage);
return creator;
}
catch (EntryPointNotFoundException) {
}
catch (DllNotFoundException) {
}
canTryWindows = false;
return null;
}
internal static MemoryMappedDataReaderFactory CreateUnix(string filename, bool mapAsImage) {
if (!canTryUnix)
return null;
var creator = new MemoryMappedDataReaderFactory(GetFullPath(filename));
try {
Unix.Mmap(creator, mapAsImage);
if (mapAsImage) { // Only check this if we know that mmap() works, i.e., if above call succeeds
creator.Dispose();
throw new ArgumentException("mapAsImage == true is not supported on this OS");
}
return creator;
}
catch (MemoryMappedIONotSupportedException ex) {
Debug.WriteLine($"mmap'd IO didn't work: {ex.Message}");
}
catch (EntryPointNotFoundException) {
}
catch (DllNotFoundException) {
}
canTryUnix = false;
return null;
}
static string GetFullPath(string filename) {
try {
return Path.GetFullPath(filename);
}
catch {
return filename;
}
}
void Dispose(bool disposing) {
FreeMemoryMappedIoData();
if (disposing) {
length = 0;
stream = EmptyDataStream.Instance;
data = IntPtr.Zero;
filename = null;
}
}
///
/// true if memory mapped I/O is enabled
///
internal bool IsMemoryMappedIO => dataAry is null;
///
/// Call this to disable memory mapped I/O. This must only be called if no other code is
/// trying to access the memory since that could lead to an exception.
///
internal void UnsafeDisableMemoryMappedIO() {
if (dataAry is not null)
return;
var newAry = new byte[length];
Marshal.Copy(data, newAry, 0, newAry.Length);
FreeMemoryMappedIoData();
length = (uint)newAry.Length;
dataAry = newAry;
gcHandle = GCHandle.Alloc(dataAry, GCHandleType.Pinned);
data = gcHandle.AddrOfPinnedObject();
stream = DataStreamFactory.Create((byte*)data);
DataReaderInvalidated?.Invoke(this, EventArgs.Empty);
}
void FreeMemoryMappedIoData() {
if (dataAry is null) {
var origData = Interlocked.Exchange(ref data, IntPtr.Zero);
if (origData != IntPtr.Zero) {
length = 0;
switch (osType) {
case OSType.Windows:
Windows.Dispose(origData);
break;
case OSType.Unix:
Unix.Dispose(origData, origDataLength);
break;
default:
throw new InvalidOperationException("Shouldn't be here");
}
}
}
if (gcHandle.IsAllocated) {
try {
gcHandle.Free();
}
catch (InvalidOperationException) {
}
}
dataAry = null;
}
}
}