1

I need to communicate with a program from my C# code. I have received a communication library from the company that made the program, along with a quite detailed explanation on how to integrate with the application. I feel I have done everything correctly, the C++ functions all return success statusses, but the most important part, the callback function, is never invoked.

I cannot use [DllImport(...)], I have to use LoadLibrary, GetProcAddress and Marshal.GetDelegateForFunctionPointer to load the library into memory and call it's functions. This is so I keep an instance of the library in memory, so it remembers the callback function. This is because the callback function is registered and then used by subsequent requests to communicate additional information.

I'm keeping a reference to the callback function so the GC doesn't collect it and it isn't available for the C++ code.

When I wait for the callback, nothing happens. I don't get errors, I don't see anything specific happening, it just lets me wait forever. The callback doesn't happen.

I'm hoping that somebody can tell me what I have missed, because I don't see the solution anymore.

class Program
{
    private static Callback keepAlive;
    static void Main(string[] args)
    {
        try
        {
            var comLib = LoadLibrary(@"C:\BIN\ComLib.dll");

            var isInstalledHandle = GetProcAddress(comLib, nameof(IsInstalled));
            var isInstalled = Marshal.GetDelegateForFunctionPointer<IsInstalled>(isInstalledHandle);
            var isInstalledResult = isInstalled("activation-key"); // returns success
            Console.WriteLine("Is installed result: " + isInstalledResult);

            var registerCallbackHandle = GetProcAddress(comLib, nameof(RegisterCallback));
            var registerCallback = Marshal.GetDelegateForFunctionPointer<RegisterCallback>(registerCallbackHandle);
            keepAlive = CallbackFunc;
            var registerResult = registerCallback(keepAlive); // returns success
            Console.WriteLine("Register result: " + registerResult);

            var initHandle = GetProcAddress(comLib, nameof(Init));
            var init = Marshal.GetDelegateForFunctionPointer<Init>(initHandle);
            var initResult = init("ERP_INTEGRATION", "activation-key"); // returns success
            Console.WriteLine("Init result: " + initResult);

            Console.WriteLine("Wait...");
            Console.ReadLine();

            FreeLibrary(comLib);
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }

    private static int CallbackFunc(int id, string data)
    {
        Console.WriteLine($"{id} > {data}");
        return 0;
    }
    
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    private delegate int IsInstalled(string registryKey);

    private delegate int Callback(int id, string data);
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    private delegate int RegisterCallback(Callback callback);

    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    private delegate int Init(string id, string command);

    [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
    public static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpLibFileName);

    [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
    public static extern IntPtr GetProcAddress(IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName);

    [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
    public static extern bool FreeLibrary(IntPtr hModule);
}

EDIT 1: I got back from the developers of the software. Apparently, because I'm doing all the work on the main thread, the app can't call back because the thread is busy. I should offload the app (and the callback method) to another thread so the app is free to call the callback funcion. Unfortunately, I have no idea how to do this.

EDIT 2: On request, I put the code in a WinForms app and the callback works nicely there. I do not fully understand why, except that the callback must happen while a) the main thread is free (waiting for input from the form) or b) it offloads the callback to another thread. I have no idea how to simulate either in a console app.

Ken Bonny
  • 729
  • 1
  • 9
  • 29
  • Where is the c# callback method? I think you registered a c++ callback method so code is running correctly. – jdweng Aug 14 '20 at 09:29
  • The callback method is named `CallbackFunc`. This is passed to the c++ library in the `RegisterCallback` delegate that takes the `CallbackFunc` as a parameter.First there is the check if the program is installed, then I register the callback and then I initialise the application. That's the order the documentation says it should be done, because when the initialisation is done, there will be a callback to the registered function. – Ken Bonny Aug 14 '20 at 10:02
  • So, if I got you correctly, C++ executable should eventually invoke CallbackFunc. Could you actually try debugging it? Or at least add some print? To check if the callable it's trying to invoke isn't null and the callback has been registered properly. – Marek Piotrowski Aug 14 '20 at 11:24
  • I can't, I don't have access to the C++ library. I can debug my app, but the C++ library is delivered as is. – Ken Bonny Aug 14 '20 at 11:41
  • I don't understand your reason for using `LoadLibrary` manually. If your problem is that a function pointer that you pass to C++ gets garbage collected prematurely, then it's a known problem with a [known solution](https://stackoverflow.com/a/6193914/11683). If your problem is that the library gets unloaded on its own, then no, that does not happen. – GSerg Aug 14 '20 at 11:43
  • @GSerg Thanks for pointing that out. I was not aware. My problem not that the callback function gets garbage collected, it's that the library I'm calling can't execute the callback because it is blocked. My main thread is waiting for the `Console.ReadLine()` and thus can't invoke the callback. – Ken Bonny Aug 14 '20 at 11:59
  • So you are getting yo the method. The data is probably not garbage. It is not managed memory so you have to move from unmanaged to managed. The memory has to be in global space (or allocated) so when you leave the c++ code the memory is not disposed. – jdweng Aug 14 '20 at 13:17
  • @jdweng And I need to make sure the callback can happen on a different thread, because the app can't invoke the callback function because the main thread of my app is busy. – Ken Bonny Aug 14 '20 at 14:11
  • Callback are automatically a separate process. – jdweng Aug 14 '20 at 15:09
  • @jdweng You mean that it can always call back and that the calling app isn't blocked by my main thread? – Ken Bonny Aug 14 '20 at 15:11
  • 1
    Event including Callback are a separate process. Threads are also a separate process. – jdweng Aug 14 '20 at 17:46
  • 1
    @jdweng That is completely wrong. Callbacks are absolutely not "automatically a separate process", and threads are most certainly not a separate process. C++ callbacks have nothing to do with C# events either. – GSerg Aug 15 '20 at 09:06

2 Answers2

1

I found an answer that is a little hacky, but it works. I converted my console application to WinForms application. This gives me access to System.Windows.Forms.Application.DoEvents. That allows me to start up a Task which runs out of process and keeps refreshing the events that need to be handled. This allows the external program to invoke the callback function.

private bool _callbackOccured = false;
static async Task Main(string[] args)
{
    try
    {
        var comLib = LoadLibrary(@"C:\BIN\ComLib.dll");

        await Task.Run(async () => {
            var isInstalledHandle = GetProcAddress(comLib, nameof(IsInstalled));
            var isInstalled = Marshal.GetDelegateForFunctionPointer<IsInstalled>(isInstalledHandle);
            var isInstalledResult = isInstalled("activation-key"); // returns success
            Console.WriteLine("Is installed result: " + isInstalledResult);

            var registerCallbackHandle = GetProcAddress(comLib, nameof(RegisterCallback));
            var registerCallback = Marshal.GetDelegateForFunctionPointer<RegisterCallback>(registerCallbackHandle);
            keepAlive = CallbackFunc;
            var registerResult = registerCallback(keepAlive); // returns success
            Console.WriteLine("Register result: " + registerResult);

            var initHandle = GetProcAddress(comLib, nameof(Init));
            var init = Marshal.GetDelegateForFunctionPointer<Init>(initHandle);
            var initResult = init("ERP_INTEGRATION", "activation-key"); // returns success
            Console.WriteLine("Init result: " + initResult);

            while (!_callbackOccured)
            {
                await Task.Delay(100);
                Appliction.DoEvents();
            }

            FreeLibrary(comLib);
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
    }
}

private static int CallbackFunc(int id, string data)
{
    Console.WriteLine($"{id} > {data}");
    _callbackOccured = true;
    return 0;
}

Edit: it didn't fully work with Task.Run, I had to use an actual Thread.Start to get it to work correctly. A Task got killed too quickly and I still had the same problem that the external app was closed before it got to do anything usefull, but with a Thread it works as expected.

Ken Bonny
  • 729
  • 1
  • 9
  • 29
0

see answer to this: C# C++ Interop callback. Try converting your delegate to a native function pointer before you pass it to the library.

cudima
  • 414
  • 4
  • 11