Logging
libuiohook can log messages throughout its execution. By default the messages are not logged anywhere, but you can get
these logs by using UioHook.SetLoggerProc
, or the ILogSource
interface and its default implementation, LogSource
.
libuiohook logs contain the log level (debug, info, warning, error), message format, and message arguments.
The message structure is the following:
function [line]: message
function
is the function name in the libuiohook source code, and line
is the source code line.
Using High-Level Types
The easiest way to subscribe to libuiohook's logs is to use the LogSource
class and its interface – ILogSource
. The
interface contains the MessageLogged
event, and extends IDisposable
. Calling the Dispose
method will stop the log
source from receiving the logs. The IsDisposed
property is also available.
LogSource
also contains the MinLevel
property which can be set to filter log messages by level. It's not recommended
to use the debug level for long periods of time since a debug message is logged for every single input event.
Here's a usage example:
using SharpHook.Data;
using SharpHook.Logging;
// ...
var logSource = LogSource.RegisterOrGet(minLevel: LogLevel.Info);
logSource.MessageLogged += this.OnMessageLogged;
private void OnMessageLogged(object? sender, LogEventArgs e) =>
this.logger.Log(this.AdaptLogLevel(e.LogEntry.Level), e.LogEntry.FullText);
You can get an instance of LogSource
by using the static RegisterOrGet
method. Subsequent calls to this method will
return the same registered instance. You can dispose of a log source to stop receiving libuiohook messages. After that,
calling RegisterOrGet
will register a new instance.
The MessageLogged
event contains event args of type LogEventArgs
which contains just one property of type
LogEntry
. This class contains the actual log message.
The simplest way to use LogEntry
is to use its Level
and FullText
properties. FullText
is created using the log
message format and arguments so you don't have to do it yourself.
SharpHook.Reactive contains the IReactiveLogSource
and its implementation – ReactiveLogSourceAdapter
. Here's a
usage example:
using SharpHook.Logging;
using SharpHook.Native;
using SharpHook.Reactive.Logging;
// ...
var logSource = LogSource.RegisterOrGet(minLevel: LogLevel.Info);
var reactiveLogSource = new ReactiveLogSourceAdapter(logSource);
reactiveLogSource.MessageLogged.Subscribe(this.OnMessageLogged);
IReactiveLogSource
is basically the same as ILogSource
, but MessageLogged
is an observable of LogEntry
instead
of an event. ReactiveLogSourceAdapter
adapts an ILogSource
to the IReactiveLogSource
interface. A default
scheduler can be set for the MessageLogged
observable.
SharpHook.R3 contains the IR3LogSource
and its implementation – R3LogSourceAdapter
. Here's a usage example:
using SharpHook.Logging;
using SharpHook.Native;
using SharpHook.R3.Logging;
// ...
var logSource = LogSource.RegisterOrGet(minLevel: LogLevel.Info);
var reactiveLogSource = new R3LogSourceAdapter(logSource);
reactiveLogSource.MessageLogged.Subscribe(this.OnMessageLogged);
IR3LogSource
is basically the same as ILogSource
, but MessageLogged
is an observable of LogEntry
instead of an
event. R3LogSourceAdapter
adapts an ILogSource
to the IR3LogSource
interface. A default time provider can be set
for the MessageLogged
observable.
Using the Low-Level Functionality
Calling the SetLoggerProc
Method
The logging functionality works by using ILoggingProvider.SetLoggerProc
(which in turn uses UioHook.SetLoggerProc
by
default). This method sets the log callback – a delegate of type LoggerProc
which will be called to log the messages
of libuiohook. LoggerProc
receives the log level, a pointer to the message format, and a pointer to the message
arguments. It also receives user-supplied data as an IntPtr
(which is set in the UioHook.SetLoggerProc
), but you
usually shouldn't use it.
When calling SetLoggerProc
, the function must be wrapped into a delegate reference and the reference must be stored
to prevent garbage collection. This is because the following code:
provider.SetLoggerProc(someObj.SomeMethod, IntPtr.Zero);
is actually transformed into this code by the C# compiler:
provider.SetLoggerProc(new LoggerProc(someObj.SomeMethod), IntPtr.Zero);
The CLR protects the LoggerProc
reference from being garbage-collected only until the SetLoggerProc
methods exits
(which happens almost instantly). The CLR does not and cannot know that the reference will be used later and so it will
happily collect this reference thinking it's not needed anymore. Instead, the following should be done:
LoggerProc loggerProc = someObj.SomeMethod; // This reference should be stored, e.g., as a field of the object
provider.SetLoggerProc(loggerProc, IntPtr.Zero);
Additionally, if you need to support Mac Catalyst, there are two conditions that the logger callback must satisfy:
- The method must be
static
- The method must be decorated with the
[ObjCRuntime.MonoPInvokeCallback(typeof(LoggerProc))]
attribute
Using LogEntryParser
It is highly recommended to use LogEntryParser
in order to create a log entry out of the pointers to the message
format and arguments. This way you won't have to handle these pointers directly. The problem with handling them directly
is that the log callback receives a variable number of arguments. In C# you can use the params
keyword for that, but
native functions do that in an entirely different way, and .NET doesn't have a default way to handle that (there is an
undocumented __arglist
keyword, but it can't be used in delegates and callbacks). LogEntryParser
handles all that -
its code is based on the log handling code of LibVLCSharp. Basically it calls
the native vsprintf
function from the C runtime and lets it deal with formatting the log message with native variable
arguments. It then parses the log message and the log format and extracts the arguments.
If you want to use your own callback, then its form should be the following:
private void OnLog(LogLevel level, IntPtr userData, IntPtr format, IntPtr args)
{
// Filter by log level if needed
var logEntry = LogEntryParser.Instance.ParseNativeLogEntry(level, format, args);
// Handle the log entry instead of the native format and arguments
}
Advanced Usage
If you use structured logging, then you may want to use the message format and arguments directly, instead of using the
formatted result. LogEntry
contains properties which can help you with that:
Format
– the format of the log message which can be passed toString.Format
.RawFormat
– the raw native format of the log message (which uses argument placeholders for C'sprintf
function).Arguments
– the strongly-typed arguments of the log message.RawArguments
– the arguments of the log message as they appear in the formatted log message.ArgumentPlaceholders
– the placeholders extracted from the native log format (e.g.%d
for a number).
String.Format(entry.Format, entry.RawAguments.ToArray())
is equal to entry.FullText
.
String.Format(entry.Format, entry.Aguments.ToArray())
is not necessarily equal to entry.FullText
since some
formatting information is discarded, but using Arguments
instead of RawArguments
is better suited for structured
logging.
Arguments
contains parsed message arguments which can be of one of the types listed below, according to the argument
placeholders. Only the specifier and length are considered (see the C's printf
docs for reference).
Type | Placeholder |
---|---|
int |
%d , %i |
sbyte |
%hhd , %hhi |
short |
%hd , %hi |
long |
%ld , %li , %lld , %lli , %jd , %ji
|
uint |
%u , %o , %x , %X |
byte |
%hhu , %hho , %hhx , %hhX |
ushort |
%hu , %ho , %hx , %hX |
ulong |
%lu , %lo , %lx , %lX , %llu , %llo ,
%llx , %llX , %ju , %jo , %jx , %jX
|
double |
%f , %F , %e , %E , %g , %G |
decimal |
%Lf , %LF , %Le , %LE , %Lg , %LG |
char |
%c |
nint |
%p |
string |
Any other placeholder, including %s |
The %a
, %A
, and %n
specifiers are not supported, as well as length z
and t
.
LogEntry
also contains the Function
and SourceLine
properties. These are the first two arguments of the log
message – the function name in the libuiohook source code, and the source code line.