diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f8a05a1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,77 @@ +# ------------------------------------------------------------------------------ +# +# +# This code was generated. +# +# - To turn off auto-generation set: +# +# [GitHubActions (AutoGenerate = false)] +# +# - To trigger manual generation invoke: +# +# nuke --generate-configuration GitHubActions_ci --host GitHubActions +# +# +# ------------------------------------------------------------------------------ + +name: ci + +on: + push: + branches: + - master + - dev + - 'release/**' + pull_request: + branches: + - 'release/**' + +jobs: + ubuntu-latest: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v3 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - name: 'Run: Compile, Test' + run: ./build.cmd Compile Test + macos-latest: + name: macos-latest + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v3 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - name: 'Run: Compile, Test' + run: ./build.cmd Compile Test + windows-latest: + name: windows-latest + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 'Cache: .nuke/temp, ~/.nuget/packages' + uses: actions/cache@v3 + with: + path: | + .nuke/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - name: 'Run: Compile, Test' + run: ./build.cmd Compile Test diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6ef56e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Rikarin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 03a50f4..9ccec15 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Create game engine with requirements: ## Features - Entity Component System (Arch) +- Serialization & InputSystem is based on the Stride Engine ## Used libraries diff --git a/Rin.BuildEngine.Common/BuildStep.cs b/Rin.BuildEngine.Common/BuildStep.cs index cab7537..3318a59 100644 --- a/Rin.BuildEngine.Common/BuildStep.cs +++ b/Rin.BuildEngine.Common/BuildStep.cs @@ -195,12 +195,12 @@ internal void RegisterResult(IExecuteContext executeContext, ResultStatus status if (StepProcessed != null) { try { var outputObjectsGroups = executeContext.GetOutputObjectsGroups(); - MicrothreadLocalDatabases.MountDatabase(outputObjectsGroups); - StepProcessed(this, new BuildStepEventArgs(this, executeContext.Logger)); + MicroThreadLocalDatabases.MountDatabase(outputObjectsGroups); + StepProcessed(this, new(this, executeContext.Logger)); } catch (Exception ex) { executeContext.Logger.Error(ex, "Exception in command {Command}", this); } finally { - MicrothreadLocalDatabases.UnmountDatabase(); + MicroThreadLocalDatabases.UnmountDatabase(); } } } diff --git a/Rin.BuildEngine.Common/Command.cs b/Rin.BuildEngine.Common/Command.cs index 193a9cc..9e9d50e 100644 --- a/Rin.BuildEngine.Common/Command.cs +++ b/Rin.BuildEngine.Common/Command.cs @@ -1,5 +1,6 @@ using Rin.Core.Serialization; using Rin.Core.Serialization.Binary; +using Rin.Core.Serialization.Storage; using Rin.Core.Storage; using Rin.Core.TODO; using System.Reflection; diff --git a/Rin.BuildEngine.Common/CommandBuildStep.cs b/Rin.BuildEngine.Common/CommandBuildStep.cs index eb863f7..960a854 100644 --- a/Rin.BuildEngine.Common/CommandBuildStep.cs +++ b/Rin.BuildEngine.Common/CommandBuildStep.cs @@ -1,3 +1,4 @@ +using Rin.Core.IO; using Rin.Core.Storage; using Rin.Core.TODO; @@ -104,7 +105,7 @@ public override async Task Execute(IExecuteContext executeContext, if (stepInProgress != null) { Monitor.Exit(executeContext); monitorExited = true; - executeContext.Logger.Debug($"Command {Command} delayed because it is currently running..."); + executeContext.Logger.Debug("Command {Command} delayed because it is currently running...", Command); status = (await stepInProgress.ExecutedAsync()).Status; matchingResult = stepInProgress.Result; } else { @@ -112,7 +113,7 @@ public override async Task Execute(IExecuteContext executeContext, Monitor.Exit(executeContext); monitorExited = true; - executeContext.Logger.Debug($"Command {Command} scheduled..."); + executeContext.Logger.Debug("Command {Command} scheduled...", Command); // Register the cancel callback var cancellationTokenSource = executeContext.CancellationTokenSource; @@ -206,7 +207,7 @@ BuilderContext builderContext // Merge results from prerequisites // TODO: This will prevent us from overwriting this asset with different content as it will result in a write conflict // At some point we _might_ want to get rid of WaitBuildStep/ListBuildStep system and write a fully stateless input/output-based system; probably need further discussions - var fileProvider = MicrothreadLocalDatabases.DatabaseFileProvider; + var fileProvider = MicroThreadLocalDatabases.DatabaseFileProvider; if (fileProvider != null) { var assetIndexMap = fileProvider.ContentIndexMap; foreach (var prerequisiteStep in PrerequisiteSteps) { @@ -256,11 +257,11 @@ internal bool ShouldExecute( out CommandResultEntry matchingResult ) { var outputObjectsGroups = executeContext.GetOutputObjectsGroups(); - MicrothreadLocalDatabases.MountDatabase(outputObjectsGroups); + MicroThreadLocalDatabases.MountDatabase(outputObjectsGroups); try { matchingResult = FindMatchingResult(executeContext, previousResultCollection); } finally { - MicrothreadLocalDatabases.UnmountDatabase(); + MicroThreadLocalDatabases.UnmountDatabase(); } if (matchingResult == null || Command.ShouldForceExecution()) { diff --git a/Rin.BuildEngine.Common/FileVersionStorage.cs b/Rin.BuildEngine.Common/FileVersionStorage.cs index d78e7ea..ac77789 100644 --- a/Rin.BuildEngine.Common/FileVersionStorage.cs +++ b/Rin.BuildEngine.Common/FileVersionStorage.cs @@ -1,3 +1,5 @@ +using Rin.Core.Serialization; +using Rin.Core.Serialization.Serializers; using Rin.Core.Storage; using System.Text; @@ -35,8 +37,7 @@ public static bool Compact(string storagePath) { new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var keyValue in localTracker.GetValues()) { var filePath = keyValue.Key.Path; - KeyValuePair previousKeyValue; - if (!latestVersion.TryGetValue(filePath, out previousKeyValue) + if (!latestVersion.TryGetValue(filePath, out var previousKeyValue) || keyValue.Key.LastModifiedDate > previousKeyValue.Key.LastModifiedDate) { latestVersion[filePath] = keyValue; } @@ -68,21 +69,21 @@ protected override List> ReadEntries(Stre var key = new FileVersionKey { Path = values[0] }; if (!long.TryParse(values[1], out var dateTime)) { throw new InvalidOperationException( - "Unable to decode datetime [{0}] when reading file version index".ToFormat(values[1]) + $"Unable to decode datetime [{values[1]}] when reading file version index" ); } key.LastModifiedDate = new(dateTime); if (!long.TryParse(values[2], out key.FileSize)) { throw new InvalidOperationException( - "Unable to decode filesize [{0}] when reading file version index".ToFormat(values[2]) + $"Unable to decode filesize [{values[2]}] when reading file version index" ); } var objectIdStr = values[3]; if (!ObjectId.TryParse(objectIdStr, out ObjectId objectId)) { throw new InvalidOperationException( - "Unable to decode objectid [{0}] when reading file version index".ToFormat(objectIdStr) + $"Unable to decode ObjectId [{objectIdStr}] when reading file version index" ); } diff --git a/Rin.BuildEngine.Common/FileVersionTracker.cs b/Rin.BuildEngine.Common/FileVersionTracker.cs index dd3ee43..8dabe89 100644 --- a/Rin.BuildEngine.Common/FileVersionTracker.cs +++ b/Rin.BuildEngine.Common/FileVersionTracker.cs @@ -1,3 +1,4 @@ +using Rin.Core.Storage; using Serilog; namespace Rin.BuildEngine.Common; diff --git a/Rin.BuildEngine.Common/MicroThreadLocalDatabases.cs b/Rin.BuildEngine.Common/MicroThreadLocalDatabases.cs new file mode 100644 index 0000000..e7bbc34 --- /dev/null +++ b/Rin.BuildEngine.Common/MicroThreadLocalDatabases.cs @@ -0,0 +1,109 @@ +using Rin.Core.TODO; + +namespace Rin.BuildEngine.Common; + +/// +/// A static class that allows to have a different on each . +/// Objects can still be shared +/// between micro-threads by using the method. +/// +public static class MicroThreadLocalDatabases { + static readonly Dictionary SharedOutputObjects = new(); + static readonly MicroThreadLocal MicroThreadLocalDatabaseFileProvider; + + public static IDatabaseFileProviderService ProviderService { get; } + + /// + /// Gets a value indicating whether this instance has a valid database file provider. + /// + /// true if this instance has database file provider; otherwise, false. + public static bool HasValidDatabaseFileProvider => MicroThreadLocalDatabaseFileProvider.Value != null; + + /// + /// Gets the currently mounted microthread-local database provider. + /// + public static DatabaseFileProvider DatabaseFileProvider => MicroThreadLocalDatabaseFileProvider.Value; + + static MicroThreadLocalDatabases() { + MicroThreadLocalDatabaseFileProvider = new MicroThreadLocal(); + + ProviderService = new MicroThreadLocalProviderService(); + } + + /// + /// Merges the given dictionary of build output objects into the shared group. Objects merged here will be integrated + /// to every database. + /// + /// The dictionary containing the to merge into the shared group. + public static void AddToSharedGroup(IReadOnlyDictionary outputObjects) { + lock (SharedOutputObjects) { + foreach (var outputObject in outputObjects) { + SharedOutputObjects[outputObject.Key] = outputObject.Value; + } + } + } + + /// + /// Gets a containing only objects from the shared group. + /// The shared group is a group of objects registered via and shared amongst all + /// databases. + /// + /// A that can provide objects from the common group. + public static DatabaseFileProvider GetSharedDatabase() => CreateDatabase(CreateTransaction(null)); + + /// + /// Creates and mounts a database containing the given output object groups and the shared group in the + /// microthread-local + /// . + /// + /// A collection of dictionaries representing a group of output object. + public static void MountDatabase(IEnumerable> outputObjectsGroups) { + MountDatabase(CreateTransaction(outputObjectsGroups)); + } + + /// + /// Creates and mounts a database containing output objects from the shared group in the microthread-local + /// . + /// + public static void MountCommonDatabase() { + MicroThreadLocalDatabaseFileProvider.Value = CreateDatabase(CreateTransaction(null)); + } + + /// + /// Unmounts the currently mounted microthread-local database. + /// + public static void UnmountDatabase() { + MicroThreadLocalDatabaseFileProvider.ClearValue(); + } + + public static IEnumerable> GetOutputObjectsGroups( + IEnumerable> transactionOutputObjectsGroups + ) { + if (transactionOutputObjectsGroups != null) { + foreach (var outputObjects in transactionOutputObjectsGroups) { + yield return outputObjects; + } + } + + yield return SharedOutputObjects; + } + + static DatabaseFileProvider CreateDatabase(BuildTransaction transaction) => + new DatabaseFileProvider(new BuildTransaction.DatabaseContentIndexMap(transaction), Builder.ObjectDatabase); + + internal static void MountDatabase(BuildTransaction transaction) { + MicroThreadLocalDatabaseFileProvider.Value = CreateDatabase(transaction); + } + + internal static BuildTransaction CreateTransaction( + IEnumerable> transactionOutputObjectsGroups + ) => + new BuildTransaction( + Builder.ObjectDatabase.ContentIndexMap, + GetOutputObjectsGroups(transactionOutputObjectsGroups) + ); + + class MicroThreadLocalProviderService : IDatabaseFileProviderService { + public DatabaseFileProvider FileProvider => MicroThreadLocalDatabaseFileProvider.Value; + } +} diff --git a/Rin.BuildEngine.Common/Rin.BuildEngine.Common.csproj b/Rin.BuildEngine.Common/Rin.BuildEngine.Common.csproj index eadbca0..021419a 100644 --- a/Rin.BuildEngine.Common/Rin.BuildEngine.Common.csproj +++ b/Rin.BuildEngine.Common/Rin.BuildEngine.Common.csproj @@ -5,6 +5,8 @@ enable + + diff --git a/Rin.Core.MicroThreading/Channel.cs b/Rin.Core.MicroThreading/Channel.cs new file mode 100644 index 0000000..593e2be --- /dev/null +++ b/Rin.Core.MicroThreading/Channel.cs @@ -0,0 +1,107 @@ +namespace Rin.Core.MicroThreading; + +/// +/// Provides a communication mechanism between . +/// +/// +/// can send and receive to a . Depending on the +/// , +/// sending or receiving might be suspended and yield execution to another +/// . +/// +/// The type of element handled by this channel. +// TODO: Thread-safety +public class Channel { + readonly Queue> receivers = new(); + readonly Queue> senders = new(); + + /// + /// Gets or sets the preference, allowing you to customize how and behave + /// regarding scheduling. + /// + /// + /// The preference. + /// + public ChannelPreference Preference { get; set; } + + /// + /// Gets the balance, which is the number of waiting to send (if greater than 0) or receive + /// (if smaller than 0). + /// + /// + /// The balance. + /// + public int Balance => senders.Count - receivers.Count; + + public Channel() { + Preference = ChannelPreference.PreferReceiver; + } + + public void Reset() { + receivers.Clear(); + senders.Clear(); + } + + /// + /// Sends a value over the channel. If no other is waiting for data, the sender will be + /// blocked. + /// If someone was waiting for data, which of the sender or receiver continues next depends on + /// . + /// + /// The data. + /// Awaitable data. + public ChannelMicroThreadAwaiter Send(T data) { + if (receivers.Count == 0) { + // Nobody receiving, let's wait until something comes up + var microThread = MicroThread.Current; + var waitingMicroThread = ChannelMicroThreadAwaiter.New(microThread); + waitingMicroThread.Result = data; + senders.Enqueue(waitingMicroThread); + return waitingMicroThread; + } + + var receiver = receivers.Dequeue(); + receiver.Result = data; + if (Preference == ChannelPreference.PreferSender) { + receiver.MicroThread.ScheduleContinuation(ScheduleMode.Last, receiver.Continuation); + } else if (Preference == ChannelPreference.PreferReceiver) { + receiver.MicroThread.ScheduleContinuation(ScheduleMode.First, receiver.Continuation); + throw new NotImplementedException(); + //await Scheduler.Yield(); + } + + receiver.IsCompleted = true; + return receiver; + } + + /// + /// Receives a value over the channel. If no other is sending data, the receiver will be + /// blocked. + /// If someone was sending data, which of the sender or receiver continues next depends on . + /// + /// Awaitable data. + public ChannelMicroThreadAwaiter Receive() { + if (senders.Count == 0) { + var microThread = MicroThread.Current; + if (microThread == null) { + throw new("Cannot receive out of micro-thread context."); + } + + var waitingMicroThread = ChannelMicroThreadAwaiter.New(microThread); + receivers.Enqueue(waitingMicroThread); + return waitingMicroThread; + } + + var sender = senders.Dequeue(); + if (Preference == ChannelPreference.PreferReceiver) { + sender.MicroThread.ScheduleContinuation(ScheduleMode.Last, sender.Continuation); + } else if (Preference == ChannelPreference.PreferSender) { + sender.MicroThread.ScheduleContinuation(ScheduleMode.First, sender.Continuation); + throw new NotImplementedException(); + //await Scheduler.Yield(); + } + + sender.IsCompleted = true; + return sender; + } +} diff --git a/Rin.Core.MicroThreading/ChannelMicroThreadAwaiter.cs b/Rin.Core.MicroThreading/ChannelMicroThreadAwaiter.cs new file mode 100644 index 0000000..36f8f56 --- /dev/null +++ b/Rin.Core.MicroThreading/ChannelMicroThreadAwaiter.cs @@ -0,0 +1,68 @@ +using System.Runtime.CompilerServices; + +namespace Rin.Core.MicroThreading; + +public class ChannelMicroThreadAwaiter : ICriticalNotifyCompletion { + internal MicroThread? MicroThread; + internal Action Continuation; + internal T Result; + static readonly List> pool = new(); + + bool isCompleted; + + public bool IsCompleted { + get => isCompleted || MicroThread is { IsOver: true }; + set => isCompleted = value; + } + + public ChannelMicroThreadAwaiter(MicroThread microThread) { + MicroThread = microThread; + } + + public static ChannelMicroThreadAwaiter New(MicroThread microThread) { + lock (pool) { + if (pool.Count > 0) { + var index = pool.Count - 1; + var lastItem = pool[index]; + pool.RemoveAt(index); + + lastItem.MicroThread = microThread; + return lastItem; + } + + return new(microThread); + } + } + + public ChannelMicroThreadAwaiter GetAwaiter() => this; + + public void OnCompleted(Action continuation) { + Continuation = continuation; + } + + public void UnsafeOnCompleted(Action continuation) { + Continuation = continuation; + } + + public T GetResult() { + // Check Task Result (exception, etc...) + MicroThread.CancellationToken.ThrowIfCancellationRequested(); + + var result = Result; + + // After result has been taken, we can reuse this item, so put it in the pool + // We mitigate pool size, but another approach than hard limit might be interesting + lock (pool) { + if (pool.Count < 4096) { + isCompleted = false; + MicroThread = null; + Continuation = null; + Result = default; + } + + pool.Add(this); + } + + return result; + } +} diff --git a/Rin.Core.MicroThreading/ChannelPreference.cs b/Rin.Core.MicroThreading/ChannelPreference.cs new file mode 100644 index 0000000..3998a89 --- /dev/null +++ b/Rin.Core.MicroThreading/ChannelPreference.cs @@ -0,0 +1,7 @@ +namespace Rin.Core.MicroThreading; + +public enum ChannelPreference { + PreferReceiver = -1, + //Neutral = 0, + PreferSender = 1 +} diff --git a/Rin.Core.MicroThreading/IMicroThreadSynchronizationContext.cs b/Rin.Core.MicroThreading/IMicroThreadSynchronizationContext.cs new file mode 100644 index 0000000..62f5957 --- /dev/null +++ b/Rin.Core.MicroThreading/IMicroThreadSynchronizationContext.cs @@ -0,0 +1,5 @@ +namespace Rin.Core.MicroThreading; + +interface IMicroThreadSynchronizationContext { + MicroThread MicroThread { get; } +} diff --git a/Rin.Core.MicroThreading/MicroThread.cs b/Rin.Core.MicroThreading/MicroThread.cs new file mode 100644 index 0000000..467c514 --- /dev/null +++ b/Rin.Core.MicroThreading/MicroThread.cs @@ -0,0 +1,283 @@ +using Rin.Core.Collections; +using Rin.Diagnostics; +using System.Diagnostics; + +namespace Rin.Core.MicroThreading; + +/// +/// Represents an execution context managed by a , that can cooperatively yield execution to +/// another at any point (usually using async calls). +/// +public class MicroThread { + /// + /// Gets the attached properties to this component. + /// + public PropertyContainer Tags; + + internal ProfilingKey? ProfilingKey; + internal PriorityQueueNode ScheduledLinkedListNode; + internal LinkedListNode AllLinkedListNode; // Also used as lock for "CompletionTask" + internal MicroThreadCallbackList Callbacks; + internal SynchronizationContext SynchronizationContext; + + static long globalCounterId; + + int state; + readonly CancellationTokenSource cancellationTokenSource; + + /// + /// Gets or sets the priority of this . + /// + /// + /// The priority. + /// + public long Priority { + get => ScheduledLinkedListNode.Value.Priority; + set { + if (ScheduledLinkedListNode.Value.Priority != value) { + Reschedule(ScheduleMode.First, value); + } + } + } + + /// + /// Gets the id of this . + /// + /// + /// The id. + /// + public long Id { get; private set; } + + /// + /// Gets or sets the name of this . + /// + /// + /// The name. + /// + public string Name { get; set; } + + /// + /// Gets the scheduler associated with this . + /// + /// The scheduler associated with this . + public Scheduler Scheduler { get; private set; } + + /// + /// Gets the state of this . + /// + /// The state of this . + public MicroThreadState State { + get => (MicroThreadState)state; + internal set => state = (int)value; + } + + /// + /// Gets the exception that was thrown by this . + /// + /// It could come from either internally, or from + /// + /// if it was successfully processed. + /// The exception. + public Exception? Exception { get; private set; } + + /// + /// Gets the flags. + /// + /// + /// The flags. + /// + public MicroThreadFlags Flags { get; private set; } + + /// + /// Gets or sets the scheduling mode. + /// + /// + /// The scheduling mode. + /// + public ScheduleMode ScheduleMode { get; set; } + + /// + /// A token for listening to the cancellation of the MicroThread. + /// + public CancellationToken CancellationToken => cancellationTokenSource.Token; + + /// + /// Indicates whether the MicroThread is terminated or not, either in Completed, Canceled or Failed status. + /// + public bool IsOver => State is MicroThreadState.Completed or MicroThreadState.Canceled or MicroThreadState.Failed; + + /// + /// Gets the current micro thread (self). + /// + /// The current micro thread (self). + public static MicroThread Current => Scheduler.CurrentMicroThread; + + /// + /// Gets or sets the task that will be executed upon completion (used internally for ) + /// + internal TaskCompletionSource? CompletionTask { get; set; } + + public MicroThread(Scheduler scheduler, MicroThreadFlags flags = MicroThreadFlags.None) { + Id = Interlocked.Increment(ref globalCounterId); + Scheduler = scheduler; + ScheduledLinkedListNode = new(new(this)); + AllLinkedListNode = new(this); + ScheduleMode = ScheduleMode.Last; + Flags = flags; + Tags = new(this); + cancellationTokenSource = new(); + } + + public void Migrate(Scheduler scheduler) { + throw new NotImplementedException(); + } + + public void Remove() { + throw new NotImplementedException(); + } + + /// + /// Starts this with the specified function. + /// + /// The micro thread function. + /// The schedule mode. + /// MicroThread was already started before. + public void Start(Func microThreadFunction, ScheduleMode scheduleMode = ScheduleMode.Last) { + // TODO: Interlocked compare exchange? + if (Interlocked.CompareExchange(ref state, (int)MicroThreadState.Starting, (int)MicroThreadState.None) + != (int)MicroThreadState.None) { + throw new InvalidOperationException("MicroThread was already started before."); + } + + async void WrappedMicroThreadFunction() { + try { + State = MicroThreadState.Running; + + await microThreadFunction(); + + if (State != MicroThreadState.Running) { + throw new InvalidOperationException("MicroThread completed in an invalid state."); + } + + State = MicroThreadState.Completed; + } catch (OperationCanceledException e) { + // Exit gracefully on cancellation exceptions + SetException(e); + } catch (Exception e) { + Scheduler.Log.Error("Unexpected exception while executing a micro-thread", e); + SetException(e); + } finally { + lock (Scheduler.AllMicroThreads) { + Scheduler.AllMicroThreads.Remove(AllLinkedListNode); + } + } + } + + void Callback() { + SynchronizationContext = new MicroThreadSynchronizationContext(this); + SynchronizationContext.SetSynchronizationContext(SynchronizationContext); + + WrappedMicroThreadFunction(); + } + + lock (Scheduler.AllMicroThreads) { + Scheduler.AllMicroThreads.AddLast(AllLinkedListNode); + } + + ScheduleContinuation(scheduleMode, Callback); + } + + /// + /// Yields to this . + /// + /// Task. + public async Task Run() { + Reschedule(ScheduleMode.First, Priority); + var currentScheduler = Scheduler.Current; + if (currentScheduler == Scheduler) { + await Scheduler.Yield(); + } + } + + /// + /// Cancels the . + /// + public void Cancel() { + // TODO: If we unschedule the microthread after cancellation, we never give user code the chance to throw OperationCanceledException. + // If we don't, we can't be sure that the MicroThread ends. + // Should we run continuations manually? + + // Notify awaitables + cancellationTokenSource.Cancel(); + + // Unschedule microthread + //lock (Scheduler.scheduledMicroThreads) + //{ + // if (ScheduledLinkedListNode.Index != -1) + // { + // Scheduler.scheduledMicroThreads.Remove(ScheduledLinkedListNode); + // } + //} + } + + MicroThreadCallbackNode NewCallback() { + MicroThreadCallbackNode node; + var pool = Scheduler.CallbackNodePool; + + if (Scheduler.CallbackNodePool.Count > 0) { + var index = pool.Count - 1; + node = pool[index]; + pool.RemoveAt(index); + } else { + node = new(); + } + + return node; + } + + internal void SetException(Exception exception) { + Exception = exception; + + // Depending on if exception was raised from outside or inside, set appropriate state + State = exception is OperationCanceledException ? MicroThreadState.Canceled : MicroThreadState.Failed; + } + + internal void Reschedule(ScheduleMode scheduleMode, long newPriority) { + lock (Scheduler.ScheduledEntries) { + if (ScheduledLinkedListNode.Index != -1) { + Scheduler.ScheduledEntries.Remove(ScheduledLinkedListNode); + ScheduledLinkedListNode.Value.Priority = newPriority; + Scheduler.Schedule(ScheduledLinkedListNode, scheduleMode); + } else { + ScheduledLinkedListNode.Value.Priority = newPriority; + } + } + } + + internal void ScheduleContinuation(ScheduleMode scheduleMode, SendOrPostCallback callback, object callbackState) { + Debug.Assert(callback != null); + lock (Scheduler.ScheduledEntries) { + var node = NewCallback(); + node.SendOrPostCallback = callback; + node.CallbackState = callbackState; + Callbacks.Add(node); + + if (ScheduledLinkedListNode.Index == -1) { + Scheduler.Schedule(ScheduledLinkedListNode, scheduleMode); + } + } + } + + internal void ScheduleContinuation(ScheduleMode scheduleMode, Action callback) { + Debug.Assert(callback != null); + lock (Scheduler.ScheduledEntries) { + var node = NewCallback(); + node.MicroThreadAction = callback; + Callbacks.Add(node); + + if (ScheduledLinkedListNode.Index == -1) { + Scheduler.Schedule(ScheduledLinkedListNode, scheduleMode); + } + } + } +} diff --git a/Rin.Core.MicroThreading/MicroThreadCallbackList.cs b/Rin.Core.MicroThreading/MicroThreadCallbackList.cs new file mode 100644 index 0000000..bf1fc21 --- /dev/null +++ b/Rin.Core.MicroThreading/MicroThreadCallbackList.cs @@ -0,0 +1,29 @@ +namespace Rin.Core.MicroThreading; + +struct MicroThreadCallbackList { + public MicroThreadCallbackNode First { get; private set; } + public MicroThreadCallbackNode Last { get; private set; } + + public void Add(MicroThreadCallbackNode node) { + if (First == null) { + First = node; + } else { + Last.Next = node; + } + + Last = node; + } + + public bool TakeFirst(out MicroThreadCallbackNode callback) { + callback = First; + + if (First == null) { + return false; + } + + First = callback.Next; + callback.Next = null; + + return true; + } +} diff --git a/Rin.Core.MicroThreading/MicroThreadCallbackNode.cs b/Rin.Core.MicroThreading/MicroThreadCallbackNode.cs new file mode 100644 index 0000000..57961ff --- /dev/null +++ b/Rin.Core.MicroThreading/MicroThreadCallbackNode.cs @@ -0,0 +1,22 @@ +namespace Rin.Core.MicroThreading; + +class MicroThreadCallbackNode { + public Action? MicroThreadAction; + public SendOrPostCallback? SendOrPostCallback; + public object? CallbackState; + public MicroThreadCallbackNode? Next; + + public void Invoke() { + if (MicroThreadAction != null) { + MicroThreadAction(); + } else { + SendOrPostCallback(CallbackState); + } + } + + public void Clear() { + MicroThreadAction = null; + SendOrPostCallback = null; + CallbackState = null; + } +} diff --git a/Rin.Core.MicroThreading/MicroThreadFlags.cs b/Rin.Core.MicroThreading/MicroThreadFlags.cs new file mode 100644 index 0000000..dded586 --- /dev/null +++ b/Rin.Core.MicroThreading/MicroThreadFlags.cs @@ -0,0 +1,19 @@ +namespace Rin.Core.MicroThreading; + +[Flags] +public enum MicroThreadFlags { + None = 0, + + /// + /// If a faulted is not being waited on, do not propagate exception outside of + /// . + /// + /// + /// If an exception happens in a , two things can happen. + /// Either something was waiting on it (i.e. with ), in that case exception will be + /// propagated to waiting code. + /// Otherwise, exception will be rethrow outside of . + /// This flags allows exception to be ignored even if nothing was waiting on it. + /// + IgnoreExceptions = 1 +} diff --git a/Rin.Core.MicroThreading/MicroThreadLocal.cs b/Rin.Core.MicroThreading/MicroThreadLocal.cs new file mode 100644 index 0000000..655fe82 --- /dev/null +++ b/Rin.Core.MicroThreading/MicroThreadLocal.cs @@ -0,0 +1,103 @@ +using System.Runtime.CompilerServices; + +namespace Rin.Core.MicroThreading; + +/// +/// Provides MicroThread-local storage of data. +/// +/// Type of data stored. +public class MicroThreadLocal where T : class { + readonly Func? valueFactory; + readonly ConditionalWeakTable values = new(); + + /// + /// The value return when we are not in a micro thread. That is the value return when + /// 'Scheduler.CurrentMicroThread==null' + /// + T? valueOutOfMicroThread; + + /// + /// Indicate if the value out of micro-thread have been set at least once or not. + /// + bool valueOutOfMicroThreadSet; + + /// + /// Gets or sets the value for the current microthread. + /// + /// + /// The value for the current microthread. + /// + public T Value { + get { + T value; + var microThread = Scheduler.CurrentMicroThread; + + lock (values) { + if (microThread == null) { + if (!valueOutOfMicroThreadSet) { + valueOutOfMicroThread = valueFactory?.Invoke(); + } + + value = valueOutOfMicroThread; + } else if (!values.TryGetValue(microThread, out value)) { + values.Add(microThread, value = valueFactory != null ? valueFactory() : default); + } + } + + return value; + } + set { + var microThread = Scheduler.CurrentMicroThread; + + lock (values) { + if (microThread == null) { + valueOutOfMicroThread = value; + valueOutOfMicroThreadSet = true; + } else { + values.Remove(microThread); + values.Add(microThread, value); + } + } + } + } + + public bool IsValueCreated { + get { + var microThread = Scheduler.CurrentMicroThread; + + lock (values) { + return microThread == null ? valueOutOfMicroThreadSet : values.TryGetValue(microThread, out _); + } + } + } + + /// + /// Initializes a new instance of the class. + /// + public MicroThreadLocal() + : this(null) { } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The value factory invoked to create a value when is retrieved before + /// having been previously initialized. + /// + public MicroThreadLocal(Func valueFactory) { + this.valueFactory = valueFactory; + } + + public void ClearValue() { + var microThread = Scheduler.CurrentMicroThread; + + lock (values) { + if (microThread == null) { + valueOutOfMicroThread = default; + valueOutOfMicroThreadSet = false; + } else { + values.Remove(microThread); + } + } + } +} diff --git a/Rin.Core.MicroThreading/MicroThreadProfiling.cs b/Rin.Core.MicroThreading/MicroThreadProfiling.cs new file mode 100644 index 0000000..3ebf18f --- /dev/null +++ b/Rin.Core.MicroThreading/MicroThreadProfiling.cs @@ -0,0 +1,14 @@ +using Rin.Diagnostics; +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace Rin.Core.MicroThreading; + +public static class MicroThreadProfiling { + public static readonly ActivitySource MicroThreadingSource = new("Rin.Core.MicroThreading"); + public static readonly Meter MicroThreadingMeter = new("Rin.Core.MicroThreading"); + + public static readonly Histogram MicroThread = MicroThreadingMeter.CreateHistogram("MicroThread"); + + public static readonly ProfilingKey ProfilingKey = new(MicroThread, MicroThreadingSource.StartActivity("MicroThread")); +} diff --git a/Rin.Core.MicroThreading/MicroThreadState.cs b/Rin.Core.MicroThreading/MicroThreadState.cs new file mode 100644 index 0000000..b6d2ff7 --- /dev/null +++ b/Rin.Core.MicroThreading/MicroThreadState.cs @@ -0,0 +1,10 @@ +namespace Rin.Core.MicroThreading; + +public enum MicroThreadState { + None, + Starting, + Running, + Completed, + Canceled, + Failed +} \ No newline at end of file diff --git a/Rin.Core.MicroThreading/MicroThreadSynchronizationContext.cs b/Rin.Core.MicroThreading/MicroThreadSynchronizationContext.cs new file mode 100644 index 0000000..2a1b2e6 --- /dev/null +++ b/Rin.Core.MicroThreading/MicroThreadSynchronizationContext.cs @@ -0,0 +1,30 @@ +namespace Rin.Core.MicroThreading; + +public class MicroThreadSynchronizationContext : SynchronizationContext, IMicroThreadSynchronizationContext { + readonly MicroThread microThread; + + MicroThread IMicroThreadSynchronizationContext.MicroThread => microThread; + + public MicroThreadSynchronizationContext(MicroThread microThread) { + this.microThread = microThread; + } + + public override SynchronizationContext CreateCopy() => this; + + public override void Post(SendOrPostCallback d, object state) { + // There is two case: + // 1/ We are either in normal MicroThread inside Scheduler.Step() (CurrentThread test), + // in which case we will directly execute the callback to avoid further processing from scheduler. + // Also, note that Wait() sends us event that are supposed to come back into scheduler. + // Note: As it will end up on the callstack, it might be better to Schedule it instead (to avoid overflow)? + // 2/ Otherwise, we just received an external task continuation (i.e. Task.Sleep()), or a microthread triggering another, + // so schedule it so that it comes back in our regular scheduler. + if (microThread.Scheduler.RunningMicroThread == microThread) { + d(state); + } else if (microThread.State == MicroThreadState.Completed) { + throw new InvalidOperationException("MicroThread is already completed but still posting continuations."); + } else { + microThread.ScheduleContinuation(microThread.ScheduleMode, d, state); + } + } +} diff --git a/Rin.Core.MicroThreading/MicroThreadYieldAwaiter.cs b/Rin.Core.MicroThreading/MicroThreadYieldAwaiter.cs new file mode 100644 index 0000000..95dab42 --- /dev/null +++ b/Rin.Core.MicroThreading/MicroThreadYieldAwaiter.cs @@ -0,0 +1,33 @@ +using System.Runtime.CompilerServices; + +namespace Rin.Core.MicroThreading; + +public struct MicroThreadYieldAwaiter : INotifyCompletion { + readonly MicroThread microThread; + + public bool IsCompleted { + get { + if (microThread.IsOver) { + return true; + } + + lock (microThread.Scheduler.ScheduledEntries) { + return microThread.Scheduler.ScheduledEntries.Count == 0; + } + } + } + + public MicroThreadYieldAwaiter(MicroThread microThread) { + this.microThread = microThread; + } + + public MicroThreadYieldAwaiter GetAwaiter() => this; + + public void GetResult() { + microThread.CancellationToken.ThrowIfCancellationRequested(); + } + + public void OnCompleted(Action continuation) { + microThread.ScheduleContinuation(ScheduleMode.Last, continuation); + } +} diff --git a/Rin.Core.MicroThreading/Rin.Core.MicroThreading.csproj b/Rin.Core.MicroThreading/Rin.Core.MicroThreading.csproj index 9d130cd..83ad43a 100644 --- a/Rin.Core.MicroThreading/Rin.Core.MicroThreading.csproj +++ b/Rin.Core.MicroThreading/Rin.Core.MicroThreading.csproj @@ -4,4 +4,12 @@ enable enable + + + + + + + + diff --git a/Rin.Core.MicroThreading/ScheduleMode.cs b/Rin.Core.MicroThreading/ScheduleMode.cs new file mode 100644 index 0000000..f779af5 --- /dev/null +++ b/Rin.Core.MicroThreading/ScheduleMode.cs @@ -0,0 +1,6 @@ +namespace Rin.Core.MicroThreading; + +public enum ScheduleMode { + First, + Last +} diff --git a/Rin.Core.MicroThreading/Scheduler.cs b/Rin.Core.MicroThreading/Scheduler.cs new file mode 100644 index 0000000..78b818c --- /dev/null +++ b/Rin.Core.MicroThreading/Scheduler.cs @@ -0,0 +1,321 @@ +using Rin.Core.Collections; +using Rin.Diagnostics; +using Serilog; +using System.Runtime.ExceptionServices; + +namespace Rin.Core.MicroThreading; + +/// +/// Scheduler that manage a group of cooperating . +/// +/// +/// Microthreading provides a way to execute many small execution contexts who cooperatively yield to each others. +/// +public class Scheduler { + // An ever-increasing counter that will be used to have a "stable" microthread scheduling (first added is first scheduled) + internal long SchedulerCounter; + + internal PriorityNodeQueue ScheduledEntries = new(); + internal LinkedList AllMicroThreads = new(); + + internal List CallbackNodePool = new(); + + // internal static readonly Logger Log = GlobalLogger.GetLogger("Scheduler"); + readonly ILogger Log = Serilog.Log.ForContext(); + + readonly ThreadLocal runningMicroThread = new(); + + /// + /// Gets the current running micro thread in this scheduler through . + /// + /// The current running micro thread in this scheduler. + public MicroThread RunningMicroThread => runningMicroThread.Value; + + /// + /// Gets the scheduler associated with current micro thread. + /// + /// The scheduler associated with current micro thread. + public static Scheduler? Current => CurrentMicroThread?.Scheduler; + + /// + /// Gets the list of every non-stopped micro threads. + /// + /// The list of every non-stopped micro threads. + public ICollection MicroThreads => AllMicroThreads; + + /// + /// Gets the current micro thread (self). + /// + /// The current micro thread (self). + public static MicroThread? CurrentMicroThread => + (SynchronizationContext.Current as IMicroThreadSynchronizationContext)?.MicroThread; + + protected Channel FrameChannel { get; } + + /// + /// Gets or sets a value indicating whether microthread exceptions are propagated (and crashes the process). Default to + /// true. + /// This can be overridden to false per by using + /// . + /// + /// + /// true if [propagate exceptions]; otherwise, false. + /// + internal bool PropagateExceptions { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public Scheduler() { + PropagateExceptions = true; + FrameChannel = new() { Preference = ChannelPreference.PreferSender }; + } + + /// + /// Yields execution. + /// If any other micro thread is pending, it will be run now and current micro thread will be scheduled as last. + /// + /// Task that will resume later during same frame. + public static MicroThreadYieldAwaiter Yield() => new(CurrentMicroThread); + + /// + /// Yields execution until next frame. + /// + /// Task that will resume next frame. + public ChannelMicroThreadAwaiter NextFrame() { + if (MicroThread.Current == null) { + throw new("NextFrame cannot be called out of the micro-thread context."); + } + + return FrameChannel.Receive(); + } + + /// + /// Runs until no runnable tasks left. + /// This function is reentrant. + /// + public void Run() { + var managedThreadId = Thread.CurrentThread.ManagedThreadId; + var callbacks = default(MicroThreadCallbackList); + + while (true) { + SchedulerEntry schedulerEntry; + MicroThread? microThread; + lock (ScheduledEntries) { + // Reclaim callbacks of previous microthread + MicroThreadCallbackNode callback; + while (callbacks.TakeFirst(out callback)) { + callback.Clear(); + CallbackNodePool.Add(callback); + } + + if (ScheduledEntries.Count == 0) { + break; + } + + schedulerEntry = ScheduledEntries.Dequeue(); + microThread = schedulerEntry.MicroThread; + if (microThread != null) { + callbacks = microThread.Callbacks; + microThread.Callbacks = default; + } + } + + // Since it can be reentrant, it should be restored after running the callback. + var previousRunningMicrothread = runningMicroThread.Value; + if (previousRunningMicrothread != null) { + MicroThreadCallbackEnd?.Invoke( + this, + new(previousRunningMicrothread, managedThreadId) + ); + } + + runningMicroThread.Value = microThread; + + if (microThread != null) { + var previousSyncContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(microThread.SynchronizationContext); + + // TODO: Do we still need to try/catch here? Everything should be caught in the continuation wrapper and put into MicroThread.Exception + try { + if (microThread.State == MicroThreadState.Starting) { + MicroThreadStarted?.Invoke(this, new(microThread, managedThreadId)); + } + + MicroThreadCallbackStart?.Invoke(this, new(microThread, managedThreadId)); + + var profilingKey = microThread.ProfilingKey + ?? schedulerEntry.ProfilingKey ?? MicroThreadProfiling.ProfilingKey; + using (profilingKey.Begin()) { + var callback = callbacks.First; + while (callback != null) { + callback.Invoke(); + callback = callback.Next; + } + } + } catch (Exception e) { + Log.Error("Unexpected exception while executing a micro-thread", e); + microThread.SetException(e); + } finally { + MicroThreadCallbackEnd?.Invoke(this, new(microThread, managedThreadId)); + + SynchronizationContext.SetSynchronizationContext(previousSyncContext); + if (microThread.IsOver) { + lock (microThread.AllLinkedListNode) { + if (microThread.CompletionTask != null) { + if (microThread.State is MicroThreadState.Failed or MicroThreadState.Canceled) { + microThread.CompletionTask.TrySetException(microThread.Exception); + } else { + microThread.CompletionTask.TrySetResult(1); + } + } else if (microThread is { State: MicroThreadState.Failed, Exception: not null }) { + // Nothing was listening on the micro thread and it crashed + // Let's treat it as unhandled exception and propagate it + // Use ExceptionDispatchInfo.Capture to not overwrite callstack + if (PropagateExceptions + && (microThread.Flags & MicroThreadFlags.IgnoreExceptions) + != MicroThreadFlags.IgnoreExceptions) { + ExceptionDispatchInfo.Capture(microThread.Exception).Throw(); + } + } + + MicroThreadEnded?.Invoke(this, new(microThread, managedThreadId)); + } + } + + runningMicroThread.Value = previousRunningMicrothread; + if (previousRunningMicrothread != null) { + MicroThreadCallbackStart?.Invoke( + this, + new(previousRunningMicrothread, managedThreadId) + ); + } + } + } else { + try { + var profilingKey = schedulerEntry.ProfilingKey ?? MicroThreadProfiling.ProfilingKey; + using (profilingKey.Begin()) { + schedulerEntry.Action(); + } + } catch (Exception e) { + ActionException?.Invoke(this, schedulerEntry, e); + } + } + } + + while (FrameChannel.Balance < 0) { + FrameChannel.Send(0); + } + } + + /// + /// Creates a micro thread out of the specified function and schedules it as last micro thread to run in this + /// scheduler. + /// Note that in case of multithreaded scheduling, it might start before this function returns. + /// + /// The function to create a micro thread from. + /// The flags. + /// A micro thread. + public MicroThread Add(Func microThreadFunction, MicroThreadFlags flags = MicroThreadFlags.None) { + var microThread = new MicroThread(this, flags); + microThread.Start(microThreadFunction); + return microThread; + } + + /// + /// Creates a new empty micro thread, that could later be started with . + /// + /// A new empty micro thread. + public MicroThread Create() => new(this); + + /// + /// Task that will completes when all MicroThread executions are completed. + /// + /// The micro threads. + /// A task that will complete when all micro threads are complete. + public async Task WhenAll(params MicroThread[] microThreads) { + var currentMicroThread = CurrentMicroThread; + Task[] continuationTasks; + var tcs = new TaskCompletionSource(); + + // Need additional checks: Not sure if we should switch to return a Task and set it before returning it. + // It should continue execution right away (no execution flow yielding). + lock (microThreads) { + if (microThreads.All(x => x.State == MicroThreadState.Completed)) { + return; + } + + if (microThreads.Any(x => x.State == MicroThreadState.Failed || x.State == MicroThreadState.Canceled)) { + throw new AggregateException(microThreads.Select(x => x.Exception).Where(x => x != null)); + } + + var completionTasks = new List>(); + foreach (var thread in microThreads) { + if (!thread.IsOver) { + lock (thread.AllLinkedListNode) { + if (thread.CompletionTask == null) { + thread.CompletionTask = new(); + } + } + + completionTasks.Add(thread.CompletionTask.Task); + } + } + + continuationTasks = completionTasks.ToArray(); + } + + // Force tasks exception to be checked and propagated + await Task.Factory.ContinueWhenAll(continuationTasks, tasks => Task.WaitAll(tasks)); + } + + public event EventHandler MicroThreadStarted; + public event EventHandler MicroThreadEnded; + + public event EventHandler MicroThreadCallbackStart; + public event EventHandler MicroThreadCallbackEnd; + + // This is part of temporary internal API, this should be improved before exposed + internal event Action ActionException; + + // TODO: We will need a better API than exposing PriorityQueueNode before we can make this public. + internal PriorityQueueNode Add( + Action simpleAction, + int priority = 0, + object? token = null, + ProfilingKey? profilingKey = null + ) { + var schedulerEntryNode = new PriorityQueueNode( + new(simpleAction, priority) { Token = token, ProfilingKey = profilingKey } + ); + Schedule(schedulerEntryNode, ScheduleMode.Last); + return schedulerEntryNode; + } + + internal PriorityQueueNode Create(Action simpleAction, long priority) => + new(new(simpleAction, priority)); + + internal void Schedule(PriorityQueueNode schedulerEntry, ScheduleMode scheduleMode) { + lock (ScheduledEntries) { + var nextCounter = SchedulerCounter++; + if (scheduleMode == ScheduleMode.First) { + nextCounter = -nextCounter; + } + + schedulerEntry.Value.SchedulerCounter = nextCounter; + + ScheduledEntries.Enqueue(schedulerEntry); + } + } + + internal void Unschedule(PriorityQueueNode schedulerEntry) { + lock (ScheduledEntries) { + if (schedulerEntry.Index != -1) { + ScheduledEntries.Remove(schedulerEntry); + } + } + } + + // TODO: Currently kept as a struct, but maybe a class would make more sense? + // Ideally it should be merged with PriorityQueueNode so that we need to allocate only one object? +} diff --git a/Rin.Core.MicroThreading/SchedulerEntry.cs b/Rin.Core.MicroThreading/SchedulerEntry.cs new file mode 100644 index 0000000..4a0092f --- /dev/null +++ b/Rin.Core.MicroThreading/SchedulerEntry.cs @@ -0,0 +1,29 @@ +using Rin.Diagnostics; + +namespace Rin.Core.MicroThreading; + +/// +/// Either a MicroThread or an action with priority. +/// +struct SchedulerEntry : IComparable { + public readonly Action Action; + public readonly MicroThread? MicroThread; + public long Priority; + public long SchedulerCounter; + public object Token; + public ProfilingKey? ProfilingKey; + + public SchedulerEntry(MicroThread microThread) : this() { + MicroThread = microThread; + } + + public SchedulerEntry(Action action, long priority) : this() { + Action = action; + Priority = priority; + } + + public int CompareTo(SchedulerEntry other) { + var priorityDiff = Priority.CompareTo(other.Priority); + return priorityDiff != 0 ? priorityDiff : SchedulerCounter.CompareTo(other.SchedulerCounter); + } +} diff --git a/Rin.Core.MicroThreading/SchedulerThreadEventArgs.cs b/Rin.Core.MicroThreading/SchedulerThreadEventArgs.cs new file mode 100644 index 0000000..790500f --- /dev/null +++ b/Rin.Core.MicroThreading/SchedulerThreadEventArgs.cs @@ -0,0 +1,33 @@ +namespace Rin.Core.MicroThreading; + +/// +/// Provides data for the , , +/// and events. +/// +public class SchedulerThreadEventArgs : EventArgs { + /// + /// Gets the this event concerns. + /// + /// + /// The micro thread. + /// + public MicroThread MicroThread { get; private set; } + + /// + /// Gets the active when this event happened. + /// + /// + /// The managed thread identifier. + /// + public int ThreadId { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The micro thread. + /// The managed thread identifier. + public SchedulerThreadEventArgs(MicroThread microThread, int threadId) { + MicroThread = microThread; + ThreadId = threadId; + } +} diff --git a/Rin.Core.Serialization/Rin.Core.Serialization.csproj b/Rin.Core.Serialization/Rin.Core.Serialization.csproj index 9d130cd..9c75c3d 100644 --- a/Rin.Core.Serialization/Rin.Core.Serialization.csproj +++ b/Rin.Core.Serialization/Rin.Core.Serialization.csproj @@ -4,4 +4,7 @@ enable enable + + + diff --git a/Rin.Core.Serialization/Serialization/Contents/IContentIndexMap.cs b/Rin.Core.Serialization/Serialization/Contents/IContentIndexMap.cs new file mode 100644 index 0000000..d77a4be --- /dev/null +++ b/Rin.Core.Serialization/Serialization/Contents/IContentIndexMap.cs @@ -0,0 +1,12 @@ +using Rin.Core.Storage; + +namespace Rin.Core.Serialization.Serialization.Contents; + +public interface IContentIndexMap : IDisposable { + ObjectId this[string url] { get; set; } + + bool TryGetValue(string url, out ObjectId objectId); + bool Contains(string url); + IEnumerable> SearchValues(Func, bool> predicate); + IEnumerable> GetMergedIdMap(); +} diff --git a/Rin.Core.Serialization/Storage/DigestStream.cs b/Rin.Core.Serialization/Storage/DigestStream.cs new file mode 100644 index 0000000..fa3090b --- /dev/null +++ b/Rin.Core.Serialization/Storage/DigestStream.cs @@ -0,0 +1,33 @@ +using Rin.Core.Storage; + +namespace Rin.Core.Serialization.Storage; + +public class DigestStream : OdbStreamWriter { + ObjectIdBuilder builder; + + public override ObjectId CurrentHash => builder.ComputeHash(); + + public DigestStream(Stream stream) : base(stream, null) { } + + internal DigestStream(Stream stream, string temporaryName) : base(stream, temporaryName) { } + + public void Reset() { + Position = 0; + builder.Reset(); + } + + public override void WriteByte(byte value) { + builder.WriteByte(value); + stream.WriteByte(value); + } + + public override void Write(byte[] buffer, int offset, int count) { + builder.Write(buffer, offset, count); + stream.Write(buffer, offset, count); + } + + public override void Write(ReadOnlySpan buffer) { + builder.Write(buffer); + stream.Write(buffer); + } +} diff --git a/Rin.Core.Serialization/Storage/IOdbBackend.cs b/Rin.Core.Serialization/Storage/IOdbBackend.cs new file mode 100644 index 0000000..47c0d1a --- /dev/null +++ b/Rin.Core.Serialization/Storage/IOdbBackend.cs @@ -0,0 +1,91 @@ +using Rin.Core.IO; +using Rin.Core.Serialization.Serialization.Contents; +using Rin.Core.Storage; + +namespace Rin.Core.Serialization.Storage; + +/// +/// Base class for custom object database backends (ODB). +/// +public interface IOdbBackend : IDisposable { + /// + /// Gets the asset index map. + /// + /// + /// The asset index map. + /// + IContentIndexMap ContentIndexMap { get; } + + /// + /// Opens a of the object with the specified . + /// + /// The . + /// The mode. + /// The access. + /// The process share mode. + /// + /// A opened from the specified . + /// + Stream OpenStream( + ObjectId objectId, + VirtualFileMode mode = VirtualFileMode.Open, + VirtualFileAccess access = VirtualFileAccess.Read, + VirtualFileShare share = VirtualFileShare.Read + ); + + /// + /// Requests that this backend read an object's length (but not its contents). + /// + /// The . + /// The object size. + int GetSize(ObjectId objectId); + + /// + /// Writes an object to the backing store. + /// The backend may need to compute the object ID and return it to the caller. + /// + /// + /// The if already computed, or if not + /// determined yet. + /// + /// The data stream. + /// The data length. + /// + /// Set to true to force writing the datastream even if a content is already stored with the same + /// id. Default is false. + /// + /// The generated . + ObjectId Write(ObjectId objectId, Stream dataStream, int length, bool forceWrite = false); + + /// + /// Creates a stream that will be saved to database when closed and/or disposed. + /// + /// a stream writer that should be passed to in order to be stored in the database + OdbStreamWriter CreateStream(); + + /// + /// Determines weither the object with the specified exists. + /// + /// The to check existence for. + /// true if an object with the specified exists; otherwise, false. + bool Exists(ObjectId objectId); + + /// + /// Enumerates the object stored in this backend. + /// + /// + IEnumerable EnumerateObjects(); + + /// + /// Deletes the specified . + /// + /// The object id. + void Delete(ObjectId objectId); + + /// + /// Returns the file path corresponding to the given id (in the VFS domain), if appliable. + /// + /// The . + /// The file path. + string GetFilePath(ObjectId objectId); +} diff --git a/Rin.Core.Serialization/Storage/OdbStreamWriter.cs b/Rin.Core.Serialization/Storage/OdbStreamWriter.cs new file mode 100644 index 0000000..af54044 --- /dev/null +++ b/Rin.Core.Serialization/Storage/OdbStreamWriter.cs @@ -0,0 +1,41 @@ +using Rin.Core.Storage; + +namespace Rin.Core.Serialization.Storage; + +public abstract class OdbStreamWriter : Stream { + public Action Disposed; + + public string? TemporaryName; + protected readonly Stream stream; + readonly long initialPosition; + + public abstract ObjectId CurrentHash { get; } + public override bool CanRead => false; + public override bool CanSeek => true; + public override bool CanWrite => stream.CanWrite; + public override long Length => stream.Length - initialPosition; + + public override long Position { + get => stream.Position - initialPosition; + set => stream.Position = initialPosition + value; + } + + protected OdbStreamWriter(Stream stream, string? temporaryName) { + this.stream = stream; + initialPosition = stream.Position; + TemporaryName = temporaryName; + } + + public override void Flush() => stream.Flush(); + public override int Read(byte[] buffer, int offset, int count) => throw new InvalidOperationException(); + public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin); + public override void SetLength(long value) => throw new InvalidOperationException(); + + protected override void Dispose(bool disposing) { + // Force hash computation before stream is closed. + var hash = CurrentHash; + stream.Dispose(); + + Disposed?.Invoke(this); + } +} diff --git a/Rin.Core/Collections/PriorityNodeQueue.cs b/Rin.Core/Collections/PriorityNodeQueue.cs new file mode 100644 index 0000000..fd7363d --- /dev/null +++ b/Rin.Core/Collections/PriorityNodeQueue.cs @@ -0,0 +1,208 @@ +namespace Rin.Core.Collections; + +/// +/// Implements a priority queue of type T. +/// Elements may be added to the queue in any order, but when we pull +/// elements out of the queue, they will be returned in 'ascending' order. +/// Adding new elements into the queue may be done at any time, so this is +/// useful to implement a dynamically growing and shrinking queue. Both adding +/// an element and removing the first element are log(N) operations. +/// The queue is implemented using a priority-heap data structure. For more +/// details on this elegant and simple data structure see "Programming Pearls" +/// in our library. The tree is implemented atop a list, where 2N and 2N+1 are +/// the child nodes of node N. The tree is balanced and left-aligned so there +/// are no 'holes' in this list. +/// +/// Type T. +public class PriorityNodeQueue { + /// The List we use for implementation. + readonly List> items = new(); + + // Used for comparing and sorting elements. + readonly IComparer comparer; + + /// Returns the number of elements in the queue. + public int Count => items.Count; + + /// Returns true if the queue is empty. + /// Trying to call Peek() or Next() on an empty queue will throw an exception. + /// Check using Empty first before calling these methods. + public bool Empty => items.Count == 0; + + public PriorityNodeQueue(IComparer comparer) { + this.comparer = comparer; + } + + public PriorityNodeQueue() { + comparer = Comparer.Default; + } + + /// Clear all the elements from the priority queue + public void Clear() { + items.Clear(); + } + + /// + /// Removes the specified item. + /// + /// The item to remove. + public void Remove(PriorityQueueNode item) { + var index = item.Index; + if (index == -1) { + return; + } + + // The element to return is of course the first element in the array, + // or the root of the tree. However, this will leave a 'hole' there. We + // fill up this hole with the last element from the array. This will + // break the heap property. So we bubble the element downwards by swapping + // it with it's lower child until it reaches it's correct level. The lower + // child (one of the original elements with index 1 or 2) will now be at the + // head of the queue (root of the tree). + var nMax = items.Count - 1; + var itemMax = items[nMax]; + itemMax.Index = index; + var itemToRemove = items[index]; + itemToRemove.Index = -1; + items[index] = itemMax; + items.RemoveAt(nMax); // Move the last element to the top + + var p = index; + while (true) { + // c is the child we want to swap with. If there + // is no child at all, then the heap is balanced + var c = p * 2; + if (c >= nMax) { + break; + } + + // If the second child is smaller than the first, that's the one + // we want to swap with this parent. + if (c + 1 < nMax && comparer.Compare(items[c + 1].Value, items[c].Value) < 0) { + c++; + } + + // If the parent is already smaller than this smaller child, then + // we are done + if (comparer.Compare(items[p].Value, items[c].Value) <= 0) { + break; + } + + // Otherwise, swap parent and child, and follow down the parent + var tmp = items[p]; + var tmp2 = items[c]; + tmp.Index = c; + tmp2.Index = p; + items[p] = tmp2; + items[c] = tmp; + p = c; + } + + item.Index = -1; + } + + /// Add an element to the priority queue - O(log(n)) time operation. + /// The item to be added to the queue + /// A node representing the item. + public PriorityQueueNode Enqueue(T item) { + var result = new PriorityQueueNode(item); + Enqueue(result); + return result; + } + + /// Add an element to the priority queue - O(log(n)) time operation. + /// The item to be added to the queue + public void Enqueue(PriorityQueueNode item) { + if (item.Index != -1) { + throw new InvalidOperationException("Item belongs to another PriorityNodeQueue."); + } + + // We add the item to the end of the list (at the bottom of the + // tree). Then, the heap-property could be violated between this element + // and it's parent. If this is the case, we swap this element with the + // parent (a safe operation to do since the element is known to be less + // than it's parent). Now the element move one level up the tree. We repeat + // this test with the element and it's new parent. The element, if lesser + // than everybody else in the tree will eventually bubble all the way up + // to the root of the tree (or the head of the list). It is easy to see + // this will take log(N) time, since we are working with a balanced binary + // tree. + var n = items.Count; + items.Add(item); + item.Index = n; + while (n != 0) { + var p = n / 2; // This is the 'parent' of this item + if (comparer.Compare(items[n].Value, items[p].Value) >= 0) { + break; // Item >= parent + } + + // Swap item and parent + var tmp = items[n]; + var tmp2 = items[p]; + tmp2.Index = n; + tmp.Index = p; + items[n] = tmp2; + items[p] = tmp; + + n = p; // And continue + } + } + + /// Allows you to look at the first element waiting in the queue, without removing it. + /// This element will be the one that will be returned if you subsequently call Next(). + public T Peek() => items[0].Value; + + /// Removes and returns the first element from the queue (least element) + /// The first element in the queue, in ascending order. + public T Dequeue() { + // The element to return is of course the first element in the array, + // or the root of the tree. However, this will leave a 'hole' there. We + // fill up this hole with the last element from the array. This will + // break the heap property. So we bubble the element downwards by swapping + // it with it's lower child until it reaches it's correct level. The lower + // child (one of the original elements with index 1 or 2) will now be at the + // head of the queue (root of the tree). + var nMax = items.Count - 1; + var itemToRemove = items[0]; + var itemMax = items[nMax]; + itemMax.Index = 0; + var val = itemToRemove.Value; + itemToRemove.Index = -1; + items[0] = itemMax; + items.RemoveAt(nMax); // Move the last element to the top + + var p = 0; + while (true) { + // c is the child we want to swap with. If there + // is no child at all, then the heap is balanced + var c = p * 2; + if (c >= nMax) { + break; + } + + // If the second child is smaller than the first, that's the one + // we want to swap with this parent. + if (c + 1 < nMax && comparer.Compare(items[c + 1].Value, items[c].Value) < 0) { + c++; + } + + // If the parent is already smaller than this smaller child, then + // we are done + if (comparer.Compare(items[p].Value, items[c].Value) <= 0) { + break; + } + + // Otherwise, swap parent and child, and follow down the parent + var tmp = items[p]; + var tmp2 = items[c]; + tmp.Index = c; + tmp2.Index = p; + items[p] = tmp2; + items[c] = tmp; + + p = c; + } + + return val; + } +} diff --git a/Rin.Core/Collections/PriorityQueueNode.cs b/Rin.Core/Collections/PriorityQueueNode.cs new file mode 100644 index 0000000..a05e2fe --- /dev/null +++ b/Rin.Core/Collections/PriorityQueueNode.cs @@ -0,0 +1,15 @@ +namespace Rin.Core.Collections; + +/// +/// Represents a node in a priority queue, to allow O(n) removal. +/// +/// +public class PriorityQueueNode { + public T Value; + public int Index { get; internal set; } + + public PriorityQueueNode(T value) { + Value = value; + Index = -1; + } +} diff --git a/Rin.Core/Collections/SortedList.cs b/Rin.Core/Collections/SortedList.cs new file mode 100644 index 0000000..9a501a9 --- /dev/null +++ b/Rin.Core/Collections/SortedList.cs @@ -0,0 +1,1056 @@ +using System.Collections; +using System.Diagnostics; + +namespace Rin.Core.Collections; + +/// +/// Represents a collection of associated keys and values +/// that are sorted by the keys and are accessible by key +/// and by index. +/// +[DebuggerDisplay("Count = {" + nameof(Count) + "}")] +public class SortedList : IDictionary, IDictionary { + static readonly int INITIAL_SIZE = 16; + + int modificationCount; + KeyValuePair[] table; + int defaultCapacity; + + public int Count { get; private set; } + + public int Capacity { + get => table.Length; + + set { + var current = table.Length; + + if (Count > value) { + throw new ArgumentOutOfRangeException("capacity too small"); + } + + if (value == 0) { + // return to default size + var newTable = new KeyValuePair[defaultCapacity]; + Array.Copy(table, newTable, Count); + table = newTable; + } else if (value > Count) { + var newTable = new KeyValuePair[value]; + Array.Copy(table, newTable, Count); + table = newTable; + } else if (value > current) { + var newTable = new KeyValuePair[value]; + Array.Copy(table, newTable, current); + table = newTable; + } + } + } + + public IList Keys => new ListKeys(this); + public IList Values => new ListValues(this); + public IComparer Comparer { get; private set; } + bool ICollection.IsSynchronized => false; + object ICollection.SyncRoot => this; + + // IDictionary + + bool IDictionary.IsFixedSize => false; + bool IDictionary.IsReadOnly => false; + ICollection IDictionary.Keys => new ListKeys(this); + ICollection IDictionary.Values => new ListValues(this); + ICollection IDictionary.Keys => Keys; + ICollection IDictionary.Values => Values; + bool ICollection>.IsReadOnly => false; + + public TValue this[TKey key] { + get { + if (key == null) { + throw new ArgumentNullException(nameof(key)); + } + + var i = Find(key); + + if (i >= 0) { + return table[i].Value; + } + + throw new KeyNotFoundException(); + } + set { + if (key == null) { + throw new ArgumentNullException(nameof(key)); + } + + PutImpl(key, value, true); + } + } + + object IDictionary.this[object key] { + get { + if (key is not TKey key1) { + return null; + } + + return this[key1]; + } + + set => this[ToKey(key)] = ToValue(value); + } + + // + // Constructors + // + public SortedList() + : this(INITIAL_SIZE, null) { } + + public SortedList(int capacity) + : this(capacity, null) { } + + public SortedList(int capacity, IComparer comparer) { + if (capacity < 0) { + throw new ArgumentOutOfRangeException(nameof(capacity)); + } + + defaultCapacity = capacity == 0 ? 0 : INITIAL_SIZE; + Init(comparer, capacity, true); + } + + public SortedList(IComparer comparer) + : this(INITIAL_SIZE, comparer) { } + + public SortedList(IDictionary dictionary) + : this(dictionary, null) { } + + public SortedList(IDictionary dictionary, IComparer comparer) { + if (dictionary == null) { + throw new ArgumentNullException(nameof(dictionary)); + } + + Init(comparer, dictionary.Count, true); + + foreach (var kvp in dictionary) { + Add(kvp.Key, kvp.Value); + } + } + + // + // Public instance methods. + // + + public void Add(TKey key, TValue value) { + if (key == null) { + throw new ArgumentNullException(nameof(key)); + } + + PutImpl(key, value, false); + } + + public bool ContainsKey(TKey key) { + if (key == null) { + throw new ArgumentNullException(nameof(key)); + } + + return Find(key) >= 0; + } + + public bool Remove(TKey key) { + if (key == null) { + throw new ArgumentNullException(nameof(key)); + } + + var i = IndexOfKey(key); + if (i >= 0) { + RemoveAt(i); + return true; + } + + return false; + } + + public void Clear() { + defaultCapacity = INITIAL_SIZE; + table = new KeyValuePair[defaultCapacity]; + Count = 0; + modificationCount++; + } + + // IEnumerable> + + public Enumerator GetEnumerator() => new(this); + + // + // SortedList + // + + public void RemoveAt(int index) { + var table = this.table; + var cnt = Count; + if (index >= 0 && index < cnt) { + if (index != cnt - 1) { + Array.Copy(table, index + 1, table, index, cnt - 1 - index); + } else { + table[index] = default; + } + + --Count; + ++modificationCount; + } else { + throw new ArgumentOutOfRangeException("index out of range"); + } + } + + public int IndexOfKey(TKey key) { + if (key == null) { + throw new ArgumentNullException(nameof(key)); + } + + var indx = 0; + try { + indx = Find(key); + } catch (Exception) { + throw new InvalidOperationException(); + } + + return indx | (indx >> 31); + } + + public int IndexOfValue(TValue value) { + if (Count == 0) { + return -1; + } + + for (var i = 0; i < Count; i++) { + var current = table[i]; + + if (Equals(value, current.Value)) { + return i; + } + } + + return -1; + } + + public bool ContainsValue(TValue value) => IndexOfValue(value) >= 0; + + public void TrimExcess() { + if (Count < table.Length * 0.9) { + Capacity = Count; + } + } + + public bool TryGetValue(TKey key, out TValue value) { + if (key == null) { + throw new ArgumentNullException(nameof(key)); + } + + var i = Find(key); + + if (i >= 0) { + value = table[i].Value; + return true; + } + + value = default; + return false; + } + + // ICollection> + + void ICollection>.Clear() { + defaultCapacity = INITIAL_SIZE; + table = new KeyValuePair[defaultCapacity]; + Count = 0; + modificationCount++; + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { + if (Count == 0) { + return; + } + + if (null == array) { + throw new ArgumentNullException(); + } + + if (arrayIndex < 0) { + throw new ArgumentOutOfRangeException(); + } + + if (arrayIndex >= array.Length) { + throw new ArgumentNullException("arrayIndex is greater than or equal to array.Length"); + } + + if (Count > array.Length - arrayIndex) { + throw new ArgumentNullException("Not enough space in array from arrayIndex to end of array"); + } + + var i = arrayIndex; + foreach (var pair in this) { + array[i++] = pair; + } + } + + void ICollection>.Add(KeyValuePair keyValuePair) { + Add(keyValuePair.Key, keyValuePair.Value); + } + + bool ICollection>.Contains(KeyValuePair keyValuePair) { + var i = Find(keyValuePair.Key); + + if (i >= 0) { + return Comparer>.Default.Compare(table[i], keyValuePair) == 0; + } + + return false; + } + + bool ICollection>.Remove(KeyValuePair keyValuePair) { + var i = Find(keyValuePair.Key); + + if (i >= 0 && Comparer>.Default.Compare(table[i], keyValuePair) == 0) { + RemoveAt(i); + return true; + } + + return false; + } + + IEnumerator> IEnumerable>.GetEnumerator() => + new Enumerator(this); + + // IEnumerable + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + // IDictionary + + void IDictionary.Add(object key, object value) { + PutImpl(ToKey(key), ToValue(value), false); + } + + bool IDictionary.Contains(object key) { + if (null == key) { + throw new ArgumentNullException(); + } + + if (!(key is TKey)) { + return false; + } + + return Find((TKey)key) >= 0; + } + + IDictionaryEnumerator IDictionary.GetEnumerator() => new DictionaryEnumerator(this, EnumeratorMode.ENTRY_MODE); + + void IDictionary.Remove(object key) { + if (null == key) { + throw new ArgumentNullException(nameof(key)); + } + + if (!(key is TKey)) { + return; + } + + var i = IndexOfKey((TKey)key); + if (i >= 0) { + RemoveAt(i); + } + } + + // ICollection + + void ICollection.CopyTo(Array array, int arrayIndex) { + if (Count == 0) { + return; + } + + if (null == array) { + throw new ArgumentNullException(); + } + + if (arrayIndex < 0) { + throw new ArgumentOutOfRangeException(); + } + + if (array.Rank > 1) { + throw new ArgumentException("array is multi-dimensional"); + } + + if (arrayIndex >= array.Length) { + throw new ArgumentNullException("arrayIndex is greater than or equal to array.Length"); + } + + if (Count > array.Length - arrayIndex) { + throw new ArgumentNullException("Not enough space in array from arrayIndex to end of array"); + } + + IEnumerator> it = GetEnumerator(); + var i = arrayIndex; + + while (it.MoveNext()) { + array.SetValue(it.Current, i++); + } + } + + // + // Private methods + // + + void EnsureCapacity(int n, int free) { + var table = this.table; + KeyValuePair[] newTable = null; + var cap = Capacity; + var gap = free >= 0 && free < Count; + + if (n > cap) { + newTable = new KeyValuePair[n << 1]; + } + + if (newTable != null) { + if (gap) { + var copyLen = free; + if (copyLen > 0) { + Array.Copy(table, 0, newTable, 0, copyLen); + } + + copyLen = Count - free; + if (copyLen > 0) { + Array.Copy(table, free, newTable, free + 1, copyLen); + } + } else { + // Just a resizing, copy the entire table. + Array.Copy(table, newTable, Count); + } + + this.table = newTable; + } else if (gap) { + Array.Copy(table, free, table, free + 1, Count - free); + } + } + + void PutImpl(TKey key, TValue value, bool overwrite) { + if (key == null) { + throw new ArgumentNullException(nameof(key)); + } + + var table = this.table; + + var freeIndx = -1; + + try { + freeIndx = Find(key); + } catch (Exception) { + throw new InvalidOperationException(); + } + + if (freeIndx >= 0) { + if (!overwrite) { + throw new ArgumentException("element already exists"); + } + + table[freeIndx] = new(key, value); + ++modificationCount; + return; + } + + freeIndx = ~freeIndx; + + if (freeIndx > Capacity + 1) { + throw new("SortedList::internal error (" + key + ", " + value + ") at [" + freeIndx + "]"); + } + + + EnsureCapacity(Count + 1, freeIndx); + + table = this.table; + table[freeIndx] = new(key, value); + + ++Count; + ++modificationCount; + } + + void Init(IComparer comparer, int capacity, bool forceSize) { + if (comparer == null) { + comparer = Comparer.Default; + } + + Comparer = comparer; + if (!forceSize && capacity < defaultCapacity) { + capacity = defaultCapacity; + } + + table = new KeyValuePair[capacity]; + Count = 0; + modificationCount = 0; + } + + void CopyToArray( + Array arr, + int i, + EnumeratorMode mode + ) { + if (arr == null) { + throw new ArgumentNullException(nameof(arr)); + } + + if (i < 0 || i + Count > arr.Length) { + throw new ArgumentOutOfRangeException(nameof(i)); + } + + IEnumerator it = new DictionaryEnumerator(this, mode); + + while (it.MoveNext()) { + arr.SetValue(it.Current, i++); + } + } + + int Find(TKey key) { + var table = this.table; + var len = Count; + + if (len == 0) { + return ~0; + } + + var left = 0; + var right = len - 1; + + while (left <= right) { + var guess = (left + right) >> 1; + + var cmp = Comparer.Compare(table[guess].Key, key); + if (cmp == 0) { + return guess; + } + + if (cmp < 0) { + left = guess + 1; + } else { + right = guess - 1; + } + } + + return ~left; + } + + TKey ToKey(object key) { + if (key == null) { + throw new ArgumentNullException(nameof(key)); + } + + if (!(key is TKey)) { + throw new ArgumentException( + "The value \"" + + key + + "\" isn't of type \"" + + typeof(TKey) + + "\" and can't be used in this generic collection.", + nameof(key) + ); + } + + return (TKey)key; + } + + TValue ToValue(object value) { + if (!(value is TValue)) { + throw new ArgumentException( + "The value \"" + + value + + "\" isn't of type \"" + + typeof(TValue) + + "\" and can't be used in this generic collection.", + nameof(value) + ); + } + + return (TValue)value; + } + + enum EnumeratorMode { + KEY_MODE = 0, + VALUE_MODE, + ENTRY_MODE + } + + internal TKey KeyAt(int index) { + if (index >= 0 && index < Count) { + return table[index].Key; + } + + throw new ArgumentOutOfRangeException(nameof(index)); + } + + internal TValue ValueAt(int index) { + if (index >= 0 && index < Count) { + return table[index].Value; + } + + throw new ArgumentOutOfRangeException(nameof(index)); + } + + // + // Inner classes + // + + public sealed class Enumerator : IEnumerator> { + SortedList host; + int pos = -1; + + public KeyValuePair Current => host.table[pos]; + object IEnumerator.Current => Current; + + public Enumerator(SortedList host) { + this.host = host; + } + + public void Dispose() { + host = null; + } + + public bool MoveNext() => ++pos < host.Count; + + public void Reset() { + throw new NotSupportedException(); + } + } + + + sealed class DictionaryEnumerator : IDictionaryEnumerator, IEnumerator { + readonly SortedList host; + int stamp; + int pos; + int size; + readonly EnumeratorMode mode; + + object currentKey; + object currentValue; + + bool invalid; + + static readonly string xstr = "SortedList.Enumerator: snapshot out of sync."; + + public DictionaryEntry Entry { + get { + if (invalid || pos >= size || pos == -1) { + throw new InvalidOperationException(xstr); + } + + return new( + currentKey, + currentValue + ); + } + } + + public object Key { + get { + if (invalid || pos >= size || pos == -1) { + throw new InvalidOperationException(xstr); + } + + return currentKey; + } + } + + public object Value { + get { + if (invalid || pos >= size || pos == -1) { + throw new InvalidOperationException(xstr); + } + + return currentValue; + } + } + + public object Current { + get { + if (invalid || pos >= size || pos == -1) { + throw new InvalidOperationException(xstr); + } + + switch (mode) { + case EnumeratorMode.KEY_MODE: + return currentKey; + case EnumeratorMode.VALUE_MODE: + return currentValue; + case EnumeratorMode.ENTRY_MODE: + return Entry; + + default: + throw new NotSupportedException(mode + " is not a supported mode."); + } + } + } + + public DictionaryEnumerator(SortedList host, EnumeratorMode mode) { + this.host = host; + stamp = host.modificationCount; + size = host.Count; + this.mode = mode; + Reset(); + } + + public DictionaryEnumerator(SortedList host) + : this(host, EnumeratorMode.ENTRY_MODE) { } + + public void Reset() { + if (host.modificationCount != stamp || invalid) { + throw new InvalidOperationException(xstr); + } + + pos = -1; + currentKey = null; + currentValue = null; + } + + public bool MoveNext() { + if (host.modificationCount != stamp || invalid) { + throw new InvalidOperationException(xstr); + } + + var table = host.table; + + if (++pos < size) { + var entry = table[pos]; + + currentKey = entry.Key; + currentValue = entry.Value; + return true; + } + + currentKey = null; + currentValue = null; + return false; + } + + // ICloneable + + public object Clone() => + new DictionaryEnumerator(host, mode) { + stamp = stamp, + pos = pos, + size = size, + currentKey = currentKey, + currentValue = currentValue, + invalid = invalid + }; + } + + struct KeyEnumerator : IEnumerator, IDisposable { + const int NOT_STARTED = -2; + + // this MUST be -1, because we depend on it in move next. + // we just decr the size, so, 0 - 1 == FINISHED + const int FINISHED = -1; + + readonly SortedList l; + int idx; + readonly int ver; + + public TKey Current { + get { + if (idx < 0) { + throw new InvalidOperationException(); + } + + return l.KeyAt(l.Count - 1 - idx); + } + } + + object IEnumerator.Current => Current; + + internal KeyEnumerator(SortedList l) { + this.l = l; + idx = NOT_STARTED; + ver = l.modificationCount; + } + + public void Dispose() { + idx = NOT_STARTED; + } + + public bool MoveNext() { + if (ver != l.modificationCount) { + throw new InvalidOperationException("Collection was modified after the enumerator was instantiated."); + } + + if (idx == NOT_STARTED) { + idx = l.Count; + } + + return idx != FINISHED && --idx != FINISHED; + } + + void IEnumerator.Reset() { + if (ver != l.modificationCount) { + throw new InvalidOperationException("Collection was modified after the enumerator was instantiated."); + } + + idx = NOT_STARTED; + } + } + + struct ValueEnumerator : IEnumerator, IDisposable { + const int NOT_STARTED = -2; + + // this MUST be -1, because we depend on it in move next. + // we just decr the size, so, 0 - 1 == FINISHED + const int FINISHED = -1; + + readonly SortedList l; + int idx; + readonly int ver; + + public TValue Current { + get { + if (idx < 0) { + throw new InvalidOperationException(); + } + + return l.ValueAt(l.Count - 1 - idx); + } + } + + object IEnumerator.Current => Current; + + internal ValueEnumerator(SortedList l) { + this.l = l; + idx = NOT_STARTED; + ver = l.modificationCount; + } + + public void Dispose() { + idx = NOT_STARTED; + } + + public bool MoveNext() { + if (ver != l.modificationCount) { + throw new InvalidOperationException("Collection was modified after the enumerator was instantiated."); + } + + if (idx == NOT_STARTED) { + idx = l.Count; + } + + return idx != FINISHED && --idx != FINISHED; + } + + void IEnumerator.Reset() { + if (ver != l.modificationCount) { + throw new InvalidOperationException("Collection was modified after the enumerator was instantiated."); + } + + idx = NOT_STARTED; + } + } + + class ListKeys : IList, IReadOnlyList, ICollection, IEnumerable { + readonly SortedList host; + + // + // ICollection + // + + public virtual int Count => host.Count; + + public virtual bool IsSynchronized => ((ICollection)host).IsSynchronized; + + public virtual bool IsReadOnly => true; + + public virtual object SyncRoot => ((ICollection)host).SyncRoot; + + public virtual TKey this[int index] { + get => host.KeyAt(index); + set => throw new NotSupportedException("attempt to modify a key"); + } + + public ListKeys(SortedList host) { + if (host == null) { + throw new ArgumentNullException(); + } + + this.host = host; + } + + // ICollection + + public virtual void Add(TKey item) { + throw new NotSupportedException(); + } + + public virtual bool Remove(TKey key) => throw new NotSupportedException(); + + public virtual void Clear() { + throw new NotSupportedException(); + } + + public virtual void CopyTo(TKey[] array, int arrayIndex) { + if (host.Count == 0) { + return; + } + + if (array == null) { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0) { + throw new ArgumentOutOfRangeException(); + } + + if (arrayIndex >= array.Length) { + throw new ArgumentOutOfRangeException("arrayIndex is greater than or equal to array.Length"); + } + + if (Count > array.Length - arrayIndex) { + throw new ArgumentOutOfRangeException("Not enough space in array from arrayIndex to end of array"); + } + + var j = arrayIndex; + for (var i = 0; i < Count; ++i) { + array[j++] = host.KeyAt(i); + } + } + + public virtual bool Contains(TKey item) => host.IndexOfKey(item) > -1; + + // + // IList + // + public virtual int IndexOf(TKey item) => host.IndexOfKey(item); + + public virtual void Insert(int index, TKey item) { + throw new NotSupportedException(); + } + + public virtual void RemoveAt(int index) { + throw new NotSupportedException(); + } + + // + // IEnumerable + // + + public virtual IEnumerator GetEnumerator() => + /* We couldn't use yield as it does not support Reset () */ + new KeyEnumerator(host); + + public virtual void CopyTo(Array array, int arrayIndex) { + host.CopyToArray(array, arrayIndex, EnumeratorMode.KEY_MODE); + } + + // + // IEnumerable + // + + IEnumerator IEnumerable.GetEnumerator() { + for (var i = 0; i < host.Count; ++i) { + yield return host.KeyAt(i); + } + } + } + + class ListValues : IList, IReadOnlyList, ICollection, IEnumerable { + readonly SortedList host; + + // + // ICollection + // + + public virtual int Count => host.Count; + + public virtual bool IsSynchronized => ((ICollection)host).IsSynchronized; + + public virtual bool IsReadOnly => true; + + public virtual object SyncRoot => ((ICollection)host).SyncRoot; + + public virtual TValue this[int index] { + get => host.ValueAt(index); + set => throw new NotSupportedException("attempt to modify a key"); + } + + public ListValues(SortedList host) { + if (host == null) { + throw new ArgumentNullException(); + } + + this.host = host; + } + + // ICollection + + public virtual void Add(TValue item) { + throw new NotSupportedException(); + } + + public virtual bool Remove(TValue value) => throw new NotSupportedException(); + + public virtual void Clear() { + throw new NotSupportedException(); + } + + public virtual void CopyTo(TValue[] array, int arrayIndex) { + if (host.Count == 0) { + return; + } + + if (array == null) { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0) { + throw new ArgumentOutOfRangeException(); + } + + if (arrayIndex >= array.Length) { + throw new ArgumentOutOfRangeException("arrayIndex is greater than or equal to array.Length"); + } + + if (Count > array.Length - arrayIndex) { + throw new ArgumentOutOfRangeException("Not enough space in array from arrayIndex to end of array"); + } + + var j = arrayIndex; + for (var i = 0; i < Count; ++i) { + array[j++] = host.ValueAt(i); + } + } + + public virtual bool Contains(TValue item) => host.IndexOfValue(item) > -1; + + // + // IList + // + public virtual int IndexOf(TValue item) => host.IndexOfValue(item); + + public virtual void Insert(int index, TValue item) { + throw new NotSupportedException(); + } + + public virtual void RemoveAt(int index) { + throw new NotSupportedException(); + } + + // + // IEnumerable + // + + public virtual IEnumerator GetEnumerator() => + /* We couldn't use yield as it does not support Reset () */ + new ValueEnumerator(host); + + public virtual void CopyTo(Array array, int arrayIndex) { + host.CopyToArray(array, arrayIndex, EnumeratorMode.VALUE_MODE); + } + + // + // IEnumerable + // + + IEnumerator IEnumerable.GetEnumerator() { + for (var i = 0; i < host.Count; ++i) { + yield return host.ValueAt(i); + } + } + } +} // SortedList diff --git a/Rin.Core/IIdentifiable.cs b/Rin.Core/IIdentifiable.cs new file mode 100644 index 0000000..8c21b6b --- /dev/null +++ b/Rin.Core/IIdentifiable.cs @@ -0,0 +1,12 @@ +namespace Rin.Core; + +/// +/// Base interface for all identifiable instances. +/// +public interface IIdentifiable { + /// + /// Gets the id of this instance + /// + // [NonOverridable] + Guid Id { get; set; } +} diff --git a/Rin.Core/PropertyContainer.cs b/Rin.Core/PropertyContainer.cs index 8b56013..b3ea855 100644 --- a/Rin.Core/PropertyContainer.cs +++ b/Rin.Core/PropertyContainer.cs @@ -1,5 +1,6 @@ using Rin.Core; using Rin.Core.Serialization; +using Rin.Core.Serialization.Serializers; using System.Collections; using System.Reflection; diff --git a/Rin.Core/Rin.Core.csproj b/Rin.Core/Rin.Core.csproj index 3f6ceff..f536504 100644 --- a/Rin.Core/Rin.Core.csproj +++ b/Rin.Core/Rin.Core.csproj @@ -19,4 +19,19 @@ + + + + TextTemplatingFileGenerator + MemberSerializerGenerated.cs + + + + + + True + True + MemberSerializerGenerated.tt + + diff --git a/Rin.Core/Serialization/DataSerializerGlobalAttribute.cs b/Rin.Core/Serialization/DataSerializerGlobalAttribute.cs new file mode 100644 index 0000000..3264420 --- /dev/null +++ b/Rin.Core/Serialization/DataSerializerGlobalAttribute.cs @@ -0,0 +1,29 @@ +namespace Rin.Core.Serialization; + +/// +/// Declares a serializer like or , but +/// externally. +/// +[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = true)] +public class DataSerializerGlobalAttribute : Attribute { + public string Profile { get; set; } + + /// + /// Initializes a new instance of the class, + /// either by its serializer type (it will act like and guess data type from the + /// generic type of ) or the data type (it will act just like if + /// was set on the data type). + /// + /// The serializer type. Can be null if if set. + /// The data type. Can be null if is set. + /// Defines how generic type are added to . + /// Similar to + /// True if it should use the auto-generated serializer. + public DataSerializerGlobalAttribute( + Type serializerType, + Type? dataType = null, + DataSerializerGenericMode mode = DataSerializerGenericMode.None, + bool inherited = false, + bool complexSerializer = false + ) { } +} diff --git a/Rin.Core/Serialization/IDataSerializerGenericInstantiation.cs b/Rin.Core/Serialization/IDataSerializerGenericInstantiation.cs new file mode 100644 index 0000000..a7f0f33 --- /dev/null +++ b/Rin.Core/Serialization/IDataSerializerGenericInstantiation.cs @@ -0,0 +1,18 @@ +namespace Rin.Core.Serialization; + +/// +/// Allows enumeration of required data serializers. +/// +public interface IDataSerializerGenericInstantiation { + /// + /// Enumerates required required by this instance of DataSerializer. + /// + /// + /// The code won't be executed, it will only be scanned for typeof() operands by the assembly processor. + /// Null is authorized in enumeration (for now). + /// + /// + /// + /// + void EnumerateGenericInstantiations(SerializerSelector serializerSelector, IList genericInstantiations); +} diff --git a/Rin.Core/Serialization/MemberSerializer.cs b/Rin.Core/Serialization/MemberSerializer.cs new file mode 100644 index 0000000..b4632d6 --- /dev/null +++ b/Rin.Core/Serialization/MemberSerializer.cs @@ -0,0 +1,96 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Rin.Core.Serialization; + +public class MemberSerializer { + public static readonly Dictionary CachedTypes = new(); + public static readonly Dictionary ReverseCachedTypes = new(); + + // Holds object references during serialization, useful when same object is referenced multiple time in same serialization graph. + public static PropertyKey> ObjectSerializeReferences = new( + "ObjectSerializeReferences", + typeof(SerializerExtensions), + DefaultValueMetadata.Delegate( + delegate { + return new Dictionary(ObjectReferenceEqualityComparer.Default); + } + ) + ); + + public static PropertyKey> ExternalIdentifiables = new( + "ExternalIdentifiables", + typeof(SerializerExtensions), + DefaultValueMetadata.Delegate( + delegate { + return new Dictionary(); + } + ) + ); + + public static PropertyKey> ObjectDeserializeReferences = new( + "ObjectDeserializeReferences", + typeof(SerializerExtensions), + DefaultValueMetadata.Delegate( + delegate { + return new List(); + } + ) + ); + + public static PropertyKey> ObjectDeserializeCallback = new( + "ObjectDeserializeCallback", + typeof(SerializerExtensions) + ); + + /// + /// Implements an equality comparer based on object reference instead of . + /// + public class ObjectReferenceEqualityComparer : EqualityComparer { + static IEqualityComparer? defaultEqualityComparer; + + public new static IEqualityComparer Default => + defaultEqualityComparer ??= new ObjectReferenceEqualityComparer(); + + public override bool Equals(object? x, object? y) => ReferenceEquals(x, y); + + public override int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); + } +} + +/// +/// Helper for serializing members of a class. +/// +/// The type of member to serialize. +public abstract class MemberSerializer : DataSerializer { + protected static bool isValueType = typeof(T).GetTypeInfo().IsValueType; + + // For now we hardcode here that Type subtypes should be ignored, but this should probably be a DataSerializerAttribute flag? + protected static bool isSealed = typeof(T).GetTypeInfo().IsSealed || typeof(T) == typeof(Type); + + protected DataSerializer dataSerializer; + + protected MemberSerializer(DataSerializer dataSerializer) { + this.dataSerializer = dataSerializer; + } + + public static DataSerializer Create(SerializerSelector serializerSelector, bool nullable = true) { + var dataSerializer = serializerSelector.GetSerializer(); + + if (!isValueType) { + if (serializerSelector.ReuseReferences) { + dataSerializer = typeof(T) == typeof(object) + ? new MemberReuseSerializerObject(dataSerializer) + : new MemberReuseSerializer(dataSerializer); + } else if (!isSealed) { + dataSerializer = typeof(T) == typeof(object) + ? new MemberNonSealedSerializerObject(dataSerializer) + : new MemberNonSealedSerializer(dataSerializer); + } else if (nullable) { + dataSerializer = new MemberNullableSerializer(dataSerializer); + } + } + + return dataSerializer; + } +} diff --git a/Rin.Core/Serialization/MemberSerializerClass.ttinclude b/Rin.Core/Serialization/MemberSerializerClass.ttinclude new file mode 100644 index 0000000..f6b2137 --- /dev/null +++ b/Rin.Core/Serialization/MemberSerializerClass.ttinclude @@ -0,0 +1,43 @@ +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".cs" #> +public static unsafe class <#= className #> { + public static void SerializeExtended(SerializationStream stream, Type objType, ref object obj, ArchiveMode mode, DataSerializer dataSerializer = null) { +<# supportsNullDataSerializer = true; +supportsValueType = true; +supportsGenerics = false; #> +<#@ include file="MemberSerializerCore.ttinclude" #> + } +} + +public unsafe class <#= className #> : MemberSerializer { + public <#= className #>(DataSerializer dataSerializer) : base(dataSerializer) { } + + public override void Serialize(ref T obj, ArchiveMode mode, SerializationStream stream) { +<# supportsNullDataSerializer = false; +supportsValueType = false; +supportsGenerics = true; #> +<#@ include file="MemberSerializerCore.ttinclude" #> + } + + internal static void SerializeExtended(ref T obj, ArchiveMode mode, SerializationStream stream, DataSerializer dataSerializer = null) { +<# supportsNullDataSerializer = true; +supportsValueType = true; +supportsGenerics = true; #> +<#@ include file="MemberSerializerCore.ttinclude" #> + } +} + +public unsafe class <#= className #>Object : MemberSerializer { + public <#= className #>Object(DataSerializer dataSerializer) : base(dataSerializer) { } + + public override void Serialize(ref T obj, ArchiveMode mode, SerializationStream stream) { +<# supportsNullDataSerializer = false; +supportsValueType = true; +supportsGenerics = true; #> +<#@ include file="MemberSerializerCore.ttinclude" #> + } +} \ No newline at end of file diff --git a/Rin.Core/Serialization/MemberSerializerCore.ttinclude b/Rin.Core/Serialization/MemberSerializerCore.ttinclude new file mode 100644 index 0000000..2f35774 --- /dev/null +++ b/Rin.Core/Serialization/MemberSerializerCore.ttinclude @@ -0,0 +1,287 @@ +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".cs" #> +<# { + var typeExpr = supportsGenerics ? "typeof(T)" : "objType"; + var objCopyExpr = supportsGenerics ? "objCopy" : "obj"; + var serializerCastExpr = supportsGenerics ? "(DataSerializer)" : string.Empty; + var castExpr = supportsGenerics ? "(T)" : string.Empty; #> +<# if (supportsReuseReferences || supportsNonSealed || supportsNullDataSerializer) { #> + var context = stream.Context; +<# } #> + +<# if (supportsValueType) { #> +<# if (supportsGenerics) { #> + if (isValueType) { +<# } else { #> + if (objType.IsValueType) { +<# } #> +<# if (supportsNullDataSerializer) { #> + if (dataSerializer is null) { + dataSerializer = <#= serializerCastExpr #>context.SerializerSelector.GetSerializer(<#= typeExpr #>); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + <#= typeExpr #>.FullName); + } + } +<# } // supportsNullDataSerializer #> + + // Structure, no need to check for inheritance or null values. + dataSerializer.Serialize(ref obj, mode, stream); + return; + } +<# } #> + +<# if (supportsNonSealed || supportsReuseReferences) { #> + // Serialize either with dataSerializer if obj is really of type T, otherwise look for appropriate serializer. + SerializeClassFlags flags; +<# } #> +<# if (supportsReuseReferences) { #> + bool reuseReferences = context.SerializerSelector.ReuseReferences; + int index; +<# } #> +<# if (supportsExternalIdentifiableAsGuid) { #> + bool externalIdentifiableAsGuid = context.SerializerSelector.ExternalIdentifiableAsGuid; +<# } #> +<# if (supportsNonSealed) { #> + bool hasTypeInfo; +<# if (supportsGenerics) { #> + object objCopy; +<# } #> + DataSerializer objectDataSerializer; + Type type; +<# } // supportsNonSealed #> + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { +<# if (supportsNonSealed || supportsReuseReferences) { #> + flags = SerializeClassFlags.None; +<# } #> + +<# if (supportsExternalIdentifiableAsGuid) { #> + if (externalIdentifiableAsGuid) { + var externalIdentifiables = context.Get(MemberSerializer.ExternalIdentifiables); + if (obj is IIdentifiable identifiable && externalIdentifiables?.ContainsKey(identifiable.Id) == true) { + flags |= SerializeClassFlags.IsExternalIdentifiable; + stream.Write((byte)flags); + stream.Write(identifiable.Id); + return; + } + } +<# } #> + +<# if (supportsReuseReferences) { #> + index = -1; + if (reuseReferences) { + var objectReferences = context.Get(MemberSerializer.ObjectSerializeReferences); + if (objectReferences.TryGetValue(obj, out index)) { + // Already serialized, just write its contentRef index + flags |= SerializeClassFlags.IsReference; + stream.Write((byte)flags); + stream.Write(index); + return; + } + + // First time it is serialized, add it to objectReferences. + objectReferences.Add(obj, index = objectReferences.Count); + } +<# } #> + +<# if (supportsNonSealed) { #> + // If real type is not expected type, we need to store type info as well. + var expectedType = <#= typeExpr #>; + type = <# if (supportsGenerics) { #>isSealed<# } else { #>objType.IsSealed<# } #> ? expectedType : obj.GetType(); + hasTypeInfo = type != expectedType; + objectDataSerializer = null; + + if (hasTypeInfo) { + // Find matching serializer (always required w/ typeinfo, since type was different than expected) + objectDataSerializer = context.SerializerSelector.GetSerializer(type); + + if (objectDataSerializer is null) + throw new ArgumentException("No serializer available for type " + type.FullName); + + // Update expected type + type = objectDataSerializer.SerializationType; + + // Special case: serializer reports the actual serialized type, and it might actually match the expected type + // (i.e. there is a hidden inherited type, such as PropertyInfo/RuntimePropertyInfo). + // Let's detect it here. + if (type == expectedType) { + // Cancel type info + hasTypeInfo = false; + } else { + // Continue as expected with type info + flags |= SerializeClassFlags.IsTypeInfo; + } + } +<# } // supportsNonSealed #> + + // Serialize flags +<# if (supportsNonSealed || supportsReuseReferences) { #> + stream.Write((byte)flags); +<# } else { #> + stream.Write((byte)SerializeClassFlags.None); +<# } #> + +<# if (supportsReuseReferences) { #> + // Serialize object index (if required) + if (reuseReferences) { + stream.Write(index); + } + +<# } #> +<# if (supportsNonSealed) { #> + if (hasTypeInfo) { + // Serialize type info + stream.Serialize(ref objectDataSerializer.SerializationTypeId); + + // Serialize object +<# if (supportsGenerics) { #> + objCopy = obj; +<# } #> + objectDataSerializer.PreSerialize(ref <#= objCopyExpr #>, mode, stream); + objectDataSerializer.Serialize(ref <#= objCopyExpr #>, mode, stream); +<# if (supportsGenerics) { #> + obj = (T)objCopy; +<# } #> + } + else +<# } // supportsNonSealed #> + { +<# if (supportsNullDataSerializer) { #> + if (dataSerializer is null) + { + dataSerializer = <#= serializerCastExpr #>context.SerializerSelector.GetSerializer(<# if (supportsNonSealed) { #>expectedType<# } else { #><#= typeExpr #><# } #>); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) + throw new ArgumentException("No serializer available for type " + <# if (supportsNonSealed) { #>expectedType<# } else { #><#= typeExpr #><# } #>.FullName); + } + +<# } // supportsNullDataSerializer #> + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + +<# if (supportsNonSealed || supportsReuseReferences) { #> + flags = (SerializeClassFlags)stream.ReadByte(); + var isNull = 0 != (flags & SerializeClassFlags.IsNull); +<# } else { // optimized version where flags is only used for isNull: optimize locals #> + var isNull = 0 != ((SerializeClassFlags)stream.ReadByte() & SerializeClassFlags.IsNull); +<# } #> +<# if (supportsReuseReferences) { #> + var objectReferences = reuseReferences ? context.Get(MemberSerializer.ObjectDeserializeReferences) : null; + var referenceCallback = reuseReferences ? context.Get(MemberSerializer.ObjectDeserializeCallback) : null; + var isReference = 0 != (flags & SerializeClassFlags.IsReference); + var isExternalIdentifiable = 0 != (flags & SerializeClassFlags.IsExternalIdentifiable); + index = reuseReferences && !isNull && !isExternalIdentifiable ? stream.ReadInt32() : -1; +<# } #> +<# if (supportsNonSealed) { #> + hasTypeInfo = 0 != (flags & SerializeClassFlags.IsTypeInfo); +<# } // supportsNonSealed #> + + if (isNull) { + obj = <# if (supportsGenerics) { #>default(T)<# } else { #>null<# } #>; + } +<# if (supportsExternalIdentifiableAsGuid) { #> + else if (externalIdentifiableAsGuid && isExternalIdentifiable) { + var externalIdentifiables = context.Get(MemberSerializer.ExternalIdentifiables); + var identifier = stream.Read(); + IIdentifiable identifiable; + externalIdentifiables.TryGetValue(identifier, out identifiable); + obj = <#= castExpr #>identifiable; + } +<# } #> +<# if (supportsReuseReferences) { #> + else if (reuseReferences && isReference) { + obj = <#= castExpr #>objectReferences[index]; + } +<# } #> + else { +<# if (supportsReuseReferences) { #> + if (reuseReferences) + { + if (objectReferences.Count != index) + throw new InvalidOperationException("Serialization contentRef indices are out of sync."); + objectReferences.Add(null); + } +<# } #> +<# if (supportsNonSealed) { #> + if (hasTypeInfo) { + Unsafe.SkipInit(out ObjectId serializationTypeId); + stream.Serialize(ref serializationTypeId); + + objectDataSerializer = context.SerializerSelector.GetSerializer(ref serializationTypeId); + if (objectDataSerializer is null) { + throw new ArgumentException("No serializer available for type id " + serializationTypeId + " and base type " + <#= typeExpr #>.FullName); + } + +<# if (supportsGenerics) { #> + objCopy = obj; +<# } #> + objectDataSerializer.PreSerialize(ref <#= objCopyExpr #>, mode, stream); + +<# if (supportsReuseReferences) { #> + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = <#= objCopyExpr #>; + } +<# } #> + + objectDataSerializer.Serialize(ref <#= objCopyExpr #>, mode, stream); +<# if (supportsGenerics) { #> + obj = (T)objCopy; +<# } #> + } + else +<# } // supportsNonSealed #> + { +<# if (supportsNullDataSerializer) { #> + if (dataSerializer is null) { + dataSerializer = <#= serializerCastExpr #>context.SerializerSelector.GetSerializer(<#= typeExpr #>); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + <#= typeExpr #>.FullName); + } + } + +<# } // supportsNullDataSerializer #> + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + +<# if (supportsReuseReferences) { #> + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = obj; + } +<# } #> + + dataSerializer.Serialize(ref obj, mode, stream); + } + +<# if (supportsReuseReferences) { #> + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = obj; + + // Call Reference Callback + referenceCallback?.Invoke(index, obj); + } +<# } #> + } + } +<# } #> \ No newline at end of file diff --git a/Rin.Core/Serialization/MemberSerializerGenerated.cs b/Rin.Core/Serialization/MemberSerializerGenerated.cs new file mode 100644 index 0000000..c1e2ad4 --- /dev/null +++ b/Rin.Core/Serialization/MemberSerializerGenerated.cs @@ -0,0 +1,1567 @@ +// +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using Rin.Core.Storage; + +namespace Rin.Core.Serialization; + +public static unsafe class MemberNullableSerializer { + public static void SerializeExtended(SerializationStream stream, Type objType, ref object obj, ArchiveMode mode, DataSerializer dataSerializer = null) { + var context = stream.Context; + + if (objType.IsValueType) { + if (dataSerializer is null) { + dataSerializer = context.SerializerSelector.GetSerializer(objType); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + objType.FullName); + } + } + + // Structure, no need to check for inheritance or null values. + dataSerializer.Serialize(ref obj, mode, stream); + return; + } + + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { + + + + + // Serialize flags + stream.Write((byte)SerializeClassFlags.None); + + { + if (dataSerializer is null) + { + dataSerializer = context.SerializerSelector.GetSerializer(objType); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) + throw new ArgumentException("No serializer available for type " + objType.FullName); + } + + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + + var isNull = 0 != ((SerializeClassFlags)stream.ReadByte() & SerializeClassFlags.IsNull); + + if (isNull) { + obj = null; + } + else { + { + if (dataSerializer is null) { + dataSerializer = context.SerializerSelector.GetSerializer(objType); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + objType.FullName); + } + } + + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + + + dataSerializer.Serialize(ref obj, mode, stream); + } + + } + } + + } +} + +public unsafe class MemberNullableSerializer : MemberSerializer { + public MemberNullableSerializer(DataSerializer dataSerializer) : base(dataSerializer) { } + + public override void Serialize(ref T obj, ArchiveMode mode, SerializationStream stream) { + + + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { + + + + + // Serialize flags + stream.Write((byte)SerializeClassFlags.None); + + { + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + + var isNull = 0 != ((SerializeClassFlags)stream.ReadByte() & SerializeClassFlags.IsNull); + + if (isNull) { + obj = default(T); + } + else { + { + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + + + dataSerializer.Serialize(ref obj, mode, stream); + } + + } + } + + } + + internal static void SerializeExtended(ref T obj, ArchiveMode mode, SerializationStream stream, DataSerializer dataSerializer = null) { + var context = stream.Context; + + if (isValueType) { + if (dataSerializer is null) { + dataSerializer = (DataSerializer)context.SerializerSelector.GetSerializer(typeof(T)); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + typeof(T).FullName); + } + } + + // Structure, no need to check for inheritance or null values. + dataSerializer.Serialize(ref obj, mode, stream); + return; + } + + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { + + + + + // Serialize flags + stream.Write((byte)SerializeClassFlags.None); + + { + if (dataSerializer is null) + { + dataSerializer = (DataSerializer)context.SerializerSelector.GetSerializer(typeof(T)); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) + throw new ArgumentException("No serializer available for type " + typeof(T).FullName); + } + + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + + var isNull = 0 != ((SerializeClassFlags)stream.ReadByte() & SerializeClassFlags.IsNull); + + if (isNull) { + obj = default(T); + } + else { + { + if (dataSerializer is null) { + dataSerializer = (DataSerializer)context.SerializerSelector.GetSerializer(typeof(T)); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + typeof(T).FullName); + } + } + + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + + + dataSerializer.Serialize(ref obj, mode, stream); + } + + } + } + + } +} + +public unsafe class MemberNullableSerializerObject : MemberSerializer { + public MemberNullableSerializerObject(DataSerializer dataSerializer) : base(dataSerializer) { } + + public override void Serialize(ref T obj, ArchiveMode mode, SerializationStream stream) { + + if (isValueType) { + + // Structure, no need to check for inheritance or null values. + dataSerializer.Serialize(ref obj, mode, stream); + return; + } + + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { + + + + + // Serialize flags + stream.Write((byte)SerializeClassFlags.None); + + { + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + + var isNull = 0 != ((SerializeClassFlags)stream.ReadByte() & SerializeClassFlags.IsNull); + + if (isNull) { + obj = default(T); + } + else { + { + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + + + dataSerializer.Serialize(ref obj, mode, stream); + } + + } + } + + } +} + +public static unsafe class MemberNonSealedSerializer { + public static void SerializeExtended(SerializationStream stream, Type objType, ref object obj, ArchiveMode mode, DataSerializer dataSerializer = null) { + var context = stream.Context; + + if (objType.IsValueType) { + if (dataSerializer is null) { + dataSerializer = context.SerializerSelector.GetSerializer(objType); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + objType.FullName); + } + } + + // Structure, no need to check for inheritance or null values. + dataSerializer.Serialize(ref obj, mode, stream); + return; + } + + // Serialize either with dataSerializer if obj is really of type T, otherwise look for appropriate serializer. + SerializeClassFlags flags; + bool hasTypeInfo; + DataSerializer objectDataSerializer; + Type type; + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { + flags = SerializeClassFlags.None; + + + + // If real type is not expected type, we need to store type info as well. + var expectedType = objType; + type = objType.IsSealed ? expectedType : obj.GetType(); + hasTypeInfo = type != expectedType; + objectDataSerializer = null; + + if (hasTypeInfo) { + // Find matching serializer (always required w/ typeinfo, since type was different than expected) + objectDataSerializer = context.SerializerSelector.GetSerializer(type); + + if (objectDataSerializer is null) + throw new ArgumentException("No serializer available for type " + type.FullName); + + // Update expected type + type = objectDataSerializer.SerializationType; + + // Special case: serializer reports the actual serialized type, and it might actually match the expected type + // (i.e. there is a hidden inherited type, such as PropertyInfo/RuntimePropertyInfo). + // Let's detect it here. + if (type == expectedType) { + // Cancel type info + hasTypeInfo = false; + } else { + // Continue as expected with type info + flags |= SerializeClassFlags.IsTypeInfo; + } + } + + // Serialize flags + stream.Write((byte)flags); + + if (hasTypeInfo) { + // Serialize type info + stream.Serialize(ref objectDataSerializer.SerializationTypeId); + + // Serialize object + objectDataSerializer.PreSerialize(ref obj, mode, stream); + objectDataSerializer.Serialize(ref obj, mode, stream); + } + else + { + if (dataSerializer is null) + { + dataSerializer = context.SerializerSelector.GetSerializer(expectedType); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) + throw new ArgumentException("No serializer available for type " + expectedType.FullName); + } + + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + + flags = (SerializeClassFlags)stream.ReadByte(); + var isNull = 0 != (flags & SerializeClassFlags.IsNull); + hasTypeInfo = 0 != (flags & SerializeClassFlags.IsTypeInfo); + + if (isNull) { + obj = null; + } + else { + if (hasTypeInfo) { + Unsafe.SkipInit(out ObjectId serializationTypeId); + stream.Serialize(ref serializationTypeId); + + objectDataSerializer = context.SerializerSelector.GetSerializer(ref serializationTypeId); + if (objectDataSerializer is null) { + throw new ArgumentException("No serializer available for type id " + serializationTypeId + " and base type " + objType.FullName); + } + + objectDataSerializer.PreSerialize(ref obj, mode, stream); + + + objectDataSerializer.Serialize(ref obj, mode, stream); + } + else + { + if (dataSerializer is null) { + dataSerializer = context.SerializerSelector.GetSerializer(objType); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + objType.FullName); + } + } + + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + + + dataSerializer.Serialize(ref obj, mode, stream); + } + + } + } + + } +} + +public unsafe class MemberNonSealedSerializer : MemberSerializer { + public MemberNonSealedSerializer(DataSerializer dataSerializer) : base(dataSerializer) { } + + public override void Serialize(ref T obj, ArchiveMode mode, SerializationStream stream) { + var context = stream.Context; + + + // Serialize either with dataSerializer if obj is really of type T, otherwise look for appropriate serializer. + SerializeClassFlags flags; + bool hasTypeInfo; + object objCopy; + DataSerializer objectDataSerializer; + Type type; + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { + flags = SerializeClassFlags.None; + + + + // If real type is not expected type, we need to store type info as well. + var expectedType = typeof(T); + type = isSealed ? expectedType : obj.GetType(); + hasTypeInfo = type != expectedType; + objectDataSerializer = null; + + if (hasTypeInfo) { + // Find matching serializer (always required w/ typeinfo, since type was different than expected) + objectDataSerializer = context.SerializerSelector.GetSerializer(type); + + if (objectDataSerializer is null) + throw new ArgumentException("No serializer available for type " + type.FullName); + + // Update expected type + type = objectDataSerializer.SerializationType; + + // Special case: serializer reports the actual serialized type, and it might actually match the expected type + // (i.e. there is a hidden inherited type, such as PropertyInfo/RuntimePropertyInfo). + // Let's detect it here. + if (type == expectedType) { + // Cancel type info + hasTypeInfo = false; + } else { + // Continue as expected with type info + flags |= SerializeClassFlags.IsTypeInfo; + } + } + + // Serialize flags + stream.Write((byte)flags); + + if (hasTypeInfo) { + // Serialize type info + stream.Serialize(ref objectDataSerializer.SerializationTypeId); + + // Serialize object + objCopy = obj; + objectDataSerializer.PreSerialize(ref objCopy, mode, stream); + objectDataSerializer.Serialize(ref objCopy, mode, stream); + obj = (T)objCopy; + } + else + { + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + + flags = (SerializeClassFlags)stream.ReadByte(); + var isNull = 0 != (flags & SerializeClassFlags.IsNull); + hasTypeInfo = 0 != (flags & SerializeClassFlags.IsTypeInfo); + + if (isNull) { + obj = default(T); + } + else { + if (hasTypeInfo) { + Unsafe.SkipInit(out ObjectId serializationTypeId); + stream.Serialize(ref serializationTypeId); + + objectDataSerializer = context.SerializerSelector.GetSerializer(ref serializationTypeId); + if (objectDataSerializer is null) { + throw new ArgumentException("No serializer available for type id " + serializationTypeId + " and base type " + typeof(T).FullName); + } + + objCopy = obj; + objectDataSerializer.PreSerialize(ref objCopy, mode, stream); + + + objectDataSerializer.Serialize(ref objCopy, mode, stream); + obj = (T)objCopy; + } + else + { + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + + + dataSerializer.Serialize(ref obj, mode, stream); + } + + } + } + + } + + internal static void SerializeExtended(ref T obj, ArchiveMode mode, SerializationStream stream, DataSerializer dataSerializer = null) { + var context = stream.Context; + + if (isValueType) { + if (dataSerializer is null) { + dataSerializer = (DataSerializer)context.SerializerSelector.GetSerializer(typeof(T)); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + typeof(T).FullName); + } + } + + // Structure, no need to check for inheritance or null values. + dataSerializer.Serialize(ref obj, mode, stream); + return; + } + + // Serialize either with dataSerializer if obj is really of type T, otherwise look for appropriate serializer. + SerializeClassFlags flags; + bool hasTypeInfo; + object objCopy; + DataSerializer objectDataSerializer; + Type type; + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { + flags = SerializeClassFlags.None; + + + + // If real type is not expected type, we need to store type info as well. + var expectedType = typeof(T); + type = isSealed ? expectedType : obj.GetType(); + hasTypeInfo = type != expectedType; + objectDataSerializer = null; + + if (hasTypeInfo) { + // Find matching serializer (always required w/ typeinfo, since type was different than expected) + objectDataSerializer = context.SerializerSelector.GetSerializer(type); + + if (objectDataSerializer is null) + throw new ArgumentException("No serializer available for type " + type.FullName); + + // Update expected type + type = objectDataSerializer.SerializationType; + + // Special case: serializer reports the actual serialized type, and it might actually match the expected type + // (i.e. there is a hidden inherited type, such as PropertyInfo/RuntimePropertyInfo). + // Let's detect it here. + if (type == expectedType) { + // Cancel type info + hasTypeInfo = false; + } else { + // Continue as expected with type info + flags |= SerializeClassFlags.IsTypeInfo; + } + } + + // Serialize flags + stream.Write((byte)flags); + + if (hasTypeInfo) { + // Serialize type info + stream.Serialize(ref objectDataSerializer.SerializationTypeId); + + // Serialize object + objCopy = obj; + objectDataSerializer.PreSerialize(ref objCopy, mode, stream); + objectDataSerializer.Serialize(ref objCopy, mode, stream); + obj = (T)objCopy; + } + else + { + if (dataSerializer is null) + { + dataSerializer = (DataSerializer)context.SerializerSelector.GetSerializer(expectedType); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) + throw new ArgumentException("No serializer available for type " + expectedType.FullName); + } + + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + + flags = (SerializeClassFlags)stream.ReadByte(); + var isNull = 0 != (flags & SerializeClassFlags.IsNull); + hasTypeInfo = 0 != (flags & SerializeClassFlags.IsTypeInfo); + + if (isNull) { + obj = default(T); + } + else { + if (hasTypeInfo) { + Unsafe.SkipInit(out ObjectId serializationTypeId); + stream.Serialize(ref serializationTypeId); + + objectDataSerializer = context.SerializerSelector.GetSerializer(ref serializationTypeId); + if (objectDataSerializer is null) { + throw new ArgumentException("No serializer available for type id " + serializationTypeId + " and base type " + typeof(T).FullName); + } + + objCopy = obj; + objectDataSerializer.PreSerialize(ref objCopy, mode, stream); + + + objectDataSerializer.Serialize(ref objCopy, mode, stream); + obj = (T)objCopy; + } + else + { + if (dataSerializer is null) { + dataSerializer = (DataSerializer)context.SerializerSelector.GetSerializer(typeof(T)); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + typeof(T).FullName); + } + } + + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + + + dataSerializer.Serialize(ref obj, mode, stream); + } + + } + } + + } +} + +public unsafe class MemberNonSealedSerializerObject : MemberSerializer { + public MemberNonSealedSerializerObject(DataSerializer dataSerializer) : base(dataSerializer) { } + + public override void Serialize(ref T obj, ArchiveMode mode, SerializationStream stream) { + var context = stream.Context; + + if (isValueType) { + + // Structure, no need to check for inheritance or null values. + dataSerializer.Serialize(ref obj, mode, stream); + return; + } + + // Serialize either with dataSerializer if obj is really of type T, otherwise look for appropriate serializer. + SerializeClassFlags flags; + bool hasTypeInfo; + object objCopy; + DataSerializer objectDataSerializer; + Type type; + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { + flags = SerializeClassFlags.None; + + + + // If real type is not expected type, we need to store type info as well. + var expectedType = typeof(T); + type = isSealed ? expectedType : obj.GetType(); + hasTypeInfo = type != expectedType; + objectDataSerializer = null; + + if (hasTypeInfo) { + // Find matching serializer (always required w/ typeinfo, since type was different than expected) + objectDataSerializer = context.SerializerSelector.GetSerializer(type); + + if (objectDataSerializer is null) + throw new ArgumentException("No serializer available for type " + type.FullName); + + // Update expected type + type = objectDataSerializer.SerializationType; + + // Special case: serializer reports the actual serialized type, and it might actually match the expected type + // (i.e. there is a hidden inherited type, such as PropertyInfo/RuntimePropertyInfo). + // Let's detect it here. + if (type == expectedType) { + // Cancel type info + hasTypeInfo = false; + } else { + // Continue as expected with type info + flags |= SerializeClassFlags.IsTypeInfo; + } + } + + // Serialize flags + stream.Write((byte)flags); + + if (hasTypeInfo) { + // Serialize type info + stream.Serialize(ref objectDataSerializer.SerializationTypeId); + + // Serialize object + objCopy = obj; + objectDataSerializer.PreSerialize(ref objCopy, mode, stream); + objectDataSerializer.Serialize(ref objCopy, mode, stream); + obj = (T)objCopy; + } + else + { + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + + flags = (SerializeClassFlags)stream.ReadByte(); + var isNull = 0 != (flags & SerializeClassFlags.IsNull); + hasTypeInfo = 0 != (flags & SerializeClassFlags.IsTypeInfo); + + if (isNull) { + obj = default(T); + } + else { + if (hasTypeInfo) { + Unsafe.SkipInit(out ObjectId serializationTypeId); + stream.Serialize(ref serializationTypeId); + + objectDataSerializer = context.SerializerSelector.GetSerializer(ref serializationTypeId); + if (objectDataSerializer is null) { + throw new ArgumentException("No serializer available for type id " + serializationTypeId + " and base type " + typeof(T).FullName); + } + + objCopy = obj; + objectDataSerializer.PreSerialize(ref objCopy, mode, stream); + + + objectDataSerializer.Serialize(ref objCopy, mode, stream); + obj = (T)objCopy; + } + else + { + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + + + dataSerializer.Serialize(ref obj, mode, stream); + } + + } + } + + } +} + +public static unsafe class MemberReuseSerializer { + public static void SerializeExtended(SerializationStream stream, Type objType, ref object obj, ArchiveMode mode, DataSerializer dataSerializer = null) { + var context = stream.Context; + + if (objType.IsValueType) { + if (dataSerializer is null) { + dataSerializer = context.SerializerSelector.GetSerializer(objType); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + objType.FullName); + } + } + + // Structure, no need to check for inheritance or null values. + dataSerializer.Serialize(ref obj, mode, stream); + return; + } + + // Serialize either with dataSerializer if obj is really of type T, otherwise look for appropriate serializer. + SerializeClassFlags flags; + bool reuseReferences = context.SerializerSelector.ReuseReferences; + int index; + bool externalIdentifiableAsGuid = context.SerializerSelector.ExternalIdentifiableAsGuid; + bool hasTypeInfo; + DataSerializer objectDataSerializer; + Type type; + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { + flags = SerializeClassFlags.None; + + if (externalIdentifiableAsGuid) { + var externalIdentifiables = context.Get(MemberSerializer.ExternalIdentifiables); + if (obj is IIdentifiable identifiable && externalIdentifiables?.ContainsKey(identifiable.Id) == true) { + flags |= SerializeClassFlags.IsExternalIdentifiable; + stream.Write((byte)flags); + stream.Write(identifiable.Id); + return; + } + } + + index = -1; + if (reuseReferences) { + var objectReferences = context.Get(MemberSerializer.ObjectSerializeReferences); + if (objectReferences.TryGetValue(obj, out index)) { + // Already serialized, just write its contentRef index + flags |= SerializeClassFlags.IsReference; + stream.Write((byte)flags); + stream.Write(index); + return; + } + + // First time it is serialized, add it to objectReferences. + objectReferences.Add(obj, index = objectReferences.Count); + } + + // If real type is not expected type, we need to store type info as well. + var expectedType = objType; + type = objType.IsSealed ? expectedType : obj.GetType(); + hasTypeInfo = type != expectedType; + objectDataSerializer = null; + + if (hasTypeInfo) { + // Find matching serializer (always required w/ typeinfo, since type was different than expected) + objectDataSerializer = context.SerializerSelector.GetSerializer(type); + + if (objectDataSerializer is null) + throw new ArgumentException("No serializer available for type " + type.FullName); + + // Update expected type + type = objectDataSerializer.SerializationType; + + // Special case: serializer reports the actual serialized type, and it might actually match the expected type + // (i.e. there is a hidden inherited type, such as PropertyInfo/RuntimePropertyInfo). + // Let's detect it here. + if (type == expectedType) { + // Cancel type info + hasTypeInfo = false; + } else { + // Continue as expected with type info + flags |= SerializeClassFlags.IsTypeInfo; + } + } + + // Serialize flags + stream.Write((byte)flags); + + // Serialize object index (if required) + if (reuseReferences) { + stream.Write(index); + } + + if (hasTypeInfo) { + // Serialize type info + stream.Serialize(ref objectDataSerializer.SerializationTypeId); + + // Serialize object + objectDataSerializer.PreSerialize(ref obj, mode, stream); + objectDataSerializer.Serialize(ref obj, mode, stream); + } + else + { + if (dataSerializer is null) + { + dataSerializer = context.SerializerSelector.GetSerializer(expectedType); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) + throw new ArgumentException("No serializer available for type " + expectedType.FullName); + } + + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + + flags = (SerializeClassFlags)stream.ReadByte(); + var isNull = 0 != (flags & SerializeClassFlags.IsNull); + var objectReferences = reuseReferences ? context.Get(MemberSerializer.ObjectDeserializeReferences) : null; + var referenceCallback = reuseReferences ? context.Get(MemberSerializer.ObjectDeserializeCallback) : null; + var isReference = 0 != (flags & SerializeClassFlags.IsReference); + var isExternalIdentifiable = 0 != (flags & SerializeClassFlags.IsExternalIdentifiable); + index = reuseReferences && !isNull && !isExternalIdentifiable ? stream.ReadInt32() : -1; + hasTypeInfo = 0 != (flags & SerializeClassFlags.IsTypeInfo); + + if (isNull) { + obj = null; + } + else if (externalIdentifiableAsGuid && isExternalIdentifiable) { + var externalIdentifiables = context.Get(MemberSerializer.ExternalIdentifiables); + var identifier = stream.Read(); + IIdentifiable identifiable; + externalIdentifiables.TryGetValue(identifier, out identifiable); + obj = identifiable; + } + else if (reuseReferences && isReference) { + obj = objectReferences[index]; + } + else { + if (reuseReferences) + { + if (objectReferences.Count != index) + throw new InvalidOperationException("Serialization contentRef indices are out of sync."); + objectReferences.Add(null); + } + if (hasTypeInfo) { + Unsafe.SkipInit(out ObjectId serializationTypeId); + stream.Serialize(ref serializationTypeId); + + objectDataSerializer = context.SerializerSelector.GetSerializer(ref serializationTypeId); + if (objectDataSerializer is null) { + throw new ArgumentException("No serializer available for type id " + serializationTypeId + " and base type " + objType.FullName); + } + + objectDataSerializer.PreSerialize(ref obj, mode, stream); + + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = obj; + } + + objectDataSerializer.Serialize(ref obj, mode, stream); + } + else + { + if (dataSerializer is null) { + dataSerializer = context.SerializerSelector.GetSerializer(objType); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + objType.FullName); + } + } + + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = obj; + } + + dataSerializer.Serialize(ref obj, mode, stream); + } + + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = obj; + + // Call Reference Callback + referenceCallback?.Invoke(index, obj); + } + } + } + + } +} + +public unsafe class MemberReuseSerializer : MemberSerializer { + public MemberReuseSerializer(DataSerializer dataSerializer) : base(dataSerializer) { } + + public override void Serialize(ref T obj, ArchiveMode mode, SerializationStream stream) { + var context = stream.Context; + + + // Serialize either with dataSerializer if obj is really of type T, otherwise look for appropriate serializer. + SerializeClassFlags flags; + bool reuseReferences = context.SerializerSelector.ReuseReferences; + int index; + bool externalIdentifiableAsGuid = context.SerializerSelector.ExternalIdentifiableAsGuid; + bool hasTypeInfo; + object objCopy; + DataSerializer objectDataSerializer; + Type type; + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { + flags = SerializeClassFlags.None; + + if (externalIdentifiableAsGuid) { + var externalIdentifiables = context.Get(MemberSerializer.ExternalIdentifiables); + if (obj is IIdentifiable identifiable && externalIdentifiables?.ContainsKey(identifiable.Id) == true) { + flags |= SerializeClassFlags.IsExternalIdentifiable; + stream.Write((byte)flags); + stream.Write(identifiable.Id); + return; + } + } + + index = -1; + if (reuseReferences) { + var objectReferences = context.Get(MemberSerializer.ObjectSerializeReferences); + if (objectReferences.TryGetValue(obj, out index)) { + // Already serialized, just write its contentRef index + flags |= SerializeClassFlags.IsReference; + stream.Write((byte)flags); + stream.Write(index); + return; + } + + // First time it is serialized, add it to objectReferences. + objectReferences.Add(obj, index = objectReferences.Count); + } + + // If real type is not expected type, we need to store type info as well. + var expectedType = typeof(T); + type = isSealed ? expectedType : obj.GetType(); + hasTypeInfo = type != expectedType; + objectDataSerializer = null; + + if (hasTypeInfo) { + // Find matching serializer (always required w/ typeinfo, since type was different than expected) + objectDataSerializer = context.SerializerSelector.GetSerializer(type); + + if (objectDataSerializer is null) + throw new ArgumentException("No serializer available for type " + type.FullName); + + // Update expected type + type = objectDataSerializer.SerializationType; + + // Special case: serializer reports the actual serialized type, and it might actually match the expected type + // (i.e. there is a hidden inherited type, such as PropertyInfo/RuntimePropertyInfo). + // Let's detect it here. + if (type == expectedType) { + // Cancel type info + hasTypeInfo = false; + } else { + // Continue as expected with type info + flags |= SerializeClassFlags.IsTypeInfo; + } + } + + // Serialize flags + stream.Write((byte)flags); + + // Serialize object index (if required) + if (reuseReferences) { + stream.Write(index); + } + + if (hasTypeInfo) { + // Serialize type info + stream.Serialize(ref objectDataSerializer.SerializationTypeId); + + // Serialize object + objCopy = obj; + objectDataSerializer.PreSerialize(ref objCopy, mode, stream); + objectDataSerializer.Serialize(ref objCopy, mode, stream); + obj = (T)objCopy; + } + else + { + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + + flags = (SerializeClassFlags)stream.ReadByte(); + var isNull = 0 != (flags & SerializeClassFlags.IsNull); + var objectReferences = reuseReferences ? context.Get(MemberSerializer.ObjectDeserializeReferences) : null; + var referenceCallback = reuseReferences ? context.Get(MemberSerializer.ObjectDeserializeCallback) : null; + var isReference = 0 != (flags & SerializeClassFlags.IsReference); + var isExternalIdentifiable = 0 != (flags & SerializeClassFlags.IsExternalIdentifiable); + index = reuseReferences && !isNull && !isExternalIdentifiable ? stream.ReadInt32() : -1; + hasTypeInfo = 0 != (flags & SerializeClassFlags.IsTypeInfo); + + if (isNull) { + obj = default(T); + } + else if (externalIdentifiableAsGuid && isExternalIdentifiable) { + var externalIdentifiables = context.Get(MemberSerializer.ExternalIdentifiables); + var identifier = stream.Read(); + IIdentifiable identifiable; + externalIdentifiables.TryGetValue(identifier, out identifiable); + obj = (T)identifiable; + } + else if (reuseReferences && isReference) { + obj = (T)objectReferences[index]; + } + else { + if (reuseReferences) + { + if (objectReferences.Count != index) + throw new InvalidOperationException("Serialization contentRef indices are out of sync."); + objectReferences.Add(null); + } + if (hasTypeInfo) { + Unsafe.SkipInit(out ObjectId serializationTypeId); + stream.Serialize(ref serializationTypeId); + + objectDataSerializer = context.SerializerSelector.GetSerializer(ref serializationTypeId); + if (objectDataSerializer is null) { + throw new ArgumentException("No serializer available for type id " + serializationTypeId + " and base type " + typeof(T).FullName); + } + + objCopy = obj; + objectDataSerializer.PreSerialize(ref objCopy, mode, stream); + + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = objCopy; + } + + objectDataSerializer.Serialize(ref objCopy, mode, stream); + obj = (T)objCopy; + } + else + { + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = obj; + } + + dataSerializer.Serialize(ref obj, mode, stream); + } + + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = obj; + + // Call Reference Callback + referenceCallback?.Invoke(index, obj); + } + } + } + + } + + internal static void SerializeExtended(ref T obj, ArchiveMode mode, SerializationStream stream, DataSerializer dataSerializer = null) { + var context = stream.Context; + + if (isValueType) { + if (dataSerializer is null) { + dataSerializer = (DataSerializer)context.SerializerSelector.GetSerializer(typeof(T)); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + typeof(T).FullName); + } + } + + // Structure, no need to check for inheritance or null values. + dataSerializer.Serialize(ref obj, mode, stream); + return; + } + + // Serialize either with dataSerializer if obj is really of type T, otherwise look for appropriate serializer. + SerializeClassFlags flags; + bool reuseReferences = context.SerializerSelector.ReuseReferences; + int index; + bool externalIdentifiableAsGuid = context.SerializerSelector.ExternalIdentifiableAsGuid; + bool hasTypeInfo; + object objCopy; + DataSerializer objectDataSerializer; + Type type; + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { + flags = SerializeClassFlags.None; + + if (externalIdentifiableAsGuid) { + var externalIdentifiables = context.Get(MemberSerializer.ExternalIdentifiables); + if (obj is IIdentifiable identifiable && externalIdentifiables?.ContainsKey(identifiable.Id) == true) { + flags |= SerializeClassFlags.IsExternalIdentifiable; + stream.Write((byte)flags); + stream.Write(identifiable.Id); + return; + } + } + + index = -1; + if (reuseReferences) { + var objectReferences = context.Get(MemberSerializer.ObjectSerializeReferences); + if (objectReferences.TryGetValue(obj, out index)) { + // Already serialized, just write its contentRef index + flags |= SerializeClassFlags.IsReference; + stream.Write((byte)flags); + stream.Write(index); + return; + } + + // First time it is serialized, add it to objectReferences. + objectReferences.Add(obj, index = objectReferences.Count); + } + + // If real type is not expected type, we need to store type info as well. + var expectedType = typeof(T); + type = isSealed ? expectedType : obj.GetType(); + hasTypeInfo = type != expectedType; + objectDataSerializer = null; + + if (hasTypeInfo) { + // Find matching serializer (always required w/ typeinfo, since type was different than expected) + objectDataSerializer = context.SerializerSelector.GetSerializer(type); + + if (objectDataSerializer is null) + throw new ArgumentException("No serializer available for type " + type.FullName); + + // Update expected type + type = objectDataSerializer.SerializationType; + + // Special case: serializer reports the actual serialized type, and it might actually match the expected type + // (i.e. there is a hidden inherited type, such as PropertyInfo/RuntimePropertyInfo). + // Let's detect it here. + if (type == expectedType) { + // Cancel type info + hasTypeInfo = false; + } else { + // Continue as expected with type info + flags |= SerializeClassFlags.IsTypeInfo; + } + } + + // Serialize flags + stream.Write((byte)flags); + + // Serialize object index (if required) + if (reuseReferences) { + stream.Write(index); + } + + if (hasTypeInfo) { + // Serialize type info + stream.Serialize(ref objectDataSerializer.SerializationTypeId); + + // Serialize object + objCopy = obj; + objectDataSerializer.PreSerialize(ref objCopy, mode, stream); + objectDataSerializer.Serialize(ref objCopy, mode, stream); + obj = (T)objCopy; + } + else + { + if (dataSerializer is null) + { + dataSerializer = (DataSerializer)context.SerializerSelector.GetSerializer(expectedType); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) + throw new ArgumentException("No serializer available for type " + expectedType.FullName); + } + + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + + flags = (SerializeClassFlags)stream.ReadByte(); + var isNull = 0 != (flags & SerializeClassFlags.IsNull); + var objectReferences = reuseReferences ? context.Get(MemberSerializer.ObjectDeserializeReferences) : null; + var referenceCallback = reuseReferences ? context.Get(MemberSerializer.ObjectDeserializeCallback) : null; + var isReference = 0 != (flags & SerializeClassFlags.IsReference); + var isExternalIdentifiable = 0 != (flags & SerializeClassFlags.IsExternalIdentifiable); + index = reuseReferences && !isNull && !isExternalIdentifiable ? stream.ReadInt32() : -1; + hasTypeInfo = 0 != (flags & SerializeClassFlags.IsTypeInfo); + + if (isNull) { + obj = default(T); + } + else if (externalIdentifiableAsGuid && isExternalIdentifiable) { + var externalIdentifiables = context.Get(MemberSerializer.ExternalIdentifiables); + var identifier = stream.Read(); + IIdentifiable identifiable; + externalIdentifiables.TryGetValue(identifier, out identifiable); + obj = (T)identifiable; + } + else if (reuseReferences && isReference) { + obj = (T)objectReferences[index]; + } + else { + if (reuseReferences) + { + if (objectReferences.Count != index) + throw new InvalidOperationException("Serialization contentRef indices are out of sync."); + objectReferences.Add(null); + } + if (hasTypeInfo) { + Unsafe.SkipInit(out ObjectId serializationTypeId); + stream.Serialize(ref serializationTypeId); + + objectDataSerializer = context.SerializerSelector.GetSerializer(ref serializationTypeId); + if (objectDataSerializer is null) { + throw new ArgumentException("No serializer available for type id " + serializationTypeId + " and base type " + typeof(T).FullName); + } + + objCopy = obj; + objectDataSerializer.PreSerialize(ref objCopy, mode, stream); + + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = objCopy; + } + + objectDataSerializer.Serialize(ref objCopy, mode, stream); + obj = (T)objCopy; + } + else + { + if (dataSerializer is null) { + dataSerializer = (DataSerializer)context.SerializerSelector.GetSerializer(typeof(T)); + + // If we still have no serializer, throw an exception + if (dataSerializer is null) { + throw new ArgumentException("No serializer available for type " + typeof(T).FullName); + } + } + + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = obj; + } + + dataSerializer.Serialize(ref obj, mode, stream); + } + + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = obj; + + // Call Reference Callback + referenceCallback?.Invoke(index, obj); + } + } + } + + } +} + +public unsafe class MemberReuseSerializerObject : MemberSerializer { + public MemberReuseSerializerObject(DataSerializer dataSerializer) : base(dataSerializer) { } + + public override void Serialize(ref T obj, ArchiveMode mode, SerializationStream stream) { + var context = stream.Context; + + if (isValueType) { + + // Structure, no need to check for inheritance or null values. + dataSerializer.Serialize(ref obj, mode, stream); + return; + } + + // Serialize either with dataSerializer if obj is really of type T, otherwise look for appropriate serializer. + SerializeClassFlags flags; + bool reuseReferences = context.SerializerSelector.ReuseReferences; + int index; + bool externalIdentifiableAsGuid = context.SerializerSelector.ExternalIdentifiableAsGuid; + bool hasTypeInfo; + object objCopy; + DataSerializer objectDataSerializer; + Type type; + + if (mode == ArchiveMode.Serialize) { + if (obj is null) { + // Null contentRef + stream.Write((byte)SerializeClassFlags.IsNull); + } else { + flags = SerializeClassFlags.None; + + if (externalIdentifiableAsGuid) { + var externalIdentifiables = context.Get(MemberSerializer.ExternalIdentifiables); + if (obj is IIdentifiable identifiable && externalIdentifiables?.ContainsKey(identifiable.Id) == true) { + flags |= SerializeClassFlags.IsExternalIdentifiable; + stream.Write((byte)flags); + stream.Write(identifiable.Id); + return; + } + } + + index = -1; + if (reuseReferences) { + var objectReferences = context.Get(MemberSerializer.ObjectSerializeReferences); + if (objectReferences.TryGetValue(obj, out index)) { + // Already serialized, just write its contentRef index + flags |= SerializeClassFlags.IsReference; + stream.Write((byte)flags); + stream.Write(index); + return; + } + + // First time it is serialized, add it to objectReferences. + objectReferences.Add(obj, index = objectReferences.Count); + } + + // If real type is not expected type, we need to store type info as well. + var expectedType = typeof(T); + type = isSealed ? expectedType : obj.GetType(); + hasTypeInfo = type != expectedType; + objectDataSerializer = null; + + if (hasTypeInfo) { + // Find matching serializer (always required w/ typeinfo, since type was different than expected) + objectDataSerializer = context.SerializerSelector.GetSerializer(type); + + if (objectDataSerializer is null) + throw new ArgumentException("No serializer available for type " + type.FullName); + + // Update expected type + type = objectDataSerializer.SerializationType; + + // Special case: serializer reports the actual serialized type, and it might actually match the expected type + // (i.e. there is a hidden inherited type, such as PropertyInfo/RuntimePropertyInfo). + // Let's detect it here. + if (type == expectedType) { + // Cancel type info + hasTypeInfo = false; + } else { + // Continue as expected with type info + flags |= SerializeClassFlags.IsTypeInfo; + } + } + + // Serialize flags + stream.Write((byte)flags); + + // Serialize object index (if required) + if (reuseReferences) { + stream.Write(index); + } + + if (hasTypeInfo) { + // Serialize type info + stream.Serialize(ref objectDataSerializer.SerializationTypeId); + + // Serialize object + objCopy = obj; + objectDataSerializer.PreSerialize(ref objCopy, mode, stream); + objectDataSerializer.Serialize(ref objCopy, mode, stream); + obj = (T)objCopy; + } + else + { + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + dataSerializer.Serialize(ref obj, mode, stream); + } + } + } + else + { + + flags = (SerializeClassFlags)stream.ReadByte(); + var isNull = 0 != (flags & SerializeClassFlags.IsNull); + var objectReferences = reuseReferences ? context.Get(MemberSerializer.ObjectDeserializeReferences) : null; + var referenceCallback = reuseReferences ? context.Get(MemberSerializer.ObjectDeserializeCallback) : null; + var isReference = 0 != (flags & SerializeClassFlags.IsReference); + var isExternalIdentifiable = 0 != (flags & SerializeClassFlags.IsExternalIdentifiable); + index = reuseReferences && !isNull && !isExternalIdentifiable ? stream.ReadInt32() : -1; + hasTypeInfo = 0 != (flags & SerializeClassFlags.IsTypeInfo); + + if (isNull) { + obj = default(T); + } + else if (externalIdentifiableAsGuid && isExternalIdentifiable) { + var externalIdentifiables = context.Get(MemberSerializer.ExternalIdentifiables); + var identifier = stream.Read(); + IIdentifiable identifiable; + externalIdentifiables.TryGetValue(identifier, out identifiable); + obj = (T)identifiable; + } + else if (reuseReferences && isReference) { + obj = (T)objectReferences[index]; + } + else { + if (reuseReferences) + { + if (objectReferences.Count != index) + throw new InvalidOperationException("Serialization contentRef indices are out of sync."); + objectReferences.Add(null); + } + if (hasTypeInfo) { + Unsafe.SkipInit(out ObjectId serializationTypeId); + stream.Serialize(ref serializationTypeId); + + objectDataSerializer = context.SerializerSelector.GetSerializer(ref serializationTypeId); + if (objectDataSerializer is null) { + throw new ArgumentException("No serializer available for type id " + serializationTypeId + " and base type " + typeof(T).FullName); + } + + objCopy = obj; + objectDataSerializer.PreSerialize(ref objCopy, mode, stream); + + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = objCopy; + } + + objectDataSerializer.Serialize(ref objCopy, mode, stream); + obj = (T)objCopy; + } + else + { + // Serialize object + dataSerializer.PreSerialize(ref obj, mode, stream); + + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = obj; + } + + dataSerializer.Serialize(ref obj, mode, stream); + } + + if (reuseReferences) { + // Register object so that later references to it are working. + objectReferences[index] = obj; + + // Call Reference Callback + referenceCallback?.Invoke(index, obj); + } + } + } + + } +} diff --git a/Rin.Core/Serialization/MemberSerializerGenerated.tt b/Rin.Core/Serialization/MemberSerializerGenerated.tt new file mode 100644 index 0000000..919e1ce --- /dev/null +++ b/Rin.Core/Serialization/MemberSerializerGenerated.tt @@ -0,0 +1,39 @@ +<#@ template debug="false" hostspecific="false" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".cs" #> +// +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using Rin.Core.Storage; + +namespace Rin.Core.Serialization; + +<# +var className = "MemberNullableSerializer"; +var supportsNullDataSerializer = false; +var supportsNonSealed = false; +var supportsGenerics = false; +var supportsValueType = false; +var supportsReuseReferences = false; +var supportsExternalIdentifiableAsGuid = false; #> +<#@ include file="MemberSerializerClass.ttinclude" #> + +<# +className = "MemberNonSealedSerializer"; +supportsNonSealed = true; +supportsValueType = false; +supportsReuseReferences = false; +supportsExternalIdentifiableAsGuid = false; #> +<#@ include file="MemberSerializerClass.ttinclude" #> + +<# +className = "MemberReuseSerializer"; +supportsNonSealed = true; +supportsValueType = false; +supportsReuseReferences = true; +supportsExternalIdentifiableAsGuid = true; #> +<#@ include file="MemberSerializerClass.ttinclude" #> diff --git a/Rin.Core/Serialization/SerializeClassFlags.cs b/Rin.Core/Serialization/SerializeClassFlags.cs new file mode 100644 index 0000000..6e475a3 --- /dev/null +++ b/Rin.Core/Serialization/SerializeClassFlags.cs @@ -0,0 +1,33 @@ +namespace Rin.Core.Serialization; + +/// +/// Specifies flags used when serializing reference types. +/// +[Flags] +public enum SerializeClassFlags { + /// + /// Default value. + /// + None = 0, + + /// + /// Specifies that the object is null. + /// + IsNull = 1, + + /// + /// Specifies that additional type info is necessary and is stored in the stream. + /// + IsTypeInfo = 2, + + /// + /// Specifies that the object has already been serialized previously in the stream, and is only stored as an index. + /// + IsReference = 4, + + /// + /// Specifies that the object is an instance has not been serialized, and only its + /// has been stored. + /// + IsExternalIdentifiable = 8 +} diff --git a/Rin.Core/Serialization/SerializerFactory.cs b/Rin.Core/Serialization/SerializerFactory.cs new file mode 100644 index 0000000..47c9d53 --- /dev/null +++ b/Rin.Core/Serialization/SerializerFactory.cs @@ -0,0 +1,11 @@ +using Rin.Core.Storage; + +namespace Rin.Core.Serialization; + +/// +/// Used as a fallback when didn't find anything. +/// +public abstract class SerializerFactory { + public abstract DataSerializer GetSerializer(SerializerSelector selector, ref ObjectId typeId); + public abstract DataSerializer GetSerializer(SerializerSelector selector, Type type); +} diff --git a/Rin.Core/Serialization/Serializers/CollectionSerializers.cs b/Rin.Core/Serialization/Serializers/CollectionSerializers.cs new file mode 100644 index 0000000..affea66 --- /dev/null +++ b/Rin.Core/Serialization/Serializers/CollectionSerializers.cs @@ -0,0 +1,535 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +namespace Rin.Core.Serialization.Serializers; + +/// +/// Data serializer for List{T}. +/// +/// Generics type of List{T}. +[DataSerializerGlobal(typeof(ListSerializer<>), typeof(List<>), DataSerializerGenericMode.GenericArguments)] +public class ListSerializer : DataSerializer>, IDataSerializerGenericInstantiation { + DataSerializer itemDataSerializer; + + /// + public override void Initialize(SerializerSelector serializerSelector) { + itemDataSerializer = MemberSerializer.Create(serializerSelector); + } + + /// + public override void PreSerialize(ref List? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + if (obj == null) { + obj = new(); + } else { + obj.Clear(); + } + } + } + + /// + public override void Serialize(ref List? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + var count = stream.ReadInt32(); + obj.Capacity = count; + for (var i = 0; i < count; ++i) { + var value = default(T); + itemDataSerializer.Serialize(ref value, mode, stream); + obj.Add(value); + } + } else if (mode == ArchiveMode.Serialize) { + stream.Write(obj.Count); + foreach (var item in obj) { + itemDataSerializer.Serialize(item, stream); + } + } + } + + /// + public void EnumerateGenericInstantiations( + SerializerSelector serializerSelector, + IList genericInstantiations + ) { + genericInstantiations.Add(typeof(T)); + } +} + +/// +/// Data serializer for IList{T}. +/// +/// Type of IList{T}. +/// Generics type of IList{T}. +public class ListAllSerializer : DataSerializer, IDataSerializerGenericInstantiation + where TList : class, IList { + readonly bool isInterface = typeof(TList).GetTypeInfo().IsInterface; + DataSerializer itemDataSerializer; + + /// + public override void Initialize(SerializerSelector serializerSelector) { + itemDataSerializer = MemberSerializer.Create(serializerSelector); + } + + /// + public override void PreSerialize(ref TList? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + if (obj == null) { + obj = isInterface ? (TList)(object)new List() : Activator.CreateInstance(); + } else { + obj.Clear(); + } + } + } + + /// + public override void Serialize(ref TList? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + var count = stream.ReadInt32(); + for (var i = 0; i < count; ++i) { + var value = default(T); + itemDataSerializer.Serialize(ref value, mode, stream); + obj.Add(value); + } + } else if (mode == ArchiveMode.Serialize) { + stream.Write(obj.Count); + foreach (var item in obj) { + itemDataSerializer.Serialize(item, stream); + } + } + } + + /// + public void EnumerateGenericInstantiations( + SerializerSelector serializerSelector, + IList genericInstantiations + ) { + genericInstantiations.Add(typeof(T)); + } +} + +/// +/// Data serializer for SortedList{TKey, TValue}. +/// +/// The type of the key in SortedList{TKey, TValue}. +/// The type of the value in SortedList{TKey, TValue}. +[DataSerializerGlobal( + typeof(SortedListSerializer<,>), + typeof(Rin.Core.Collections.SortedList<,>), + DataSerializerGenericMode.GenericArguments +)] +public class SortedListSerializer : DataSerializer>, + IDataSerializerGenericInstantiation { + DataSerializer keySerializer; + DataSerializer valueSerializer; + + /// + public override void Initialize(SerializerSelector serializerSelector) { + // Key should never be null + keySerializer = MemberSerializer.Create(serializerSelector, false); + valueSerializer = MemberSerializer.Create(serializerSelector); + } + + /// + public override void PreSerialize( + ref Rin.Core.Collections.SortedList obj, + ArchiveMode mode, + SerializationStream stream + ) { + if (mode == ArchiveMode.Deserialize) { + // TODO: Peek the SortedList size + if (obj == null) { + obj = new Rin.Core.Collections.SortedList(); + } else { + obj.Clear(); + } + } + } + + /// + public override void Serialize( + ref Rin.Core.Collections.SortedList obj, + ArchiveMode mode, + SerializationStream stream + ) { + if (mode == ArchiveMode.Deserialize) { + // Should be null if it was + var count = stream.ReadInt32(); + for (var i = 0; i < count; ++i) { + var key = default(TKey); + var value = default(TValue); + keySerializer.Serialize(ref key, mode, stream); + valueSerializer.Serialize(ref value, mode, stream); + obj.Add(key, value); + } + } else if (mode == ArchiveMode.Serialize) { + stream.Write(obj.Count); + foreach (var item in obj) { + keySerializer.Serialize(item.Key, stream); + valueSerializer.Serialize(item.Value, stream); + } + } + } + + /// + public void EnumerateGenericInstantiations( + SerializerSelector serializerSelector, + IList genericInstantiations + ) { + genericInstantiations.Add(typeof(TKey)); + genericInstantiations.Add(typeof(TValue)); + } +} + +/// +/// Data serializer for IList{T}. +/// +/// Generics type of IList{T}. +[DataSerializerGlobal(typeof(ListInterfaceSerializer<>), typeof(IList<>), DataSerializerGenericMode.GenericArguments)] +public class ListInterfaceSerializer : DataSerializer>, IDataSerializerGenericInstantiation { + DataSerializer itemDataSerializer; + + /// + public override void Initialize(SerializerSelector serializerSelector) { + itemDataSerializer = MemberSerializer.Create(serializerSelector); + } + + /// + public override void PreSerialize(ref IList? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + if (obj == null) { + obj = new List(); + } else { + obj.Clear(); + } + } + } + + /// + public override void Serialize(ref IList? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + var count = stream.ReadInt32(); + for (var i = 0; i < count; ++i) { + var value = default(T); + itemDataSerializer.Serialize(ref value, mode, stream); + obj.Add(value); + } + } else if (mode == ArchiveMode.Serialize) { + stream.Write(obj.Count); + foreach (var item in obj) { + itemDataSerializer.Serialize(item, stream); + } + } + } + + /// + public void EnumerateGenericInstantiations( + SerializerSelector serializerSelector, + IList genericInstantiations + ) { + genericInstantiations.Add(typeof(T)); + + // Force concrete type to be implemented (that's what will likely be used with this interface) + genericInstantiations.Add(typeof(List)); + } +} + +/// +/// Data serializer for T[]. +/// +/// Generics type of T[]. +public class ArraySerializer : DataSerializer, IDataSerializerGenericInstantiation { + DataSerializer itemDataSerializer; + + /// + public override void Initialize(SerializerSelector serializerSelector) { + itemDataSerializer = MemberSerializer.Create(serializerSelector); + } + + /// + public override void PreSerialize(ref T[]? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Serialize) { + stream.Write(obj.Length); + } else if (mode == ArchiveMode.Deserialize) { + var length = stream.ReadInt32(); + obj = new T[length]; + } + } + + /// + public override void Serialize(ref T[]? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + var count = obj.Length; + for (var i = 0; i < count; ++i) { + itemDataSerializer.Serialize(ref obj[i], mode, stream); + } + } else if (mode == ArchiveMode.Serialize) { + var count = obj.Length; + for (var i = 0; i < count; ++i) { + itemDataSerializer.Serialize(ref obj[i], mode, stream); + } + } + } + + /// + public void EnumerateGenericInstantiations( + SerializerSelector serializerSelector, + IList genericInstantiations + ) { + genericInstantiations.Add(typeof(T)); + } +} + +/// +/// Data serializer for blittable T[]. +/// +/// Generics type of T[]. +public class BlittableArraySerializer : ArraySerializer where T : unmanaged { + /// + public override void Initialize(SerializerSelector serializerSelector) { } + + /// + public override void Serialize(ref T[]? array, ArchiveMode mode, SerializationStream stream) { + var span = MemoryMarshal.Cast(array.AsSpan()); + if (mode == ArchiveMode.Deserialize) { + stream.UnderlyingStream.Read(span); + } else if (mode == ArchiveMode.Serialize) { + stream.UnderlyingStream.Write(span); + } + } +} + +/// +/// Data serializer for KeyValuePair{TKey, TValue}. +/// +/// The type of the key in KeyValuePair{TKey, TValue}. +/// The type of the value in KeyValuePair{TKey, TValue}. +[DataSerializerGlobal( + typeof(KeyValuePairSerializer<,>), + typeof(KeyValuePair<,>), + DataSerializerGenericMode.GenericArguments +)] +public class KeyValuePairSerializer : DataSerializer>, + IDataSerializerGenericInstantiation { + DataSerializer keySerializer; + DataSerializer valueSerializer; + + /// + public override void Initialize(SerializerSelector serializerSelector) { + // Key should never be null + keySerializer = MemberSerializer.Create(serializerSelector); + valueSerializer = MemberSerializer.Create(serializerSelector); + } + + /// + public override void Serialize(ref KeyValuePair obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + var key = default(TKey); + var value = default(TValue); + keySerializer.Serialize(ref key, mode, stream); + valueSerializer.Serialize(ref value, mode, stream); + obj = new(key, value); + } else if (mode == ArchiveMode.Serialize) { + keySerializer.Serialize(obj.Key, stream); + valueSerializer.Serialize(obj.Value, stream); + } + } + + /// + public void EnumerateGenericInstantiations( + SerializerSelector serializerSelector, + IList genericInstantiations + ) { + genericInstantiations.Add(typeof(TKey)); + genericInstantiations.Add(typeof(TValue)); + } +} + +/// +/// Data serializer for Dictionary{TKey, TValue}. +/// +/// The type of the key in Dictionary{TKey, TValue}. +/// The type of the value in Dictionary{TKey, TValue}. +[DataSerializerGlobal( + typeof(DictionarySerializer<,>), + typeof(Dictionary<,>), + DataSerializerGenericMode.GenericArguments +)] +public class DictionarySerializer : DataSerializer>, + IDataSerializerGenericInstantiation { + DataSerializer keySerializer; + DataSerializer valueSerializer; + + /// + public override void Initialize(SerializerSelector serializerSelector) { + // Key should never be null + keySerializer = MemberSerializer.Create(serializerSelector, false); + valueSerializer = MemberSerializer.Create(serializerSelector); + } + + /// + public override void PreSerialize(ref Dictionary? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + // TODO: Peek the dictionary size + if (obj == null) { + obj = new(); + } else { + obj.Clear(); + } + } + } + + /// + public override void Serialize(ref Dictionary obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + // Should be null if it was + var count = stream.ReadInt32(); + for (var i = 0; i < count; ++i) { + var key = default(TKey); + var value = default(TValue); + keySerializer.Serialize(ref key, mode, stream); + valueSerializer.Serialize(ref value, mode, stream); + obj.Add(key, value); + } + } else if (mode == ArchiveMode.Serialize) { + stream.Write(obj.Count); + foreach (var item in obj) { + keySerializer.Serialize(item.Key, stream); + valueSerializer.Serialize(item.Value, stream); + } + } + } + + /// + public void EnumerateGenericInstantiations( + SerializerSelector serializerSelector, + IList genericInstantiations + ) { + genericInstantiations.Add(typeof(TKey)); + genericInstantiations.Add(typeof(TValue)); + } +} + +public class DictionaryAllSerializer : DataSerializer, + IDataSerializerGenericInstantiation where TDictionary : IDictionary { + readonly bool isInterface = typeof(TDictionary).GetTypeInfo().IsInterface; + DataSerializer keySerializer; + DataSerializer valueSerializer; + + /// + public override void Initialize(SerializerSelector serializerSelector) { + // Key should never be null + keySerializer = MemberSerializer.Create(serializerSelector, false); + valueSerializer = MemberSerializer.Create(serializerSelector); + } + + /// + public override void PreSerialize(ref TDictionary? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + // TODO: Peek the dictionary size + if (obj == null) { + obj = isInterface + ? (TDictionary)(object)new Dictionary() + : Activator.CreateInstance(); + } else { + obj.Clear(); + } + } + } + + /// + public override void Serialize(ref TDictionary? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + // Should be null if it was + var count = stream.ReadInt32(); + for (var i = 0; i < count; ++i) { + var key = default(TKey); + var value = default(TValue); + keySerializer.Serialize(ref key, mode, stream); + valueSerializer.Serialize(ref value, mode, stream); + obj.Add(key, value); + } + } else if (mode == ArchiveMode.Serialize) { + stream.Write(obj.Count); + foreach (var item in obj) { + keySerializer.Serialize(item.Key, stream); + valueSerializer.Serialize(item.Value, stream); + } + } + } + + /// + public void EnumerateGenericInstantiations( + SerializerSelector serializerSelector, + IList genericInstantiations + ) { + genericInstantiations.Add(typeof(TKey)); + genericInstantiations.Add(typeof(TValue)); + } +} + +/// +/// Data serializer for IDictionary{TKey, TValue}. +/// +/// The type of the key in IDictionary{TKey, TValue}. +/// The type of the value in IDictionary{TKey, TValue}. +[DataSerializerGlobal( + typeof(DictionaryInterfaceSerializer<,>), + typeof(IDictionary<,>), + DataSerializerGenericMode.GenericArguments +)] +public class DictionaryInterfaceSerializer : DataSerializer>, + IDataSerializerGenericInstantiation { + DataSerializer keySerializer; + DataSerializer valueSerializer; + + /// + public override void Initialize(SerializerSelector serializerSelector) { + // Key should never be null + keySerializer = MemberSerializer.Create(serializerSelector, false); + valueSerializer = MemberSerializer.Create(serializerSelector); + } + + /// + public override void PreSerialize(ref IDictionary? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + // TODO: Peek the dictionary size + if (obj == null) { + obj = new Dictionary(); + } else { + obj.Clear(); + } + } + } + + /// + public override void Serialize(ref IDictionary? obj, ArchiveMode mode, SerializationStream stream) { + if (mode == ArchiveMode.Deserialize) { + // Should be null if it was + var count = stream.ReadInt32(); + for (var i = 0; i < count; ++i) { + var key = default(TKey); + var value = default(TValue); + keySerializer.Serialize(ref key, mode, stream); + valueSerializer.Serialize(ref value, mode, stream); + obj.Add(key, value); + } + } else if (mode == ArchiveMode.Serialize) { + stream.Write(obj.Count); + foreach (var item in obj) { + keySerializer.Serialize(item.Key, stream); + valueSerializer.Serialize(item.Value, stream); + } + } + } + + /// + public void EnumerateGenericInstantiations( + SerializerSelector serializerSelector, + IList genericInstantiations + ) { + genericInstantiations.Add(typeof(TKey)); + genericInstantiations.Add(typeof(TValue)); + + // Force concrete type to be implemented (that's what will likely be used with this interface) + genericInstantiations.Add(typeof(Dictionary)); + } +} diff --git a/Rin.Diagnostics/ProfileScope.cs b/Rin.Diagnostics/ProfileScope.cs index 26cd50f..509657d 100644 --- a/Rin.Diagnostics/ProfileScope.cs +++ b/Rin.Diagnostics/ProfileScope.cs @@ -21,4 +21,4 @@ public void Dispose() { reportHistogram.Record(activity.Duration.TotalMilliseconds); activity.Dispose(); } -} +} \ No newline at end of file diff --git a/Rin.Diagnostics/Profiler.cs b/Rin.Diagnostics/Profiler.cs index 59b6511..41aa2d1 100644 --- a/Rin.Diagnostics/Profiler.cs +++ b/Rin.Diagnostics/Profiler.cs @@ -1,7 +1,6 @@ using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; -using System.Diagnostics; namespace Rin.Diagnostics; diff --git a/Rin.Diagnostics/ProfilingKey.cs b/Rin.Diagnostics/ProfilingKey.cs new file mode 100644 index 0000000..1474c98 --- /dev/null +++ b/Rin.Diagnostics/ProfilingKey.cs @@ -0,0 +1,16 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace Rin.Diagnostics; + +public readonly struct ProfilingKey { + readonly Histogram reportHistogram; + readonly Activity? activity; + + public ProfilingKey(Histogram reportHistogram, Activity? activity) { + this.reportHistogram = reportHistogram; + this.activity = activity; + } + + public ProfileScope Begin() => new(reportHistogram, activity); +} diff --git a/build/Build.cs b/build/Build.cs index 9026020..68c2075 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -1,44 +1,118 @@ -using System; -using System.Linq; using Nuke.Common; -using Nuke.Common.CI; -using Nuke.Common.Execution; +using Nuke.Common.CI.GitHubActions; using Nuke.Common.IO; using Nuke.Common.ProjectModel; -using Nuke.Common.Tooling; -using Nuke.Common.Utilities.Collections; -using static Nuke.Common.EnvironmentInfo; -using static Nuke.Common.IO.FileSystemTasks; -using static Nuke.Common.IO.PathConstruction; - -class Build : NukeBuild -{ - /// Support plugins are available for: - /// - JetBrains ReSharper https://nuke.build/resharper - /// - JetBrains Rider https://nuke.build/rider - /// - Microsoft VisualStudio https://nuke.build/visualstudio - /// - Microsoft VSCode https://nuke.build/vscode - - public static int Main () => Execute(x => x.Compile); +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.GitVersion; +using Serilog; +using static Nuke.Common.Tools.DotNet.DotNetTasks; + +[GitHubActions("ci", + GitHubActionsImage.UbuntuLatest, + GitHubActionsImage.MacOsLatest, + GitHubActionsImage.WindowsLatest, + AutoGenerate = true, + FetchDepth = 0, + OnPushBranches = new[] { "master", "dev", "release/**" }, + OnPullRequestBranches = new[] { "release/**" }, + InvokedTargets = new[] { nameof(Compile), nameof(Test) } +)] +class Build : NukeBuild { [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; - Target Clean => _ => _ - .Before(Restore) - .Executes(() => - { - }); + [Solution] readonly Solution Solution; + [GitVersion] readonly GitVersion GitVersion; + + static AbsolutePath ArtifactsDirectory => RootDirectory / ".artifacts"; + + + Target Clean => + _ => _ + .Before(Restore) + .Executes(() => + { + DotNetClean(s => s.SetProject(Solution)); + ArtifactsDirectory.CreateOrCleanDirectory(); + } + ); + + Target Restore => + _ => _ + .After(Clean) + .Executes(() => + { + DotNetRestore(s => s.SetProjectFile(Solution)); + } + ); + + Target Compile => + _ => _ + .DependsOn(Restore) + .Executes(() => + { + DotNetBuild(s => s + .SetProjectFile(Solution) + .SetConfiguration(Configuration) + .SetVersion(GitVersion.NuGetVersionV2) + .SetAssemblyVersion(GitVersion.AssemblySemVer) + .SetInformationalVersion(GitVersion.InformationalVersion) + .SetFileVersion(GitVersion.AssemblySemFileVer) + .EnableNoRestore() + ); + } + ); + + Target Pack => + _ => _ + .Description("Packing Project with the version") + .Requires(() => Configuration.Equals(Configuration.Release)) + // .Produces(ArtifactsDirectory / ArtifactsType) + .DependsOn(Compile) + // .Triggers(PublishToGithub, PublishToMyGet, PublishToNuGet) + .Executes(() => + { + DotNetPack(p => + p + // .SetProject(Solution.src.Sundry_ielloWorld) + .SetConfiguration(Configuration) + .SetOutputDirectory(ArtifactsDirectory) + .EnableNoBuild() + .EnableNoRestore() + // .SetCopyright(Copyright) + .SetVersion(GitVersion.NuGetVersionV2) + .SetAssemblyVersion(GitVersion.AssemblySemVer) + .SetInformationalVersion(GitVersion.InformationalVersion) + .SetFileVersion(GitVersion.AssemblySemFileVer) + ); + } + ); - Target Restore => _ => _ - .Executes(() => - { - }); + Target Test => + _ => _ + .After(Compile) + .Executes(() => + { + foreach (var project in Solution.GetAllProjects("*")) { + if (project.Name.EndsWith(".Tests")) { + DotNetTest(s => s + .SetProjectFile(project) + .EnableNoRestore() + .EnableNoBuild() + ); + } + } + } + ); - Target Compile => _ => _ - .DependsOn(Restore) - .Executes(() => - { - }); + Target Print => + _ => _ + .Executes(() => + { + Log.Information("GitVersion = {Value}", GitVersion.MajorMinorPatch); + } + ); + public static int Main() => Execute(x => x.Compile); } diff --git a/build/Configuration.cs b/build/Configuration.cs index 9c08b1a..aaa026a 100644 --- a/build/Configuration.cs +++ b/build/Configuration.cs @@ -1,16 +1,10 @@ -using System; -using System.ComponentModel; -using System.Linq; using Nuke.Common.Tooling; +using System.ComponentModel; [TypeConverter(typeof(TypeConverter))] -public class Configuration : Enumeration -{ - public static Configuration Debug = new Configuration { Value = nameof(Debug) }; - public static Configuration Release = new Configuration { Value = nameof(Release) }; +public class Configuration : Enumeration { + public static Configuration Debug = new() { Value = nameof(Debug) }; + public static Configuration Release = new() { Value = nameof(Release) }; - public static implicit operator string(Configuration configuration) - { - return configuration.Value; - } + public static implicit operator string(Configuration configuration) => configuration.Value; } diff --git a/build/_build.csproj b/build/_build.csproj index c97b50f..8d38db9 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -1,5 +1,4 @@ - Exe net6.0 @@ -15,4 +14,7 @@ + + +