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'sprintffunction).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.%dfor 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.