2

I am trying to setup the native logger to call a C# function. Since logging in the native side is setup using a singleton (log4cpp), I need to make sure to setup the listener early on (before any other native call). So I decided to follow advice from:

Thus I have written the following static class (to act as my singleton):

internal static class MyNativeService
{
  private const string NativeLibraryName = "my.so.0";
  private static Delegate _logDelegate; // to prevent garbage collection of C# delegate
  static void LogMessage(int logLevel, string loggerName, string message)
  {
    Console.WriteLine($"{logLevel} - {loggerName} - {message}");
  }

  static MyNativeService()
  {
    _logDelegate = new LogDelegate(LogMessage);
    IntPtr ptr = Marshal.GetFunctionPointerForDelegate(_logDelegate);
    // my_listener_configure should be called before any other native calls (only once)
    my_listener_configure(ptr);
  }

  [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
  internal delegate void LogDelegate(int logLevel,
    [MarshalAs(UnmanagedType.LPUTF8Str)] string loggerName, 
    [MarshalAs(UnmanagedType.LPUTF8Str)] string logMessage);

  [DllImport(NativeLibraryName, CallingConvention = CallingConvention.Cdecl)]
  private static extern void my_listener_configure(IntPtr aCallback);
}

This works as expected, but this is not exactly what I want. I would prefer to forward the message to the actual ILogger framework (Microsoft.Extensions.Logging). So how should I refactor the above code so that my LogMessage becomes:

using Microsoft.Extensions.Logging;
internal class MyNativeLoggerListener
{
  private readonly ILogger<MyNativeLoggerListener> _logger; // created somehow at startup
  private void LogMessage(int logLevel, string loggerName, string message)
  {
    _logger.Log((LogLevel)logLevel, 0, $"{loggerName} - {message}", null, null);
  }
}

Since I cannot mix dependency injection and static constructor, how and when can I create my singleton in my C# application to setup forwarding of native message into the properly configured logger (defined in the application) ?


Another naive solution, would be to create a logger in the Startup class, so that:

namespace Acme
{
    public class Startup
    {
        private readonly ILogger<Startup> _logger;
        private readonly Delegate _logDelegate; // to prevent garbage collection of C# delegate
        public void LogMessage(int logLevel, string loggerName, string message)
        {
            _logger.Log((LogLevel)logLevel, 0, message, null, null);
        }

        public Startup(IConfiguration configuration, ILogger<Startup> logger)
        {
            Configuration = configuration;
            _logger = logger;
            _logDelegate = MyNativeService.my_listener_configure(LogMessage);
        }

Of course, this wont work since the ILogger mechanism has not been setup yet. The above throw a:

System.InvalidOperationException: Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger`1[Acme.Startup]' while attempting to activate 'Acme.Startup'.

For reference, here is the pattern I followed:

malat
  • 12,152
  • 13
  • 89
  • 158

1 Answers1

1

Static contexts don't fit well on dependency injection contexts.

You have some options though:

A)

Just use LoggerFactory and create your logger. In the example bellow I create a Console logger. You can then set the created logger int the static class (or encapsulate its construction) like:

using (var loggerFactory = LoggerFactory.Create(o => o.SetMinimumLevel(LogLevel.Trace).AddConsole()))
{
    MyNativeService.Logger = loggerFactory.CreateLogger("foo");
}

B)

If you still want to get something from the DI container, create the logger using application services provider in Startup class like this:

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

// Ommitted ConfigureServices... [...]

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            // startup code [...]

            var loggerFactory = app.ApplicationServices.GetService<ILoggerFactory>();
            var logger = loggerFactory.CreateLogger("foo"); // You can give your own name or nameof(MyNativeService), for instance
            
            MyNativeService.Logger = logger;
        }
    }

C) You can also set a Services property in the static class, so you have access to the full set of DI container services like:

 public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
    // Ommitted ConfigureServices... [...]
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                // startup code [...]    
               
                MyNativeService.Services = app.ApplicationServices;
            }
        }

and your LogMessage implementation could be:

public static void LogMessage(int logLevel, string loggerName, string message)
   {
      var loggerFactory = Services.GetService<ILoggerFactory>();

      var logger = loggerFactory.CreateLogger(loggerName);
      logger.Log((LogLevel)logLevel, 0, $"{loggerName} - {message}");

   }

You won't get rid of some initialization code. In all these cases you may want to protect your static class from being called before initialization.

natenho
  • 5,231
  • 4
  • 27
  • 52