sharplib/res/Resource.cs
Marc Hernandez 3f850cc9b0 Implement Xml Serialization Framework with Type Handling and Metadata Caching
- Introduced XmlSer class for XML serialization and deserialization.
- Added TypeMetaCache for caching type metadata and reflection information.
- Implemented various ITypeHandler implementations for handling different types (Primitive, Proxy, ISerializable, Collection, Object).
- Enhanced type resolution with TypeResolver to manage type lookups and conversions.
- Established a configuration class (XmlCfg) to manage serialization settings.
- Integrated support for handling graphs and references in serialized objects.
- Added extensive member processing and filtering based on attributes.
- Ensured compatibility with immutable collections and various data structures.
- Implemented detailed error handling and logging for serialization processes.
2025-05-28 10:46:00 -07:00

435 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.CodeAnalysis; // Note: This was in the original, but seems unused. Keep for now.
using System.Linq;
#nullable enable
namespace res;
// A delegate representing a function that can load a resource of type T.
public delegate T Load<out T>( string filename );
/// <summary>
/// Abstract base class for a resource reference.
/// Provides a common way to refer to resources and includes debugging/tracking info.
/// </summary>
[DebuggerDisplay( "Path = {Filename}" )]
public abstract class Ref
{
public static bool VerboseLogging { get; set; } = false;
public string Filename { get; }
public string Reason { get; }
public string DbgName { get; }
public string DbgPath { get; }
public int DbgLine { get; }
protected Ref(
string filename,
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 )
{
Filename = filename;
Reason = reason;
DbgName = dbgName;
DbgPath = dbgPath;
DbgLine = dbgLine;
if( VerboseLogging )
log.info( $"Ref Created: {GetType().Name} {Filename}" );
}
/// <summary>
/// Looks up and loads the resource.
/// </summary>
/// <returns>The loaded resource object.</returns>
public abstract object Lookup(
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 );
/// <summary>
/// Called when the resource might have changed (optional, base implementation does nothing).
/// </summary>
public virtual void OnChange() { }
/// <summary>
/// Internal method to trigger the initial load (used by deferred loading, if implemented).
/// </summary>
internal virtual void InternalLoad() { }
}
/// <summary>
/// A typed reference to a resource of type T.
/// Handles lazy loading and access to the resource.
/// </summary>
[Serializable]
[DebuggerDisplay( "Path = {Filename} / Res = {m_res}" )]
public class Ref<T> : Ref where T : class, new()
{
[NonSerialized]
private T? m_res;
/// <summary>
/// Gets the resource, loading it if necessary.
/// </summary>
public T Res => m_res ?? Lookup();
/// <summary>
/// Gets the resource, loading it if necessary.
/// </summary>
//[Deprecated("Use Res property instead.")]
public T res => m_res ?? Lookup();
public Ref(
string filename = "",
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 )
: base(
!string.IsNullOrWhiteSpace( filename ) ? filename : $"{{{dbgName}_{Path.GetFileNameWithoutExtension( dbgPath )}}}",
reason, dbgName, dbgPath, dbgLine )
{
if( VerboseLogging )
log.info( $"Ref<T> Created: {GetType().Name}<{typeof( T ).Name}> {Filename}" );
m_res = default;
}
/// <summary>
/// Looks up and loads the resource.
/// </summary>
public override T Lookup(
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 )
{
// If already loaded, return it.
if( m_res != null )
return m_res;
// Load using the Mgr.
m_res = Mgr.Load<T>( Filename, $"Ref lookup (orig: {Reason}) bcs {reason}", dbgName, dbgPath, dbgLine );
if( VerboseLogging )
log.info( $"Ref.Lookup: {GetType().Name}<{typeof( T ).Name}> {Filename}" );
return m_res;
}
/*
/// <summary>
/// Overrides the base Lookup to provide a typed result.
/// </summary>
public override object Lookup(
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0)
{
return Lookup(reason, dbgName, dbgPath, dbgLine);
}
*/
/// <summary>
/// Internal load implementation.
/// </summary>
internal override void InternalLoad()
{
if( m_res == null )
{
m_res = Mgr.Load<T>( Filename, Reason, DbgName, DbgPath, DbgLine );
if( VerboseLogging )
log.info( $"Ref.InternalLoad: {GetType().Name}<{typeof( T ).Name}> {Filename}" );
}
}
/// <summary>
/// Creates a new resource asset and saves it, returning a Ref to it.
/// Handles existing files by renaming them.
/// </summary>
public static Ref<T> CreateAsset(
T value, string path,
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 )
{
if( File.Exists( path ) )
{
log.warn( $"Asset exists: {path}. Renaming before creating." );
var newPath = $"{path}_{DateTime.Now:yyyyMMdd_HHmmss}";
File.Move( path, newPath );
log.warn( $"Renamed existing asset to: {newPath}" );
}
// Here you would typically save 'value' to 'path' using a registered saver or specific logic.
// Since saving isn't defined, we'll assume it happens externally or add it here if needed.
// For now, we'll just log and create the Ref.
log.info( $"Creating asset Ref for {typeof( T ).Name} at {path}." );
// We need a way to save 'value' to 'path' before creating the Ref.
// This part requires a 'Save' function, which isn't in the original.
// Let's assume you'll add saving logic here.
// Mgr.Save(value, path); // Example: Needs implementation
var immMeta = (value as imm.Obj)?.Meta;
var createReason = $"CreateAsset: {value.GetType().Name} - {immMeta?.Reason ?? "N/A"}";
var newRef = new Ref<T>( path, $"{createReason} bcs {reason}", dbgName, dbgPath, dbgLine );
// We should make the newRef hold the 'value' immediately,
// or ensure loading it back gives the same 'value'.
// Setting m_res directly might be an option, or caching it.
newRef.m_res = value;
Mgr.CacheResource( path, value, $"CreateAsset {typeof( T ).Name}" );
return newRef;
}
}
/// <summary>
/// Holds a weak reference to a cached resource along with metadata.
/// </summary>
internal record ResourceHolder<T>( WeakReference<T> WeakRef, string Name, DateTime Captured ) where T : class;
/// <summary>
/// Manages resource loading, caching, and loader registration.
/// This static class replaces the original `Mgr` instance.
/// </summary>
public static class Mgr
{
// Internal holder for type-specific loaders.
private abstract class LoadHolder { public abstract object Load( string filename ); }
private class LoadHolder<T> : LoadHolder
{
private readonly Load<T> _fnLoad;
public LoadHolder( Load<T> fnLoad ) { _fnLoad = fnLoad; }
public override object Load( string filename ) => _fnLoad( filename )!;
}
// Cache for resource holders (contains WeakReferences).
private static class ResCache<T> where T : class, new()
{
public static T s_default = new();
public static ImmutableDictionary<string, ResourceHolder<T>> s_cache = ImmutableDictionary<string, ResourceHolder<T>>.Empty;
}
private static ImmutableDictionary<Type, LoadHolder> s_loaders = ImmutableDictionary<Type, LoadHolder>.Empty;
// Using ConcurrentDictionary for per-file locking objects.
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, object> s_loadingLocks = new();
/// <summary>
/// Registers a specific loading function for a type T.
/// </summary>
public static void Register<T>( Load<T> loader ) where T : class, new()
{
var type = typeof( T );
if( s_loaders.ContainsKey( type ) )
{
log.warn( $"Loader for type {type.Name} is already registered. Overwriting." );
}
ImmutableInterlocked.Update( ref s_loaders, d => d.SetItem( type, new LoadHolder<T>( loader ) ) );
log.info( $"Registered loader for {type.Name}" );
}
/// <summary>
/// Registers loaders for all subtypes of T found in loaded assemblies,
/// assuming they follow the 'res_load' static generic method pattern.
/// </summary>
public static void RegisterSub<T>() => RegisterSub( typeof( T ) );
/// <summary>
/// Registers loaders for all subtypes of a base type.
/// </summary>
public static void RegisterSub( Type baseType )
{
log.info( $"Scanning for subtypes of {baseType.Name} to register loaders..." );
MethodInfo? genericLoadMethod = baseType.GetMethods( BindingFlags.Public | BindingFlags.Static )
.FirstOrDefault( mi => mi.Name == "res_load" && mi.IsGenericMethodDefinition );
if( genericLoadMethod == null )
{
log.error( $"Could not find 'public static T res_load<T>(string filename)' method on {baseType.Name} or its bases." );
return;
}
foreach( var ass in AppDomain.CurrentDomain.GetAssemblies() )
{
try
{
foreach( var t in ass.GetTypes() )
{
if( t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom( t ) && t != baseType )
{
try
{
log.debug( $"Found subtype {t.Name}, creating loader." );
var concreteLoadMethod = genericLoadMethod.MakeGenericMethod( t );
var loadDelegateType = typeof( Load<> ).MakeGenericType( t );
var loaderDelegate = Delegate.CreateDelegate( loadDelegateType, concreteLoadMethod );
var loadHolderType = typeof( LoadHolder<> ).MakeGenericType( t );
var loadHolder = Activator.CreateInstance( loadHolderType, loaderDelegate ) as LoadHolder;
if( loadHolder != null )
{
if( !ImmutableInterlocked.TryAdd( ref s_loaders, t, loadHolder ) )
{
log.debug( $"Loader for {t.Name} already exists, skipping." );
}
else
{
log.info( $"Registered loader for subtype {t.Name}." );
}
}
}
catch( Exception ex )
{
log.error( $"Failed to create loader for subtype {t.Name}: {ex.Message}" );
}
}
}
}
catch( ReflectionTypeLoadException rtle )
{
log.warn( $"Could not fully scan assembly {ass.FullName}: {rtle.Message}" );
}
catch( Exception ex )
{
log.warn( $"Error scanning assembly {ass.FullName}: {ex.Message}" );
}
}
}
/// <summary>
/// Creates a Ref<T> for a given filename.
/// </summary>
public static Ref<T> Lookup<T>(
string filename,
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 ) where T : class, new()
{
return new Ref<T>( filename, reason, dbgName, dbgPath, dbgLine );
}
/// <summary>
/// Loads a resource, handling caching and thread-safe loading.
/// </summary>
public static T Load<T>(
string filename,
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0 ) where T : class, new()
{
// 1. Check cache first (no lock)
if( TryGetFromCache<T>( filename, out var cachedValue ) )
{
return cachedValue;
}
// 2. Get a lock specific to this filename and lock it.
var fileLock = s_loadingLocks.GetOrAdd( filename, _ => new object() );
lock( fileLock )
{
// 3. Double-check cache *inside* the lock
if( TryGetFromCache<T>( filename, out cachedValue ) )
{
return cachedValue;
}
// 4. Perform the actual load
log.warn( $"Loading {typeof( T ).Name}: {filename} ({reason} at {dbgName}:{dbgLine})" );
var newValue = ActualLoad<T>( filename, reason, dbgName, dbgPath, dbgLine );
// 5. Cache the new value
CacheResource( filename, newValue, reason );
return newValue;
}
}
/// <summary>
/// Manually adds or updates a resource in the cache.
/// </summary>
internal static void CacheResource<T>( string filename, T resource, string reason ) where T : class, new()
{
var weak = new WeakReference<T>( resource );
var holder = new ResourceHolder<T>( weak, filename, DateTime.Now );
ImmutableInterlocked.Update( ref ResCache<T>.s_cache, d => d.SetItem( filename, holder ) );
log.info( $"Cached {typeof( T ).Name}: {filename} ({reason})" );
}
/// <summary>
/// Tries to retrieve a resource from the cache.
/// </summary>
private static bool TryGetFromCache<T>( string filename, out T value ) where T : class, new()
{
if( ResCache<T>.s_cache.TryGetValue( filename, out var holder ) )
{
if( holder.WeakRef.TryGetTarget( out var v ) )
{
value = v;
return true;
}
log.info( $"{filename} was in cache but dropped (GC'd), needs reloading." );
}
value = default!;
return false;
}
/// <summary>
/// The core loading logic. Must be called within a lock.
/// </summary>
private static T ActualLoad<T>(
string filename,
string reason = "",
[CallerMemberName] string dbgName = "",
[CallerFilePath] string dbgPath = "",
[CallerLineNumber] int dbgLine = 0
) where T : class, new()
{
if( s_loaders.TryGetValue( typeof( T ), out var loaderHolder ) )
{
var loadedObject = loaderHolder.Load( filename );
if( loadedObject is T value )
{
var meta = (value as imm.Obj)?.Meta;
// If it's an immutable object, record its loading.
if( value is imm.Obj imm )
{
return (T)imm.Record( $"Loading bcs {reason}", dbgName, dbgPath, dbgLine );
}
return value;
}
else
{
log.error( $"Loader for {typeof( T ).Name} returned wrong type: {loadedObject?.GetType().Name}" );
return ResCache<T>.s_default;
}
}
else
{
log.error( $"Loader could not be found for type {typeof( T ).Name} for file {filename}" );
return ResCache<T>.s_default;
}
}
}