using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using Avalonia.Platform; using Avalonia.Platform.Interop; using Avalonia.Utilities; namespace Avalonia.Shared.PlatformSupport { static class StandardRuntimePlatformServices { public static void Register(Assembly assembly = null) { var standardPlatform = new StandardRuntimePlatform(); AssetLoader.RegisterResUriParsers(); AvaloniaLocator.CurrentMutable .Bind().ToConstant(standardPlatform) .Bind().ToConstant(new AssetLoader(assembly)) .Bind().ToConstant( #if __IOS__ new IOSLoader() #else RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (IDynamicLibraryLoader)new Win32Loader() : new UnixLoader() #endif ); } } internal partial class StandardRuntimePlatform : IRuntimePlatform { public IDisposable StartSystemTimer(TimeSpan interval, Action tick) { return new Timer(_ => tick(), null, interval, interval); } public IUnmanagedBlob AllocBlob(int size) => new UnmanagedBlob(this, size); class UnmanagedBlob : IUnmanagedBlob { private readonly StandardRuntimePlatform _plat; private IntPtr _address; private readonly object _lock = new object(); #if DEBUG private static readonly List Backtraces = new List(); private static Thread GCThread; private readonly string _backtrace; private static readonly object _btlock = new object(); class GCThreadDetector { ~GCThreadDetector() { GCThread = Thread.CurrentThread; } } [MethodImpl(MethodImplOptions.NoInlining)] static void Spawn() => new GCThreadDetector(); static UnmanagedBlob() { Spawn(); GC.WaitForPendingFinalizers(); } #endif public UnmanagedBlob(StandardRuntimePlatform plat, int size) { if (size <= 0) throw new ArgumentException("Positive number required", nameof(size)); _plat = plat; _address = plat.Alloc(size); GC.AddMemoryPressure(size); Size = size; #if DEBUG _backtrace = Environment.StackTrace; lock (_btlock) Backtraces.Add(_backtrace); #endif } void DoDispose() { lock (_lock) { if (!IsDisposed) { #if DEBUG lock (_btlock) Backtraces.Remove(_backtrace); #endif _plat?.Free(_address, Size); GC.RemoveMemoryPressure(Size); IsDisposed = true; _address = IntPtr.Zero; Size = 0; } } } public void Dispose() { #if DEBUG if (Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId) { lock (_lock) { if (!IsDisposed) { Console.Error.WriteLine("Native blob disposal from finalizer thread\nBacktrace: " + Environment.StackTrace + "\n\nBlob created by " + _backtrace); } } } #endif DoDispose(); GC.SuppressFinalize(this); } ~UnmanagedBlob() { #if DEBUG Console.Error.WriteLine("Undisposed native blob created by " + _backtrace); #endif DoDispose(); } public IntPtr Address => IsDisposed ? throw new ObjectDisposedException("UnmanagedBlob") : _address; public int Size { get; private set; } public bool IsDisposed { get; private set; } } #if NET461 || NETCOREAPP2_0 [DllImport("libc", SetLastError = true)] private static extern IntPtr mmap(IntPtr addr, IntPtr length, int prot, int flags, int fd, IntPtr offset); [DllImport("libc", SetLastError = true)] private static extern int munmap(IntPtr addr, IntPtr length); [DllImport("libc", SetLastError = true)] private static extern long sysconf(int name); private bool? _useMmap; private bool UseMmap => _useMmap ?? ((_useMmap = GetRuntimeInfo().OperatingSystem == OperatingSystemType.Linux)).Value; IntPtr Alloc(int size) { if (UseMmap) { var rv = mmap(IntPtr.Zero, new IntPtr(size), 3, 0x22, -1, IntPtr.Zero); if (rv.ToInt64() == -1 || (ulong) rv.ToInt64() == 0xffffffff) { var errno = Marshal.GetLastWin32Error(); throw new Exception("Unable to allocate memory: " + errno); } return rv; } else return Marshal.AllocHGlobal(size); } void Free(IntPtr ptr, int len) { if (UseMmap) { if (munmap(ptr, new IntPtr(len)) == -1) { var errno = Marshal.GetLastWin32Error(); throw new Exception("Unable to free memory: " + errno); } } else Marshal.FreeHGlobal(ptr); } #else IntPtr Alloc(int size) => Marshal.AllocHGlobal(size); void Free(IntPtr ptr, int len) => Marshal.FreeHGlobal(ptr); #endif } internal class IOSLoader : IDynamicLibraryLoader { IntPtr IDynamicLibraryLoader.LoadLibrary(string dll) { throw new PlatformNotSupportedException(); } IntPtr IDynamicLibraryLoader.GetProcAddress(IntPtr dll, string proc, bool optional) { throw new PlatformNotSupportedException(); } } public class AssetLoader : IAssetLoader { private const string AvaloniaResourceName = "!AvaloniaResources"; private static readonly Dictionary AssemblyNameCache = new Dictionary(); private AssemblyDescriptor _defaultResmAssembly; /// /// Initializes a new instance of the class. /// /// /// The default assembly from which to load resm: assets for which no assembly is specified. /// public AssetLoader(Assembly assembly = null) { if (assembly == null) assembly = Assembly.GetEntryAssembly(); if (assembly != null) _defaultResmAssembly = new AssemblyDescriptor(assembly); } /// /// Sets the default assembly from which to load assets for which no assembly is specified. /// /// The default assembly. public void SetDefaultAssembly(Assembly assembly) { _defaultResmAssembly = new AssemblyDescriptor(assembly); } /// /// Checks if an asset with the specified URI exists. /// /// The URI. /// /// A base URI to use if is relative. /// /// True if the asset could be found; otherwise false. public bool Exists(Uri uri, Uri baseUri = null) { return GetAsset(uri, baseUri) != null; } /// /// Opens the asset with the requested URI. /// /// The URI. /// /// A base URI to use if is relative. /// /// A stream containing the asset contents. /// /// The asset could not be found. /// public Stream Open(Uri uri, Uri baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1; /// /// Opens the asset with the requested URI and returns the asset stream and the /// assembly containing the asset. /// /// The URI. /// /// A base URI to use if is relative. /// /// /// The stream containing the resource contents together with the assembly. /// /// /// The asset could not be found. /// public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri baseUri = null) { var asset = GetAsset(uri, baseUri); if (asset == null) { throw new FileNotFoundException($"The resource {uri} could not be found."); } return (asset.GetStream(), asset.Assembly); } public Assembly GetAssembly(Uri uri, Uri baseUri) { if (!uri.IsAbsoluteUri && baseUri != null) uri = new Uri(baseUri, uri); return GetAssembly(uri).Assembly; } /// /// Gets all assets of a folder and subfolders that match specified uri. /// /// The URI. /// Base URI that is used if is relative. /// All matching assets as a tuple of the absolute path to the asset and the assembly containing the asset public IEnumerable GetAssets(Uri uri, Uri baseUri) { if (uri.IsAbsoluteUri && uri.Scheme == "resm") { var assembly = GetAssembly(uri); return assembly?.Resources.Where(x => x.Key.Contains(uri.AbsolutePath)) .Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? Enumerable.Empty(); } uri = EnsureAbsolute(uri, baseUri); if (uri.Scheme == "avares") { var (asm, path) = GetResAsmAndPath(uri); if (asm == null) { throw new ArgumentException( "No default assembly, entry assembly or explicit assembly specified; " + "don't know where to look up for the resource, try specifying assembly explicitly."); } if (asm?.AvaloniaResources == null) return Enumerable.Empty(); path = path.TrimEnd('/') + '/'; return asm.AvaloniaResources.Where(r => r.Key.StartsWith(path)) .Select(x => new Uri($"avares://{asm.Name}{x.Key}")); } return Enumerable.Empty(); } private Uri EnsureAbsolute(Uri uri, Uri baseUri) { if (uri.IsAbsoluteUri) return uri; if(baseUri == null) throw new ArgumentException($"Relative uri {uri} without base url"); if (!baseUri.IsAbsoluteUri) throw new ArgumentException($"Base uri {baseUri} is relative"); if (baseUri.Scheme == "resm") throw new ArgumentException( $"Relative uris for 'resm' scheme aren't supported; {baseUri} uses resm"); return new Uri(baseUri, uri); } private IAssetDescriptor GetAsset(Uri uri, Uri baseUri) { if (uri.IsAbsoluteUri && uri.Scheme == "resm") { var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly; if (asm == null) { throw new ArgumentException( "No default assembly, entry assembly or explicit assembly specified; " + "don't know where to look up for the resource, try specifying assembly explicitly."); } IAssetDescriptor rv; var resourceKey = uri.AbsolutePath; asm.Resources.TryGetValue(resourceKey, out rv); return rv; } uri = EnsureAbsolute(uri, baseUri); if (uri.Scheme == "avares") { var (asm, path) = GetResAsmAndPath(uri); if (asm.AvaloniaResources == null) return null; asm.AvaloniaResources.TryGetValue(path, out var desc); return desc; } throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri)); } private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) { var asm = GetAssembly(uri.Authority); return (asm, uri.AbsolutePath); } private AssemblyDescriptor GetAssembly(Uri uri) { if (uri != null) { if (!uri.IsAbsoluteUri) return null; if (uri.Scheme == "avares") return GetResAsmAndPath(uri).asm; if (uri.Scheme == "resm") { var qs = ParseQueryString(uri); string assemblyName; if (qs.TryGetValue("assembly", out assemblyName)) { return GetAssembly(assemblyName); } } } return null; } private AssemblyDescriptor GetAssembly(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); AssemblyDescriptor rv; if (!AssemblyNameCache.TryGetValue(name, out rv)) { var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); var match = loadedAssemblies.FirstOrDefault(a => a.GetName().Name == name); if (match != null) { AssemblyNameCache[name] = rv = new AssemblyDescriptor(match); } else { // iOS does not support loading assemblies dynamically! // #if __IOS__ throw new InvalidOperationException( $"Assembly {name} needs to be referenced and explicitly loaded before loading resources"); #else name = Uri.UnescapeDataString(name); AssemblyNameCache[name] = rv = new AssemblyDescriptor(Assembly.Load(name)); #endif } } return rv; } private Dictionary ParseQueryString(Uri uri) { return uri.Query.TrimStart('?') .Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries) .Select(p => p.Split('=')) .ToDictionary(p => p[0], p => p[1]); } private interface IAssetDescriptor { Stream GetStream(); Assembly Assembly { get; } } private class AssemblyResourceDescriptor : IAssetDescriptor { private readonly Assembly _asm; private readonly string _name; public AssemblyResourceDescriptor(Assembly asm, string name) { _asm = asm; _name = name; } public Stream GetStream() { return _asm.GetManifestResourceStream(_name); } public Assembly Assembly => _asm; } private class AvaloniaResourceDescriptor : IAssetDescriptor { private readonly int _offset; private readonly int _length; public Assembly Assembly { get; } public AvaloniaResourceDescriptor(Assembly asm, int offset, int length) { _offset = offset; _length = length; Assembly = asm; } public Stream GetStream() { return new SlicedStream(Assembly.GetManifestResourceStream(AvaloniaResourceName), _offset, _length); } } class SlicedStream : Stream { private readonly Stream _baseStream; private readonly int _from; public SlicedStream(Stream baseStream, int from, int length) { Length = length; _baseStream = baseStream; _from = from; _baseStream.Position = from; } public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) { return _baseStream.Read(buffer, offset, (int)Math.Min(count, Length - Position)); } public override long Seek(long offset, SeekOrigin origin) { if (origin == SeekOrigin.Begin) Position = offset; if (origin == SeekOrigin.End) Position = _from + Length + offset; if (origin == SeekOrigin.Current) Position = Position + offset; return Position; } public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); public override bool CanRead => true; public override bool CanSeek => _baseStream.CanRead; public override bool CanWrite => false; public override long Length { get; } public override long Position { get => _baseStream.Position - _from; set => _baseStream.Position = value + _from; } protected override void Dispose(bool disposing) { if (disposing) _baseStream.Dispose(); } public override void Close() => _baseStream.Close(); } private class AssemblyDescriptor { public AssemblyDescriptor(Assembly assembly) { Assembly = assembly; if (assembly != null) { Resources = assembly.GetManifestResourceNames() .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n)); Name = assembly.GetName().Name; using (var resources = assembly.GetManifestResourceStream(AvaloniaResourceName)) { if (resources != null) { Resources.Remove(AvaloniaResourceName); var indexLength = new BinaryReader(resources).ReadInt32(); var index = AvaloniaResourcesIndexReaderWriter.Read(new SlicedStream(resources, 4, indexLength)); var baseOffset = indexLength + 4; AvaloniaResources = index.ToDictionary(r => "/" + r.Path.TrimStart('/'), r => (IAssetDescriptor) new AvaloniaResourceDescriptor(assembly, baseOffset + r.Offset, r.Size)); } } } } public Assembly Assembly { get; } public Dictionary Resources { get; } public Dictionary AvaloniaResources { get; } public string Name { get; } } public static void RegisterResUriParsers() { if (!UriParser.IsKnownScheme("avares")) UriParser.Register(new GenericUriParser( GenericUriParserOptions.GenericAuthority | GenericUriParserOptions.NoUserInfo | GenericUriParserOptions.NoPort | GenericUriParserOptions.NoQuery | GenericUriParserOptions.NoFragment), "avares", -1); } } }