none
P/Invoke - C++ callback from C# RRS feed

  • Question

  • Hi all,

    I have a problem wrapping the following C++ method;

    const char* Open(LOG* pfLog)  (where) typedef void LOG(const char* sErr)

    trying C#

    public delegate void LogDelegate([MarshalAsAttribute(UnmanagedType.LPStr)] string sErr);

    [DllImport("test.dll")]

    public static extern string Open(LogDelegate d);

    What are the corect signatures and marshalling for these items?

    I am keeping my delegate in a seperate static variable so it shouldnt go out of scope/get GCed.

    I have no access to the c++ dll, so cant make any changes there... :(

    Any ideas?

     

     

     

     

     


    • Edited by S_Banks Thursday, November 3, 2011 12:45 PM typo
    Thursday, November 3, 2011 12:44 PM

Answers

  • Hello S_Banks,

     

    1. Concerning the Callback Function Declaration and the Delegate.

    1.1 The callback function must be declared with __sdtcall calling convention :

    typedef void (__stdcall LOG)(const char* sErr);
    

    The __stdcall convention is expected by the CLR. Otherwise there will be an error when the managed callback function is invoked from unmanaged code.

    1.2 The delegate declaration in C# is correct :

    public delegate void LogDelegate([MarshalAsAttribute(UnmanagedType.LPStr)] string sErr);
    


    2. Concerning the Open() API.

    2.1 I recommend that you use the IntPtr type for the parameter for the delegate :

    [DllImport("test.dll", CallingConvention=CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.LPStr)]
    public static extern string Open(IntPtr d);
    
    

    Note that the Open() API may be declared as using the __cdecl calling convention. No problem here. I assme that this is the convention used by Open() as indicated by the absence of any declaration in the OP (hence __cdecl by default).

    2.2 I also recommend that you use the Marshal.GetFunctionPointerForDelegate() method to convert the callback function into an IntPtr to pass it to Open() :

    static void CallBackFunction(string strParam)
    {
        Console.WriteLine("{0:S}", strParam);
    }
    
    static void DemonstrateCallBack()
    {
        // Create an instance of a LogDelegate delegate for CallBackFunction().
        LogDelegate callback_delegate = new LogDelegate(CallBackFunction);
        // Convert callback_delegate into a function pointer that can be
        // used in unmanaged code.
        IntPtr intptr_delegate = Marshal.GetFunctionPointerForDelegate(callback_delegate);
        // Call the API passing along the function pointer.
        string strReturn = Open(intptr_delegate);
        Console.WriteLine("Returned string : " + strReturn);
    }
    


    2.3 For more information on using Delegates as callback functions from unmanaged code, pls refer to :

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

     

    3. Concerning the Returned String from Open().

    3.1 Take careful note of the returned string from Open().

    3.2 Since Open() returns this string, it is to be considered an "out" value. As such, it is the receiving code (i.e. the C# code) that owns the returned string.

    3.3 This being the case, the CLR will take the returned pointer to C-style string, convert it to a managed string and then free the memory of the buffer of the C-string. Furthermore, the freeing will be done using ::CoTaskMemFree().

    3.4 Hence it is important that the Open() function allocate the memory for the C-string to be returned and it must do so using ::CoTaskMemAlloc(). The following is an example code for Open() that I wrote for illustrative purposes :

    typedef void (__stdcall LOG)(const char* sErr);
    
    const char* __cdecl Open(LOG* pfLog)
    {
        char szSampleString[] = "Hello World";
        ULONG ulSize = strlen(szSampleString) + sizeof(char);
        char* pszReturn = NULL;
    
        pszReturn = (char*)::CoTaskMemAlloc(ulSize);
        // Copy the contents of szSampleString
        // to the memory pointed to by pszReturn.
        strcpy(pszReturn, szSampleString);
        
        // Make the callback.
        if (pfLog)
        {
          pfLog(pszReturn);
        }
        
        // Return pszReturn.
        return pszReturn;  
    }
    

    Please check out with your vendor how Open() allocates the string and if it is meant to be returned and owned by the caller.

    3.5 If the vendor indicates that the string returned by Open() is not meant to be owned by the caller, then the declaration for Open() in C# should be changed to returning an IntPtr instead of a string, e.g.  :

    [DllImport("test.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr Open2(IntPtr d);
    

    Note that I have used another name Open2() for the alternative API.

    3.6 I have written a sample Open2() function for illustrative purposes :

    const char* __cdecl Open2(LOG* pfLog)
    {
        static const char szSampleString[] = "Hello World";
       
        // Make the callback.
        if (pfLog)
        {
          pfLog(szSampleString);
        }
        
        // Return const char*.
        return szSampleString;  
    }
    
    

    3.7 The call to Open2() and the subsequent conversion of the return pointer to a managed string should be done as follows :

    static void DemonstrateCallBack2()
    {
        // Create an instance of a LogDelegate delegate for CallBackFunction().
        LogDelegate callback_delegate = new LogDelegate(CallBackFunction);
        // Convert callback_delegate into a function pointer that can be
        // used in unmanaged code.
        IntPtr intptr_delegate = Marshal.GetFunctionPointerForDelegate(callback_delegate);
        // Call the API passing along the function pointer.
        IntPtr pstrReturn = Open2(intptr_delegate);
        // Manually convert the returned pointer into a managed string.
        // The returned pointer is then left alone.
        string strReturn = Marshal.PtrToStringAnsi(pstrReturn);
        Console.WriteLine("Returned string : " + strReturn);
    }
    

    The returned pointer to string is then left alone since it remains a property of the DLL.

    - Bio.

     


    Please visit my blog : http://limbioliong.wordpress.com/
    • Proposed as answer by Paul Zhou Friday, November 4, 2011 8:21 AM
    • Marked as answer by S_Banks Friday, November 4, 2011 8:45 AM
    Friday, November 4, 2011 4:31 AM

All replies

  • Hello S_Banks,

     

    1. Concerning the Callback Function Declaration and the Delegate.

    1.1 The callback function must be declared with __sdtcall calling convention :

    typedef void (__stdcall LOG)(const char* sErr);
    

    The __stdcall convention is expected by the CLR. Otherwise there will be an error when the managed callback function is invoked from unmanaged code.

    1.2 The delegate declaration in C# is correct :

    public delegate void LogDelegate([MarshalAsAttribute(UnmanagedType.LPStr)] string sErr);
    


    2. Concerning the Open() API.

    2.1 I recommend that you use the IntPtr type for the parameter for the delegate :

    [DllImport("test.dll", CallingConvention=CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.LPStr)]
    public static extern string Open(IntPtr d);
    
    

    Note that the Open() API may be declared as using the __cdecl calling convention. No problem here. I assme that this is the convention used by Open() as indicated by the absence of any declaration in the OP (hence __cdecl by default).

    2.2 I also recommend that you use the Marshal.GetFunctionPointerForDelegate() method to convert the callback function into an IntPtr to pass it to Open() :

    static void CallBackFunction(string strParam)
    {
        Console.WriteLine("{0:S}", strParam);
    }
    
    static void DemonstrateCallBack()
    {
        // Create an instance of a LogDelegate delegate for CallBackFunction().
        LogDelegate callback_delegate = new LogDelegate(CallBackFunction);
        // Convert callback_delegate into a function pointer that can be
        // used in unmanaged code.
        IntPtr intptr_delegate = Marshal.GetFunctionPointerForDelegate(callback_delegate);
        // Call the API passing along the function pointer.
        string strReturn = Open(intptr_delegate);
        Console.WriteLine("Returned string : " + strReturn);
    }
    


    2.3 For more information on using Delegates as callback functions from unmanaged code, pls refer to :

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

     

    3. Concerning the Returned String from Open().

    3.1 Take careful note of the returned string from Open().

    3.2 Since Open() returns this string, it is to be considered an "out" value. As such, it is the receiving code (i.e. the C# code) that owns the returned string.

    3.3 This being the case, the CLR will take the returned pointer to C-style string, convert it to a managed string and then free the memory of the buffer of the C-string. Furthermore, the freeing will be done using ::CoTaskMemFree().

    3.4 Hence it is important that the Open() function allocate the memory for the C-string to be returned and it must do so using ::CoTaskMemAlloc(). The following is an example code for Open() that I wrote for illustrative purposes :

    typedef void (__stdcall LOG)(const char* sErr);
    
    const char* __cdecl Open(LOG* pfLog)
    {
        char szSampleString[] = "Hello World";
        ULONG ulSize = strlen(szSampleString) + sizeof(char);
        char* pszReturn = NULL;
    
        pszReturn = (char*)::CoTaskMemAlloc(ulSize);
        // Copy the contents of szSampleString
        // to the memory pointed to by pszReturn.
        strcpy(pszReturn, szSampleString);
        
        // Make the callback.
        if (pfLog)
        {
          pfLog(pszReturn);
        }
        
        // Return pszReturn.
        return pszReturn;  
    }
    

    Please check out with your vendor how Open() allocates the string and if it is meant to be returned and owned by the caller.

    3.5 If the vendor indicates that the string returned by Open() is not meant to be owned by the caller, then the declaration for Open() in C# should be changed to returning an IntPtr instead of a string, e.g.  :

    [DllImport("test.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern IntPtr Open2(IntPtr d);
    

    Note that I have used another name Open2() for the alternative API.

    3.6 I have written a sample Open2() function for illustrative purposes :

    const char* __cdecl Open2(LOG* pfLog)
    {
        static const char szSampleString[] = "Hello World";
       
        // Make the callback.
        if (pfLog)
        {
          pfLog(szSampleString);
        }
        
        // Return const char*.
        return szSampleString;  
    }
    
    

    3.7 The call to Open2() and the subsequent conversion of the return pointer to a managed string should be done as follows :

    static void DemonstrateCallBack2()
    {
        // Create an instance of a LogDelegate delegate for CallBackFunction().
        LogDelegate callback_delegate = new LogDelegate(CallBackFunction);
        // Convert callback_delegate into a function pointer that can be
        // used in unmanaged code.
        IntPtr intptr_delegate = Marshal.GetFunctionPointerForDelegate(callback_delegate);
        // Call the API passing along the function pointer.
        IntPtr pstrReturn = Open2(intptr_delegate);
        // Manually convert the returned pointer into a managed string.
        // The returned pointer is then left alone.
        string strReturn = Marshal.PtrToStringAnsi(pstrReturn);
        Console.WriteLine("Returned string : " + strReturn);
    }
    

    The returned pointer to string is then left alone since it remains a property of the DLL.

    - Bio.

     


    Please visit my blog : http://limbioliong.wordpress.com/
    • Proposed as answer by Paul Zhou Friday, November 4, 2011 8:21 AM
    • Marked as answer by S_Banks Friday, November 4, 2011 8:45 AM
    Friday, November 4, 2011 4:31 AM
  • Thanks for the excellent explanation. :)

    This seems to be working now.

    As far as the returned string goes, In an example C++ code segment I have from the vendor, the the returned string is retained as a pointer:

    e.g : const char* str = Open(_handler1)

    So the string memory is still owned by the dll I assume, and I should use an IntPtr and marshal it myself, as your second example. (I think, my C++ is basic at best..) ?

    Thanks again.

    Friday, November 4, 2011 8:44 AM