none
P/invoke CALLBACK DLL from C# RRS feed

  • Question

  • Hi to all,

    For logging purposes I need to implement a general callback function in my c++ dll. Messages will be returned to my C# app using the P/Invoke method.

    I already implemented something already working:

    DLL

    //----GlobalLog.h----
    #pragma once
    
    #ifndef GLOBALLOG_H
    #define GLOBALLOG_H
    
    typedef void (__stdcall * Callback)(const char* text);
    extern void Log(const char* msg);
    
    extern "C" __declspec(dllexport)
    	extern void __stdcall SetGlobalLogCallback(Callback handler);
    
    #endif
    
    //----GlobalLog.cpp----
    #include "stdafx.h"
    #include "GlobalLog.h"
    
    Callback Handler = 0;
    void __stdcall SetGlobalLogCallback(Callback handler)
    {
    	Handler = handler;
    }
    
    void Log(const char* msg)
    {
    	Handler(msg);
    }
    
    //----Main.cpp---- (Test function)
    extern "C" __declspec(dllexport) int LogTest()
    {
    	Log("Test message!");
    	return 0;
    }
    


    C#

    [DllImport("MyDLL.dll")]
    private static extern int LogTest();
    
    static void Main(string[] args)
            {
                GlobalLog Log = new GlobalLog();
                Log.EnableLog();
                LogTest();
                Console.ReadKey();
            }
    
    class GlobalLog
        {
            [DllImport("MyDLL.dll")]
            private static extern void SetGlobalLogCallback(Callback cb);
            
            private delegate void Callback(string text);
            private Callback CallbackInstance;
    
            public void EnableLog()
            {
    	        //Ensure it doesn't get garbage collected
    	        CallbackInstance = new Callback(Handler);
    	        SetGlobalLogCallback(CallbackInstance);
            }
    
            //Message returned from DLL
            private void Handler(string text)
            {
    	        Console.WriteLine(text);
            }
        
        }
    

    I have two main questions (for now!):

    1 - Is there something I'm missing in the code above ? I'm worried about the memory and I feel that I still need some addition in order to avoid memory leaks or crashes.

    2 - Supposing that on the dll side I have an enum like the following:

    typedef enum {
    	SUCCESS = 0,
    	OUT_OF_MEMORY = -5,
    	INVALID_INPUT = -6,
    	TIMEOUT = -9,
    	UNKNOWN_ERROR = -55
    } errors_t;
    
    

    How it is possible to return my custom message + an enum value (as string).

    For example something like:

    errors_t myerror;
    myerror = Function();
    Log("The following error occurred: " + myerror);
    //Expected message returned from DLL: "The following error occurred: OUT_OF_MEMORY"
    

     

    Thanks to all

    Friday, February 3, 2012 12:31 AM

Answers

  • Hello mark_555,

    >> 2- A function that returns strings according to the enum value works fine, but I have several enums and some of them only store a few values. Many other will be added in the future and create two different functions for each of them it's not practical. Something like "EnumValue.ToStr()" it's like a dream, but i would want at least one only solution that will work with every enum instead of a dedicated solution for every enum.

    1. You would have to ensure that the individual enums have unique integral values. One way is to ensure that each value from a specific enum starts from a designated base, e.g. :

    typedef enum {
    	SUCCESS = 0,
    	OUT_OF_MEMORY = -5,
    	INVALID_INPUT = -6,
    	TIMEOUT = -9,
    	UNKNOWN_ERROR = -55
    } errors_t;
    
    typedef enum {
    	errors2_base = -1000,
    	errors2_1 = errors2_base - 1,
    	errors2_2 = errors2_base - 2,
    	errors2_3 = errors2_base - 3
    } errors2;
    
    typedef enum {
    	errors3_base = -2000,
    	errors3_1 = errors3_base - 1,
    	errors3_2 = errors3_base - 2,
    	errors3_3 = errors3_base - 3
    } errors3;
    
    

    2. The function that returns a string form of the error values would have to take an integer parameter instead of any specific enum type, e.g. :

    const char* FunctionGeneric(int et)
    {
    	switch (et)
    	{
    		case SUCCESS :
    		{
    			return "SUCCESS";
    			break;
    		}
    		
    		case OUT_OF_MEMORY :
    		{
    			return "OUT_OF_MEMORY";
    			break;
    		}
    		
    		case INVALID_INPUT :
    		{
    			return "INVALID_INPUT";
    			break;
    		}
    
    		case TIMEOUT :
    		{
    			return "TIMEOUT";
    			break;
    		}
    		
    		case UNKNOWN_ERROR :
    		{
    			return "UNKNOWN_ERROR";
    			break;
    		}
    		
    		case errors2_1 :
    		{
    			return "errors2_1";
    			break;
    		}
    		
    		case errors2_2 :
    		{
    			return "errors2_2";
    			break;
    		}
    		
    		case errors2_3 :
    		{
    			return "errors2_3";
    			break;
    		}
    		
    		case errors3_1 :
    		{
    			return "errors3_1";
    			break;
    		}
    		
    		case errors3_2 :
    		{
    			return "errors3_2";
    			break;
    		}
    		
    		case errors3_3 :
    		{
    			return "errors3_3";
    			break;
    		}								
    		
    		default :
    		{
    			return "";
    			break;
    		}						
    	}
    }
    
    

    3. And the Log() function that uses FunctionGeneric() would have to use int instead of any specific enum type :

    //Overloaded log function. Accept any custom message + an error value
    void Log(const char* msg, /*errors_t*/ int error)
    {
    	char NewMsg[256]; //256 as lenght should be enough to store the full message
    	sprintf_s(NewMsg, 256, msg, FunctionGeneric(error));
    	Handler(NewMsg);
    }
    <br/><br/>
    

    4. Whether you implement FunctionGeneric() the way I've shown or use an entirely different approach (e.g. member functions of a class) is entirely according to preference. The bottom line is that the error values cannot overlap in value and that the "int" type must be used instead of any specific enum type.

    5. These questions are C++-related. I suggest that you bring them to the C++ forums for more specialized support :

    http://social.msdn.microsoft.com/Forums/en/vcgeneral/threads

    http://social.msdn.microsoft.com/Forums/en/vclanguage/threads

    - Bio.

     

     


    Please visit my blog : http://limbioliong.wordpress.com/
    • Marked as answer by mark_555 Thursday, February 9, 2012 8:24 PM
    Sunday, February 5, 2012 5:04 AM

All replies

  • Hello mark_555,

    >> 1 - Is there something I'm missing in the code above ? I'm worried about the memory and I feel that I still need some addition in order to avoid memory leaks or crashes.

    1. The main point of concern whenever you want to pass a delegate to unmanaged code in order to serve as a callback function is Garbage Collection.

    2. In this regard, your code is actually fine because "CallbackInstance" is a member object of the GlobalLog class which means that an instance of it will stay alive as long as its owning GlobalLog instance stays alive.

    3. To demonstrate the effects of garbage collection, use the following code :

    class GlobalLog
    {
        [DllImport("MyDLL.dll")]
        private static extern void SetGlobalLogCallback(Callback cb);
    
        private delegate void Callback(string text);
        // Do not make "CallbackInstance" a member of GlobalLog.
        //private Callback CallbackInstance;
    
        public void EnableLog()
        {
            // Make "CallbackInstance" a function variable.
            Callback CallbackInstance = new Callback(Handler);
    
            SetGlobalLogCallback(CallbackInstance);
        }
    
        //Message returned from DLL
        private void Handler(string text)
        {
            Console.WriteLine(text);
        }
    }
    
    static void Main(string[] args)
    {
        GlobalLog Log = new GlobalLog();
        Log.EnableLog();
    
        // Perform Garbage Collection.
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    
        LogTest();
        Console.ReadKey();
    }
    
    

    3.1 The code will demonstrate that if "CallbackInstance" is a function local variable, then it is easily garbage collected.

    3.2 After garbage collection has been activated in the Main() function (via calls to GC.Collect()), the LogTest() call will result in an exception.

    4. I will post again to provide a long term solution using the GCHandle class.

     

    - Bio.

     


    Please visit my blog : http://limbioliong.wordpress.com/
    Friday, February 3, 2012 10:26 AM
  • Hello mark_555,

    1. I'm continuing from my previous post and am now providing a solution to the garbage collection problem that was demonstrated in the example code (of the previous post).

    2. As mentioned, an important problem when transferring a delegate to unmanaged code (to serve as a callback) is garbage collection.

    3. Garbage collection can occur because once a managed object gets passed to unmanaged code, .NET reference counting is not effected. 

    4. To ensure that the "CallbackInstance" local variable (in the EnableLog() function) remains locked in memory and is not garbage collected when it is invoked from unmanaged code, always use a GCHandle to lock it. The following example code demonstrates it :

    class GlobalLog
    {
        [DllImport("MyDLL.dll")]
        private static extern void SetGlobalLogCallback(Callback cb);
    
        private delegate void Callback(string text);
        //private Callback CallbackInstance;
    
        // Define a GCHandle member to lock 
        // the "CallbackInstance" local variable 
        // in memory without being Garbage Collected.
        private GCHandle gch;
    
        public void EnableLog()
        {
            //Ensure it doesn't get garbage collected
            Callback CallbackInstance = new Callback(Handler);
    
            // Use the member GCHandle to hold the 
            // delegate object in memory.
            gch = GCHandle.Alloc(CallbackInstance);
    
            SetGlobalLogCallback(CallbackInstance);
        }
    
        // Provide a function to free the GCHandle.
        public void DisableLog()
        {
            // Free the GCHandle.
            gch.Free();
        }
    
        //Message returned from DLL
        private void Handler(string text)
        {
            Console.WriteLine(text);
        }
    }
    
    static void Main(string[] args)
    {
        GlobalLog Log = new GlobalLog();
        Log.EnableLog();
    
        // Perform Garbage Collection.
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    
        LogTest();
        Console.ReadKey();
    
        // Free the GCHandle when it is
        // no longer needed.
        Log.DisableLog();
    }
    

    4.1 What happens here is that we have defined a member GCHandle object "gch".

    4.2 In the EnableLog() function we allocate the GCHandle to lock on "CallbackInstance". This will prevent "CallbackInstance" from being garbage collected until we call GCHandle.Free() on "gch".

    4.3 We must also provide a DisableLog() function the purpose of which is to call GCHandle.Free() on "gch".

    4.4 Inside the Main() function, when we deem that the "CallbackInstance" delegate will no longer be invoked from unmanaged code (e.g. after Console.ReadKey()), we call Log.DisableLog().

    5. For your current code, I recommend that you :

    • Let "CallbackInstance" remain a member object.
    • Define the GCHandle member object "gch" for the GlobalLog class and allocate it inside EnableLog() (to lock on "CallbackInstance") just like the example code above.
    • Provide a DisableLog() function to call GCHandle.Free() on "gch" when callbacks will no longer be invoked.

     

    - Bio.

     


    Please visit my blog : http://limbioliong.wordpress.com/
    Friday, February 3, 2012 11:04 AM
  • Hello mark_555,

    >> 2 - Supposing that on the dll side I have an enum like the following:...

    1. The only way to convert an errors_t enum value into its string equivalent is to provide a function that returns strings according to the errors_t enum value.

    2. The following is an example of such a function (Function()) and a sample calling code (LogTest()) :

    const char* Function(errors_t et)
    {
    	switch (et)
    	{
    		case SUCCESS :
    		{
    			return "SUCCESS";
    			break;
    		}
    		
    		case OUT_OF_MEMORY :
    		{
    			return "OUT_OF_MEMORY";
    			break;
    		}
    		
    		case INVALID_INPUT :
    		{
    			return "INVALID_INPUT";
    			break;
    		}
    
    		case TIMEOUT :
    		{
    			return "TIMEOUT";
    			break;
    		}
    		
    		case UNKNOWN_ERROR :
    		{
    			return "UNKNOWN_ERROR";
    			break;
    		}
    		
    		default :
    		{
    			return "";
    			break;
    		}						
    	}
    }
    
    
    
    
    
    extern "C" __declspec(dllexport) int LogTest()
    {
    	char szMessage[256];
    	sprintf_s (szMessage, sizeof(szMessage), "The following error occurred: %s", Function(OUT_OF_MEMORY));
    	Log(szMessage);
    	return 0;
    }
    
    


    - Bio.

     


    Please visit my blog : http://limbioliong.wordpress.com/
    • Edited by Lim Bio Liong Sunday, February 5, 2012 2:32 AM corrected silly typo error.
    Friday, February 3, 2012 11:44 AM
  • Hi Bio and many thanks for your professional and detailed answer.
    So, the most important thing I have to consider is the garbage collection, a problem that I don't have with the actual code because I keep a reference to the object.
    Involving the GCHandle member with the free function seems to be required only when I don't need anymore any callback.
    My original intention was to leave the callback enabled for the entire life of the application. Enabling/disabling the log callback everytime I need to call a dll function does not seems much pratical. I will have to involve GCHandle everytime I call a function and if the only one advantage I can gain it's just a few more Kilobytes of memory, at this point I prefer to renounce and leave the callback always enabled, expecially because I cannot predict the quantity and frequency of calls.
    I tried your technique for the enum, but when the sprintf_s function return, I receive a debug assertion error with the following message: Expression: ("Buffer too small", 0).
    Even If increase the buffer I still get the same error.
    Are you sure that creating a buffer is really required ? In the past I tried something like that and even if it was working, I ended up with memory leaks (even after calling the free method).
    I don't like the idea to write two additional lines of code everytime I call the log function, but I don't think this is a problem because I can always move them directly inside the log function.
    I could use the same log function and add an additional parameter. If parameter is null, the callback will return only the main message, otherwise it will return the main message along with the parameter (the parameter could be the error enum or just a custom value).
    Thanks
    Saturday, February 4, 2012 6:14 PM
  • Hi Bio,
    I figured out why your enum method was resulting in a debug assertion error. It was wrong the second parameter. The sizeof(256) operator return the size of a pointer, 4 in our case. Of course,  the size was not enough to store data. After replacing of "sizeof(256)" with "256", the function worked as expected.
    I implemented the whole thing in the following way:
    
    
    //Base log function. Accept any custom message as parameter
    void Log(const char* msg)
    {
    	Handler(msg);
    }
    
    //Overloaded log function. Accept any custom message + an errors_t enum value
    void Log(const char* msg, errors_t error)
    {
    	char NewMsg[256]; //256 as lenght should be enough to store the full message
    	sprintf_s(NewMsg, 256, msg, Function(error));
    	Handler(NewMsg);
    }
    
    //Another overloaded log function. Accept any custom message + another message (const char* data type). Useful when I want to include a variable value in the message)
    void Log(const char* msg, const char* arg)
    {
    	char NewMsg[256]; //256 as lenght should be enough to store the full message
    	sprintf_s(NewMsg, 256, msg, arg);
    	Handler(NewMsg);
    }
    
    //Sample 1:
    Log("Sample message!");
    
    //Sample 2:
    const char* var1 = "VariableValue";
    Log("Sample message! %s", var1);
    
    //Sample 3:
    errors_t myerror;
    myerror = AnyFunction();
    if (myerror != SUCCESS)
    {
       Log("The following error occurred: %s", myerror);
    }
    
    
    All seems to work fine, but I have two questions:
    1- Is there something I should do regarding memory expecially in the functions where "char NewMsg[256];" is created? Or I can just leave all as is ?
    2- A function that returns strings according to the enum value works fine, but I have several enums and some of them only store a few values. Many other will be added in the future and create two different functions for each of them it's not practical. Something like "EnumValue.ToStr()" it's like a dream, but i would want at least one only solution that will work with every enum instead of a dedicated solution for every enum.
    Many thanks
    Saturday, February 4, 2012 11:48 PM
  • Hello mark_555,

    >> I figured out why your enum method was resulting in a debug assertion error. It was wrong the second parameter. The sizeof(256) operator return the size of a pointer, 4 in our case. Of course, the size was not enough to store data...

    I am so sorry ! What a silly mistake. I have since edited my post to correct the typo. Thanks for pointing it out.

    I have corrected the code in my post to :

    extern "C" __declspec(dllexport) int LogTest()
    {
    	char szMessage[256];
    	sprintf_s (szMessage, sizeof(szMessage), "The following error occurred: %s", Function(OUT_OF_MEMORY));
    	Log(szMessage);
    	return 0;
    }
    

    Note that instead of sizeof(256), I have changed this to "sizeof(szMessage)".

    I will return to your other comments in a while...

    - Bio.

     

     


    Please visit my blog : http://limbioliong.wordpress.com/
    • Edited by Lim Bio Liong Sunday, February 5, 2012 2:34 AM Added code.
    Sunday, February 5, 2012 2:31 AM
  • Hello mark_555,

    >> 1- Is there something I should do regarding memory expecially in the functions where "char NewMsg[256];" is created? Or I can just leave all as is ?

    Just make sure that its size is generally large enough for all built up messages.

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/
    Sunday, February 5, 2012 4:22 AM
  • Hello mark_555,

    >> 2- A function that returns strings according to the enum value works fine, but I have several enums and some of them only store a few values. Many other will be added in the future and create two different functions for each of them it's not practical. Something like "EnumValue.ToStr()" it's like a dream, but i would want at least one only solution that will work with every enum instead of a dedicated solution for every enum.

    1. You would have to ensure that the individual enums have unique integral values. One way is to ensure that each value from a specific enum starts from a designated base, e.g. :

    typedef enum {
    	SUCCESS = 0,
    	OUT_OF_MEMORY = -5,
    	INVALID_INPUT = -6,
    	TIMEOUT = -9,
    	UNKNOWN_ERROR = -55
    } errors_t;
    
    typedef enum {
    	errors2_base = -1000,
    	errors2_1 = errors2_base - 1,
    	errors2_2 = errors2_base - 2,
    	errors2_3 = errors2_base - 3
    } errors2;
    
    typedef enum {
    	errors3_base = -2000,
    	errors3_1 = errors3_base - 1,
    	errors3_2 = errors3_base - 2,
    	errors3_3 = errors3_base - 3
    } errors3;
    
    

    2. The function that returns a string form of the error values would have to take an integer parameter instead of any specific enum type, e.g. :

    const char* FunctionGeneric(int et)
    {
    	switch (et)
    	{
    		case SUCCESS :
    		{
    			return "SUCCESS";
    			break;
    		}
    		
    		case OUT_OF_MEMORY :
    		{
    			return "OUT_OF_MEMORY";
    			break;
    		}
    		
    		case INVALID_INPUT :
    		{
    			return "INVALID_INPUT";
    			break;
    		}
    
    		case TIMEOUT :
    		{
    			return "TIMEOUT";
    			break;
    		}
    		
    		case UNKNOWN_ERROR :
    		{
    			return "UNKNOWN_ERROR";
    			break;
    		}
    		
    		case errors2_1 :
    		{
    			return "errors2_1";
    			break;
    		}
    		
    		case errors2_2 :
    		{
    			return "errors2_2";
    			break;
    		}
    		
    		case errors2_3 :
    		{
    			return "errors2_3";
    			break;
    		}
    		
    		case errors3_1 :
    		{
    			return "errors3_1";
    			break;
    		}
    		
    		case errors3_2 :
    		{
    			return "errors3_2";
    			break;
    		}
    		
    		case errors3_3 :
    		{
    			return "errors3_3";
    			break;
    		}								
    		
    		default :
    		{
    			return "";
    			break;
    		}						
    	}
    }
    
    

    3. And the Log() function that uses FunctionGeneric() would have to use int instead of any specific enum type :

    //Overloaded log function. Accept any custom message + an error value
    void Log(const char* msg, /*errors_t*/ int error)
    {
    	char NewMsg[256]; //256 as lenght should be enough to store the full message
    	sprintf_s(NewMsg, 256, msg, FunctionGeneric(error));
    	Handler(NewMsg);
    }
    <br/><br/>
    

    4. Whether you implement FunctionGeneric() the way I've shown or use an entirely different approach (e.g. member functions of a class) is entirely according to preference. The bottom line is that the error values cannot overlap in value and that the "int" type must be used instead of any specific enum type.

    5. These questions are C++-related. I suggest that you bring them to the C++ forums for more specialized support :

    http://social.msdn.microsoft.com/Forums/en/vcgeneral/threads

    http://social.msdn.microsoft.com/Forums/en/vclanguage/threads

    - Bio.

     

     


    Please visit my blog : http://limbioliong.wordpress.com/
    • Marked as answer by mark_555 Thursday, February 9, 2012 8:24 PM
    Sunday, February 5, 2012 5:04 AM
  • Hello mark_555,

    By the way, I have previously written a blog on using managed delegates as callbacks invoked from unmanaged code.

    Here is the link if you are interested :

    http://limbioliong.wordpress.com/2011/06/19/delegates-as-callbacks-part-2/

    - Bio.

     


    Please visit my blog : http://limbioliong.wordpress.com/
    Sunday, February 5, 2012 7:08 AM
  • Hi Bio and thanks again for your precious suggestions! I'm learning lots of important things. Thanks for revising the code. It works wonderfully now.

    Good..I was thinking I would had to handle memory even here, but I'm happy to hear from you that I just need to be sure about the buffer size and nothing more.

    Regarding the enum problem, unfortunately the others enums have the same values and therefore I cannot use an int as a general parameter. Do not worry, at the end, the work required is no more than a few minutes and therefore I can implement the old solution.

    Thanks for letting me know about your blog, I just read a few articles and I have just revised my code based on one of them. I have implemented your solution on how to marshall the data returned from unmanaged code into managed strings.
    I have updated my other thread with a sample code and I would really appreciate if you could take a look to see if everything it's ok.

    Regarding this thread I think that I have no more questions because you have been really professional and I have all the info that I was needing.

    Many thanks

    Tuesday, February 7, 2012 2:59 AM