Table of Contents

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.Logging;
using SharpHook.Native;

// ...

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.

Using the Low-Level Functionality

The logging functionality works by using UioHook.SetLoggerProc. 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 a pointer to user-supplied data (which is set in the UioHook.SetLoggerProc), but you shouldn't ever use that.

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 to String.Format.
  • RawFormat - the raw native format of the log message (which uses argument placeholders for C's printf 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
IntPtr %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.