Unity LoggingでSinkを自作する
概要
2/29日にUnityプログラミング・バイブル R6号が出ます。 この本でUnity Loggingの使い方について執筆させていただきました。ぜひみんな買ってね。
この本ではUnity Loggingについて執筆していましたが、ページ数の都合上いくつか削除した項目が存在します。 今回はその中で、自作Sinkの作り方についてちょっと記載していきます。
自分でSinkを作りたいというときに参考になれば幸いです。
ちなみにBurstの説明はそんなにないです。
Sinkを自作する
Sinkは用意された出力先だけでも強力ですが自作することもできます。ただし、自作する場合は以下の知識が必要です。
- unsafeコードの理解
- Burstの理解
本章では上記の詳細な解説は省略します。
Logの個数をカウントするSinkを自作してみる
今回は、簡単な例として「Logの個数をカウントするSink」を実装してみましょう。最終的な呼び出しは以下のようになります。
using Unity.Logging; using Unity.Logging.Sinks; using UnityEngine; namespace LoggingSample.Sinks { public class CustomSinkSample : MonoBehaviour { private void Start() { var config = new LoggerConfig() .SyncMode.FullSync() // 非同期だとすぐに出力されないので同期 .WriteTo.UnityEditorConsole() // 動作確認のためにUnityEditorConsoleにも出力 .WriteTo.CountLogger(); Log.Logger = new Unity.Logging.Logger(config); Log.Info("foo"); Log.Info("bar"); Log.Info(CountSinkSystem.LogCounter); // => 2 } } }
SinkSystemBaseとSinkConfiguration
自作Sinkを作る場合、以下の3つの実装が必要です。
- SinkConfigurationを継承したクラスの実装
- SinkSystemBaseを継承したクラスの実装
- 呼び出すための拡張メソッドの実装
Sinkは実際にログを受け取ったときの処理を行うSinkSystemBase
と、SinkSystemBaseの設定及びインスタンスの生成を行うSinkConfiguration
の2つで構成されています。今回はCountSinkSystem
という名前で実装してみましょう。
まずはConfigurationクラスの作成です。
using Unity.Collections; using Unity.Logging; using Unity.Logging.Sinks; namespace LoggingSample.Sinks { public class Configure : SinkConfiguration { public Configure(LoggerWriterConfig writeTo, FormatterStruct formatter, bool? captureStackTraceOverride = null, LogLevel? minLevelOverride = null, FixedString512Bytes? outputTemplateOverride = null) : base(writeTo, formatter, captureStackTraceOverride, minLevelOverride, outputTemplateOverride) { } public override SinkSystemBase CreateSinkInstance(Logger logger) { // CountSinkSystemのインスタンスを作成する return CreateAndInitializeSinkInstance<CountSinkSystem>(logger, this); } } }
ConfigureでやるべきことはSinkSystemインスタンスの生成です。これは CreateSinkInstance
をオーバーライドする必要があります。インスタンスの生成は直接行うのではなく、baseに実装されている CreateAndInitializeSinkInstance<T>
を呼び出します。今回はここに自作SinkであるCountSinkSystem
を指定します。
次にCountSinkSystem本体を実装します。コンパイルが通る最低限の状態にすると以下のとおりです。
using System; using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Logging; using Unity.Logging.Sinks; namespace LoggingSample.Sinks { // SinkSystemBaseを継承する場合BurstCompile対応が必須になる [BurstCompile] public class CountSinkSystem : SinkSystemBase { public static int LogCounter; public class Configure : SinkConfiguration { public Configure(LoggerWriterConfig writeTo, FormatterStruct formatter, bool? captureStackTraceOverride = null, LogLevel? minLevelOverride = null, FixedString512Bytes? outputTemplateOverride = null) : base(writeTo, formatter, captureStackTraceOverride, minLevelOverride, outputTemplateOverride) { } public override SinkSystemBase CreateSinkInstance(Logger logger) { // CountSinkSystemのインスタンスを作成する return CreateAndInitializeSinkInstance<CountSinkSystem>(logger, this); } } public override LogController.SinkStruct ToSinkStruct() { var s = base.ToSinkStruct(); // ログ取得時のデリゲートを設定する s.OnLogMessageEmit = new OnLogMessageEmitDelegate(OnLogMessageEmitFunc); return s; } public override void Initialize(Logger logger, SinkConfiguration systemConfig) { base.Initialize(logger, systemConfig); } /// <summary> /// ログメッセージが発行されたときに呼ばれるコールバック /// </summary> [BurstCompile] [AOT.MonoPInvokeCallback(typeof(OnLogMessageEmitDelegate.Delegate))] internal static void OnLogMessageEmitFunc(in LogMessage logEvent, ref FixedString512Bytes outTemplate, ref UnsafeText messageBuffer, IntPtr memoryManager, IntPtr userData, Allocator allocator) { } } }
先程定義したConfigure
はCountSinkSystem
のインナークラスにしています。これは、File SinkやJson Sinkなどデフォルトで用意されているSinkの実装方法に合わせているためです。
SinkSystemの実装にあたりやらなければならないことは以下の3つです。
- クラスをBurstCompileに対応させる
- ToSinkStructでログ取得のメッセージを受け取れるようにする
- ログメッセージを受け取った際のデリゲートを実装する
大前提としてSinkはBurstCompilerによる最適化が行われているため、自作する場合もBurstCompile対応が必要です。そのためCountSinkSystemに[BurstCompile]
属性を付与しています。
ToSinkStruct()
はLogger側で扱うSinkに関する構造体SinkStruct
を返すメソッドです。ここで生成する構造体にはログを流すためのデリゲートOnLogMessageEmit
が用意されており、ここに任意のメソッドを渡すことで実際のSink側の処理を実現できます。今回だと「CountSinkSystem.LogCounterを加算する」というのが詳細なロジックになります。
OnLogMessageEmitFunc
はログ情報を受け取って実際の処理を行うデリゲートです。
[BurstCompile] [AOT.MonoPInvokeCallback(typeof(OnLogMessageEmitDelegate.Delegate))] internal static void OnLogMessageEmitFunc(in LogMessage logEvent, ref FixedString512Bytes outTemplate, ref UnsafeText messageBuffer, IntPtr memoryManager, IntPtr userData, Allocator allocator) { }
ここで加算処理を行えると良いのですが、BurstCompileを行う場合readonlyではないstatic変数にアクセスができないためLogCounterはこのメソッド内では直接更新ができません。
つまり、Burstの世界だけだとできることが限られてしまうので、このメソッドから先はManaged C#の世界と繋いで処理を委譲させることになります。Unity Loggingパッケージは内部で Burst2ManagedCall
というクラスを定義しており、BurstとManaged C#をFunctionPointerで繋いでいます。このクラスはinternalで外部からアクセスできないので、実装を参考にして定義します。
using System; using System.Diagnostics; using System.Runtime.InteropServices; using Unity.Burst; namespace LoggingSample.Sinks { /// <summary> /// BurstからManagedコードを呼び出すためのデリゲートを管理するクラス /// </summary> internal static class Burst2ManagedCall<T, Key> { private static T _delegate; private static readonly SharedStatic<FunctionPointer<T>> _sharedStatic = SharedStatic<FunctionPointer<T>>.GetOrCreate<FunctionPointer<T>, Key>(16); public static bool IsCreated => _sharedStatic.Data.IsCreated; public static void Init(T @delegate) { CheckIsNotCreated(); _delegate = @delegate; _sharedStatic.Data = new FunctionPointer<T>(Marshal.GetFunctionPointerForDelegate(_delegate)); } public static ref FunctionPointer<T> Ptr() { CheckIsCreated(); return ref _sharedStatic.Data; } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")] private static void CheckIsCreated() { if (IsCreated == false) throw new InvalidOperationException("Burst2ManagedCall was NOT created!"); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")] private static void CheckIsNotCreated() { if (IsCreated) throw new InvalidOperationException("Burst2ManagedCall was already created!"); } } }
次に、カウントを加算するだけのクラス CountLogWrapper
を定義します。
using System; using System.Diagnostics; using System.Runtime.InteropServices; using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Logging; using Unity.Logging.Sinks; namespace LoggingSample.Sinks { /// <summary> /// ログが出力された回数をカウントするクラス /// </summary> internal static class CountLogWrapper { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void WriteDelegate(); private struct CountLogWrapperKey { } private static bool _initialized; internal static void Initialize() { if (_initialized) return; _initialized = true; // デリゲートを登録する Burst2ManagedCall<WriteDelegate, CountLogWrapperKey>.Init(WriteFunc); } [AOT.MonoPInvokeCallback(typeof(WriteDelegate))] private static void WriteFunc() { // カウント処理 CountSinkSystem.LogCounter++; } public static void Write() { var ptr = Burst2ManagedCall<WriteDelegate, CountLogWrapperKey>.Ptr(); #if LOGGING_USE_UNMANAGED_DELEGATES ((delegate * unmanaged[Cdecl] <LogLevel, byte*, int, void>)ptr.Value)(level, data, length); #else ptr.Invoke(); #endif } } }
WriteFunc
が実際の加算処理となっており、これをBurst2ManagedCall
に登録してWrite
メソッド内でFunctionPointerとして取得、及び呼び出すことでBurst上でも呼び出せるようにしています。
あとは外部からCountLogWrapper.Initialize
を呼び出して登録をしなければならないので、CountSinkSystem側で登録します。
using System; using System.Diagnostics; using System.Runtime.InteropServices; using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Logging; using Unity.Logging.Sinks; namespace LoggingSample.Sinks { // SinkSystemBaseを継承する場合BurstCompile対応が必須になる [BurstCompile] public class CountSinkSystem : SinkSystemBase { public static int LogCounter; public class Configure : SinkConfiguration { public Configure(LoggerWriterConfig writeTo, FormatterStruct formatter, bool? captureStackTraceOverride = null, LogLevel? minLevelOverride = null, FixedString512Bytes? outputTemplateOverride = null) : base(writeTo, formatter, captureStackTraceOverride, minLevelOverride, outputTemplateOverride) { } public override SinkSystemBase CreateSinkInstance(Logger logger) { // CountSinkSystemのインスタンスを作成する return CreateAndInitializeSinkInstance<CountSinkSystem>(logger, this); } } public override LogController.SinkStruct ToSinkStruct() { var s = base.ToSinkStruct(); // ログ取得時のデリゲートを設定する s.OnLogMessageEmit = new OnLogMessageEmitDelegate(OnLogMessageEmitFunc); return s; } public override void Initialize(Logger logger, SinkConfiguration systemConfig) { // インスタンスの初期化時にログのカウント回数も初期化する CountLogWrapper.Initialize(); base.Initialize(logger, systemConfig); } /// <summary> /// ログメッセージが発行されたときに呼ばれるコールバック /// </summary> [BurstCompile] [AOT.MonoPInvokeCallback(typeof(OnLogMessageEmitDelegate.Delegate))] internal static void OnLogMessageEmitFunc(in LogMessage logEvent, ref FixedString512Bytes outTemplate, ref UnsafeText messageBuffer, IntPtr memoryManager, IntPtr userData, Allocator allocator) { try { // BurstCompileに対応させる場合readonlyではないstatic変数にアクセスできないため、 // CountLogWrapperを経由してカウントする CountLogWrapper.Write(); } finally { messageBuffer.Length = 0; } } } }
OnLogMessageEmitFunc
ではWriteを呼び出すだけですが、これでログが流れるたびにカウントされるようになりました。
あとは、このSinkを有効にできるような拡張メソッドCountLogger
を定義します。
using Unity.Logging; using Unity.Logging.Sinks; namespace LoggingSample.Sinks { // WriteTo.CountLogger()のように呼び出せるようにするための拡張メソッド public static class CountLoggerExtensions { public static LoggerConfig CountLogger(this LoggerWriterConfig writeTo, LogLevel? minLevel = null) { return writeTo.AddSinkConfig(new CountSinkSystem.Configure(writeTo, default, false, minLevel)); } } }
AddSinkConfig
はSink設定を追加するメソッドです。ここでConfigureを生成することで新たにSinkを追加できます。
ちなみにSinkの拡張は、対応するExtensionsクラスを作って拡張メソッドを追加していくのが慣例となっています。
最終的なコードは以下の通りです。
using System; using System.Diagnostics; using System.Runtime.InteropServices; using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Logging; using Unity.Logging.Sinks; namespace LoggingSample.Sinks { public static class CountLoggerExtensions { // WriteTo.CountLogger()のように呼び出せるようにするための拡張メソッド public static LoggerConfig CountLogger(this LoggerWriterConfig writeTo, LogLevel? minLevel = null) { return writeTo.AddSinkConfig(new CountSinkSystem.Configure(writeTo, default, false, minLevel)); } } // SinkSystemBaseを継承する場合BurstCompile対応が必須になる [BurstCompile] public class CountSinkSystem : SinkSystemBase { public static int LogCounter; public class Configure : SinkConfiguration { public Configure(LoggerWriterConfig writeTo, FormatterStruct formatter, bool? captureStackTraceOverride = null, LogLevel? minLevelOverride = null, FixedString512Bytes? outputTemplateOverride = null) : base(writeTo, formatter, captureStackTraceOverride, minLevelOverride, outputTemplateOverride) { } public override SinkSystemBase CreateSinkInstance(Logger logger) { // CountSinkSystemのインスタンスを作成する return CreateAndInitializeSinkInstance<CountSinkSystem>(logger, this); } } public override LogController.SinkStruct ToSinkStruct() { var s = base.ToSinkStruct(); // ログ取得時のデリゲートを設定する s.OnLogMessageEmit = new OnLogMessageEmitDelegate(OnLogMessageEmitFunc); return s; } public override void Initialize(Logger logger, SinkConfiguration systemConfig) { // インスタンスの初期化時にログのカウント回数も初期化する CountLogWrapper.Initialize(); base.Initialize(logger, systemConfig); } /// <summary> /// ログメッセージが発行されたときに呼ばれるコールバック /// </summary> [BurstCompile] [AOT.MonoPInvokeCallback(typeof(OnLogMessageEmitDelegate.Delegate))] internal static void OnLogMessageEmitFunc(in LogMessage logEvent, ref FixedString512Bytes outTemplate, ref UnsafeText messageBuffer, IntPtr memoryManager, IntPtr userData, Allocator allocator) { try { // BurstCompileに対応させる場合readonlyではないstatic変数にアクセスできないため、 // CountLogWrapperを経由してカウントする CountLogWrapper.Write(); } finally { messageBuffer.Length = 0; } } } /// <summary> /// ログが出力された回数をカウントするクラス /// </summary> internal static class CountLogWrapper { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void WriteDelegate(); private struct CountLogWrapperKey { } private static bool _initialized; internal static void Initialize() { if (_initialized) return; _initialized = true; // デリゲートを登録する Burst2ManagedCall<WriteDelegate, CountLogWrapperKey>.Init(WriteFunc); } [AOT.MonoPInvokeCallback(typeof(WriteDelegate))] private static void WriteFunc() { // カウント処理 CountSinkSystem.LogCounter++; } public static void Write() { var ptr = Burst2ManagedCall<WriteDelegate, CountLogWrapperKey>.Ptr(); #if LOGGING_USE_UNMANAGED_DELEGATES ((delegate * unmanaged[Cdecl] <LogLevel, byte*, int, void>)ptr.Value)(level, data, length); #else ptr.Invoke(); #endif } } /// <summary> /// BurstからManagedコードを呼び出すためのデリゲートを管理するクラス /// </summary> internal static class Burst2ManagedCall<T, Key> { private static T _delegate; private static readonly SharedStatic<FunctionPointer<T>> _sharedStatic = SharedStatic<FunctionPointer<T>>.GetOrCreate<FunctionPointer<T>, Key>(16); public static bool IsCreated => _sharedStatic.Data.IsCreated; public static void Init(T @delegate) { CheckIsNotCreated(); _delegate = @delegate; _sharedStatic.Data = new FunctionPointer<T>(Marshal.GetFunctionPointerForDelegate(_delegate)); } public static ref FunctionPointer<T> Ptr() { CheckIsCreated(); return ref _sharedStatic.Data; } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")] private static void CheckIsCreated() { if (IsCreated == false) throw new InvalidOperationException("Burst2ManagedCall was NOT created!"); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")] private static void CheckIsNotCreated() { if (IsCreated) throw new InvalidOperationException("Burst2ManagedCall was already created!"); } } }