5

I have two problems; I believe the first, easier, problem must be resolved before the second one, so I'll stick to only that one here.

First, an overview: I have a hardware device that uses the USB port, and has a custom DLL to talk to/from it. I am using VB.net to upgrade from C++. The custom DLL has many functions, and I have been able to program for all but one, using IntPtr and Marshalling functions for the simpler DLL calls; the last one, which will be my second question/post, is giving me problems. It is a callback type operation and uses a TYPEDEF definition.

So, the first problem: How do I convert

typedef void (WINAPI *MyFunctPtr)(unsigned char*,int, LPVOID)

into VB.net? I understand (I think) that this is defining a pointer named MyFunctPtr, that takes three parameters in, and is an alias for VOID, meaning it will return nothing. Is this correct, and how do I use it in VB.net?

The typedef is used as follows:

AddHandler(MyPtrType func,  LPVOID pParam);

where AddHandler is the DLL call (which will be the subject of my second question/post, along with the DECLARE statement that is needed).

In pursuit of this subject, I have viewed a number of forums and Q/A type discussions, but none seem to specifically address this problem (at least, not that I in my ignorance, can tell). I did discover a thread in this forum that seem very close to this same problem ("Using a C-callback function with .NET"), but I do not know enough to tell; I don't even understand the answer, never mind the question!

As I indicated, there is a second part to this question:

1.This code is intended to communicate via USB to an external hardware device. I am successfully doing that with a number of other functions, using DLL calls and Marshaling with INTPTRs.

2.The functionality needed by this code is somewhat different however. Essentially, there are four efforts involved:

a) Respond to the "GoButton" click by performing a DLL call that registers the CallBack function with the external device (this is a DLL call that, of course, passes a reference to the CallBack function. This tells the external hardware where to send it's data when the appropriate event happens) and spawning the second thread.

b) Respond, as the newly spawned second thread, by performing a DLL call that, in effect, tells the external hardware "OK, start responding to events, and send the data to the CallBack"

c) Respond, in the first/original thread, to the "StopBUtton" click by performing a DLL call that, in effect, tells the external hardware, "OK, stop responding to events, and don't send any data to the CallBack"

d) The CallBack function itself.

"D" is just a data handler that, I believe, should be no different that the data handler I have already written for other, non-CallBack functions. "B" actually spawns a second thread to handle the CallBack response, because the first thread must be available to respond to the click event of "C".

OK, so here's the legacy DLLs, in sequence:

a)

    BYTE WINAPI AddHandler(MyPtrType func,  LPVOID pParam); //BYTE is Int32 in VB.net

Note the use of the "MyPtrType" typedef (definition repeated here), which has the same three pointers as the CallBack function

    typedef void (WINAPI *MyPtrType)(unsigned char*, int, LPVOID);

b)

    BYTE WINAPI Enable(); //BYTE is Int32 in VB.net

c)

    BYTE WINAPI Disable();  //BYTE is Int32 in VB.net

Here are the code functions that call the above:

a)

    GoButton_Click()
    {
        AddHandler(MyCallbackFunction, this);
        BeginThread(SecondThread, this);
        //First thread has spawned second thread, and is now free to continue = exit this function
    }

b)In the SecondThread:

    SecondThread(LPVOID pParam)
    {
       Dialog* pthis = (Dialog*)pParam;
       int ResponseFlag = 0; //int is Int32 in VB.net
       ResponseFlag = Enable();
       //This call will not return until the external device gets the "Stop" command, thus it exists in the second thread
       return 0;
    }

c)In the "Stop" button event:

    StopButton_Click()
    {
        int ResponseFlag = 0; //int is Int32 in VB.net
        ResponseFlag = Disable();
    }

d) In the Callback function:

    MyCallbackFunction((unsigned char *buf, int rev, LPVOID pParam))
    {
        Dialog* pthis = (Dialog*)pParam;
        CString str;

        for(int i = 0; i < rev; i++)
        {   
            str.Format("%02X ",buf[i]);
            pthis->Data += str;
        }   
    }

I know that BYTE = Int32 in my system, as I am using it successfully in other functions.

This is where I am now:

    Private Delegate Sub ParsedDataDelegate()
    Private Declare Function EnableData Lib "Foo.dll" () As Int32 'EnableData()
    Private Declare Function DisableData Lib "Foo.dll" () As Int32  'DisableData()
    Private Declare Function AddDataHandle Lib "Foo.dll" (By??? ??? As IntPtr, By??? Parameter As IntPtr) As Int32    'AddDataHandle(MyFunctPtr func,LPVOID pParam)
    'Note: the first parameter to "AddDataHandle" is some kind of reference to "ParseDataHandler" as the callback
    '==>ALSO, SOMETHING GOES HERE TO EQUATE TO "typedef void (WINAPI *MyFunctPtr)(unsigned char*,int, LPVOID)"

    Sub StartButton_Click
    'This is main thread

        Dim Result As Int32
        Dim EnableReadData As New Thread(AddressOf ParseDataHandler)

        'Register callback with external hardware device
        'Result = AddDataHandle(????, ????)  <==Don't yet know what to put here, 
        'until I figure out "typedef void (WINAPI *MyFunctPtr)(unsigned char*,int, LPVOID)"

        'Spawn second thread
        EnableReadData.Start()

    End Sub

    Sub EnableReadData
    'This is spawned thread

        Dim Result As Int32

        'Invoke the callback
        Me.Invoke(New ParseDataDelegate(AddressOf ParseDataHandler))

        'Start the hardware device to get data
        Result = EnableData() 'This DLL call blocks(here in the 2nd thread) until 
        'the StopButton_Click event (in the 1st thread) occurs to call the DisableData DLL

    End Sub

    Private Sub ParseDataHandler()

        'Grab and display data here

    End Sub

    Sub StopButton_Click

        Dim Result As Int32

        'Stop the hardware device
        Result = DisableData()

    End Sub

Not only do I not know for sure what to use for the TypeDef, I'm also not sure if I'm even using the concept of CallBack correctly, which means I may not be using the declaration itself properly!

Thank you for putting up with this long post. I have banged my head against the wall for almost a week now, trying to fight those three unknowns, where each one may be componding the other. I am totally lost and, at this point, not above begging for help. Please help me.

Thank you Charlie

====================================================================== Edited 10/28, Update:

Here is my latest attempt. Please note, that despite the errors listed, I feel we are making progress. Thanks to the support from this forum, I have been able to (I believe) move forward, given that the previous errors seem to be significantly resolved, allowing me this next attempt. Please realize that, for me, this is all experimentation...I may be moving totally in the wrong direction:

    Private Declare Function EnableData Lib "foo.dll" () As Int32 'EnableData()
    Private Declare Function DisableData Lib "foo.dll" () As Int32  'DisableData()
    Private Declare Function AddDataHandle Lib "foo.dll" (ByVal Handler As MyFunctPtr, ByVal ThisClass As IntPtr) As Int32    'AddDataHandle(MyFunctPtr func,LPVOID pParam)

    Private Delegate Sub ParseDataDelegate(DataBuffer As Byte(), DataLength As Integer, ParamPointer As IntPtr)
    Private Delegate Sub MyFunctPtr(DataBuffer As Byte(), DataLength As Integer, ParamPointer As IntPtr) 'typedef void (WINAPI *MyFunctPtr)(unsigned char*,int, LPVOID)



    Sub StartButton_Click
    'This is main thread

        Dim Result As Int32
        Dim Callback As ParseDataDelegate

        Note: Different attempts at same call...

        'Attempt #1 (no parameters) produces this error, repeated 3 times, one for each parameter: 
        'Argument not specified for parameter 'DataBuffer' of 'Private Sub ParseCardDataHandler(DataBuffer() As Byte, DataLength As Integer, ParamPointer As System.IntPtr)'.   
        Dim EnableReadData As New System.Threading.Thread(System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(ParseDataHandler()))

        'Attempt #2 (adding the parameters) produces this error, repeated 3 times, one for each parameter: 
        '1)'DataBuffer' is not declared. It may be inaccessible due to its protection level.
        Dim EnableData As New System.Threading.Thread(System.Runtime.InteropServices.Marshal.GetFunctionPtrForDelegate(ParseDataHandler(DataBuffer(), DataLength, ParamPointer)))


        Callback = AddressOf ParseDataHandler 'Don't let this get collected!  keep it in a class variable for as long as the DLL is using it        

        'I get this error here:
        'Value of type 'System.IntPtr' cannot be converted to 'xxxx.xxxx.MyFunctPtr'.
        Result = AddDataHandle(System.Runtime.InteropServices.Marshal.GetFunctionPtrForDelegate(Callback), IntPtr.Zero)

        EnableReadData.Start()

    End Sub


    Private Sub EnableReadData()

    Dim Result As Int32

        'This produces an error of  "Expression Expected", 3 times, one for each parameter.  What is wanted after the  ":="?  Or is this call wrong altogether?
        Me.Invoke(New ParseDataDelegate(AddressOf ParseDataHandler(DataBuffer:=,DataLength:=, ParamPointer:=)))

        Result = EnableData()

    End Sub

    Private Sub ParseDataHandler(DataBuffer As Byte(), DataLength As Integer, ParamPointer As IntPtr) '(a As Byte(), b As Integer, c As IntPtr)'(ByVal DataBuffer As String, ByVal Length As Integer, SomeParameter As IntPtr)

        Stop

    End Sub

Yet again, I must thank all for your help.

Charlie

================================================================================= Oct 29 Update:

Made some progress. The callback is working, but there are still a few other problems. Here is the code to date:

    'Class level...
    Private Declare Function EnableData Lib "foo.dll" () As Int32 'EnableData()
    Private Declare Function DisableData Lib "foo.dll" () As Int32  'DisableData()
    Private Declare Function AddDataHandle Lib "foo.dll" (ByVal Handler As MyFunctPtr, ByVal ThisClass As IntPtr) As Int32 

    Private Delegate Sub MyFunctPtr(DataBuffer As Byte(), DataLength As Integer, ParamPointer As Object) 'typedef void (WINAPI *MyFunctPtr)(unsigned char*,int, LPVOID)

    Dim Callback As System.Threading.Thread

    'Code level...
    Sub StartButton_Click
    'This is main thread

        Dim Result As Int32

        'Define the callback, point to desired second thread
        Callback = New System.Threading.Thread(AddressOf EnableReadData) 'System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate

        'Register the callback with the external hardware
        Result = AddDataHandle(AddressOf ParseDataHandler, IntPtr.Zero)

        'Start the second thread
        Callback.Start()

    End Sub

    Sub StopButton_Click

        Dim Result As Int32

        'Stop the hardware device
        Result = DisableData()

    End Sub

    Sub EnableReadData()
    'This is the secondary thread

        Dim Result As Int32

        'Start the hardware device
        Result = EnableData()

    End Sub

    Sub ParseDataHandler(DataBuffer As Byte(), DataLength As Integer, ParamPointer As Object) '(a As Byte(), b As Integer, c As IntPtr)'(ByVal DataBuffer As String, ByVal Length As Integer, SomeParameter As IntPtr)

        Debug.Print(DataBuffer(0))

    End Sub

At this point, I have two problems and a question:

1) The DataLength value in the ParseDataHandler routine shows some 200+ bytes of data, but the DataBuffer shows a length of 1. Obviously, 1 is wrong, but is 200+ correct? Need to research this further. Also, if DataLength IS correct, I'm not sure how to go from Byte array to string.

2) I get a "SEHException was unhandled" message. Description is "External component has thrown an exception." I'm assuming that, since the hardware has been working with the original code, it's still working now. Further, the term "External component" may not actually mean external to the system, but rather the second thread as external to the main thread. Does this seem a feasible theory?

3) The definition I am using for AddDataHandle includes "...ThisClass As IntPtr". The IntPtr should really be an Object, but to call it that way, I have to pass in an Object. Right now I'm using IntPtr.Zero, because the Object I would have thought was correct ("Me") gives an error. What Object should I be using? MyBase? MyClass? Or something else entirely?

Continuing my quest, and thanking all who have helped. Now, if anyone could just advise me on these last three issues...?:)

Thanks again, Charlie

==================================================

Success! Oct 30

Here's the final code snippets:

 Private Declare Function EnableData Lib "Foo.dll" () As Int32 
 Private Declare Function DisableData Lib "Foo.dll" () As Int32  
 Private Declare Function AddDataHandle Lib "Foo.dll" (ByVal Handler As MyFunctPtr, ByVal ThisClass As IntPtr) As Int32

 Private Delegate Sub MyFunctPtr(ByVal DataBuffer As IntPtr, ByVal DataLength As Integer, ByVal ParamPointer As IntPtr)

 Dim Callback As System.Threading.Thread
 Delegate Sub SetTextCallback([text] As String)


 Private Sub GoButton_Click(sender As Object, e As EventArgs) 

     Dim Result As Int32

     'Define the callback, point to desired second thread
     Callback = New System.Threading.Thread(AddressOf Me.EnableReadData)    

     'Register the callback with the external hardware
     'NOTE:  THE DLL EXPECTS THE LAST PARAMETER HERE TO BE AN OBJECT, SO IT CAN TURN AROUND AND
     'PASS THAT SAME OBJECT BACK TO US AS THE "PARAMPOINTER" IN THE "MYFUNCTPTR" DELEGATE.
     'HOWEVER, WE CAN SET IT TO ZERO SIMPLY BECAUSE WE DON'T CARE ABOUT IT.  WE ALREADY KNOW WE
     'WANT THE DATA TO END UP IN A SPECIFIC CONTROL, SO WE'LL INVOKE THAT CONTROL OURSELVES WHEN
     'NEEDED.     SEE "ParseDataHandler"
     Result = AddDataHandle(AddressOf ParseDataHandler, IntPtr.Zero)

     'Start the second thread "EnableReadData"
     Callback.Start()

 End Sub   

 Private Sub EnableReadData()

     Dim Result As Int32
     Dim ErrorData As String  

     'Start the hardware device
     Result = EnableData()

 End Sub

  Private Sub ParseDataHandler(ByVal DataBuffer As IntPtr, ByVal DataLength As Integer, ByVal ParamPointer As IntPtr)
    'HERE IS WHERE WE CAN IGNORE THE LAST PARAMETER, AS IT WAS PASSED IN VIA THE DLL AND IS
    'SUPPOSED TO REPRESENT THE OBJECT THAT DISPLAYS THE DATA, IN THIS CASE OUR "lblData" LABEL.
    'SINCE WE ARE CROSS_THREADING TO SHOW THE DATA ANYWAY, WE ALREADY KNOW WHERE WE ARE GOING TO
    'SEND IT, SO WE JUST DO THAT; DON'T NEED THE LAST PARAMETER DATA.
    'SEE "GoButton_Click"          

     Dim Data1 As String
     Dim Data2 As New System.Text.StringBuilder(DataLength * 2)
     Dim TempChar As String
     Dim TempData(DataLength - 1) As Byte
     Dim TempByte As Byte

     'Copy DataBuffer stream into TempData byte array
     System.Runtime.InteropServices.Marshal.Copy(DataBuffer, TempData, 0, DataLength)

     'Convert each byte in the byte array into a two nibble hex stream
     For Each TempByte In TempData
         TempChar = Conversion.Hex(TempByte)
         If TempChar.Length = 1 Then TempChar = "0" & TempChar
         Data2.Append(TempChar)
         Data2.Append(" ")
     Next

     'Convert hex stream to string
     Data1 = Data2.ToString()

     'Call the cross-thread delegate operation
     Me.ShowData([Data1])

     Application.DoEvents()

 End Sub

 Private Sub ShowData(ByVal [Data] As String)

     'Is thread that originally created lblData the same thread that wants to use it now?
     If Me.lblData.InvokeRequired Then
         'No, so need to invoke the delegate for it...
         'Define the delegate
         Dim DataDelegate As New SetTextCallback(AddressOf ShowData)

         'Invoke the delegate, passing the text
         Me.Invoke(DataDelegate, New Object() {[Data]})

     Else

         'Yes, so can write directly.  NOTE: THIS SHOULD NEVER HAPPEN, WE ARE NOT CALLING DIRECT FROM ANYPLACE
         Me.lblData.Text = [Data]

     End If

     Application.DoEvents()

 End Sub

 Private Sub Stop_Click(sender As Object, e As EventArgs) 

     Dim Result As Int32
     Dim ErrorData As String

     Result = DisableData()  

 End Sub

I want to thank everyone who took the time to point me in the right direction. I hope this code example helps others in turn.

Charlie

Community
  • 1
  • 1
  • This syntax is explained here: http://stackoverflow.com/a/516253/897326. I am still wondering why `WINAPI *MyFunctPtr`... Is it a pointer to pointer or something like that? (not a C++ expert here). – Victor Zakharov Oct 27 '13 at 20:22
  • I added a C++ tag, so hopefully C++ gurus can explain what it means exactly. – Victor Zakharov Oct 27 '13 at 21:38
  • 1
    You need a delegate, but it is not possible to say what it should be. Is the first parameter a null terminated string, or a byte array? – David Heffernan Oct 27 '13 at 21:50
  • 1
    @Neolisk It's a function pointer using the stdcall can,long convention – David Heffernan Oct 27 '13 at 21:50
  • @DavidHeffernan: do you mean that it should read as `typedef void (__stdcall *MyFunctPtr)(unsigned char*,int, LPVOID)`? (according to [this answer](http://stackoverflow.com/a/297661/897326)) – Victor Zakharov Oct 27 '13 at 21:55
  • 2
    @Neolisk WINAPI is a macro that expands to __stdcall. Bloody Android auto correct ate my previous comment. Was meant to be calling convention. – David Heffernan Oct 27 '13 at 22:05
  • You're also going to need an aliasing p/invoke declaration, because the function name conflicts with the `AddHandler` keyword in VB.NET – Ben Voigt Oct 28 '13 at 00:14
  • "AddHander" is not the original name, but something I choose for this example; the original data is proprietary, so I had to change the name. In the real world, the original name is safe; no alias needed. – charliee1151 Oct 28 '13 at 00:30
  • If your issue is resolved, please take your time to accept an answer. It helps other members use this website more efficiently. – Victor Zakharov Oct 31 '13 at 12:01

2 Answers2

3

Your call should end up being

callback = AddressOf MyHandler ' don't let this get collected!  keep it in a class variable for as long as the DLL is using it
Result = AddDataHandle(Marshal.GetFunctionPtrForDelegate(callback), IntPtr.Zero)

In C++, the second parameter was used to pass an object pointer. In .NET, that wouldn't work because the garbage collector moves objects around in memory. But it's not needed, since GetFunctionPtrForDelegate() function encodes the object pointer inside when it generates the machine code.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • This is interesting: ==>In C++,...generates the managed code.<==. I will have to study it. Thank you. It appears that this should be what I put into the StartButton_Click code. I will certainly try that. However, I will still need help with the declaration for the DLL call. And what do I do for the TypeDef, given that we seem to have agreed that the concept of Declare Sub is no longer valid? – charliee1151 Oct 28 '13 at 01:25
  • @charliee1151: You do need the `Delegate Sub`, use it when you declare the variable which I named `callback` (the name is not important) – Ben Voigt Oct 28 '13 at 05:14
1

Based on @David Heffernan's hint, I'd give it a try:

Delegate Sub MyFunctPtr(a As Byte(), b As Integer, c As Byte())

void means nothing is returned. Sub is an equivalent in VB.NET. Here is a link to explain how unsigned char* became a Byte(). Second parameter may also be a Short, depending on which compiler was used for your C++ code.

I dropped WINAPI, because it's the same as __stdcall and VB can only use that.

Last one can also be written as c As Any, depending on the function being used:

The right answer for how to marshal LPVoid parameters is specific to the function.

Community
  • 1
  • 1
Victor Zakharov
  • 25,801
  • 18
  • 85
  • 151
  • Thank you so much. It never occured to me that a VOID (returning nothing) would be a SUB (returning nothing) in VB. That's a learning point for me. However, I'm not sure that it should be used in the Delegate call, although I am sure that there must be a Delegate call in the final solution. Because of the length of the additional data needed, I will have to respond by using the full-length Question input. Thanks again. Charlie – charliee1151 Oct 27 '13 at 23:30
  • @charliee1151: Let's do it in steps. I suggest you accept this answer and ask another question where you would combine it with additional data. You can refer to this question, if any information here is still relevant. Doing this all in one question would end up very messy. – Victor Zakharov Oct 27 '13 at 23:52
  • You most likely want to pass a pointer to the delegate using `Marshal.GetFunctionPointerForDelegate` – newb Oct 28 '13 at 00:09
  • The usage example is completely wrong for this question... the C++ code is going to call the function, he needs to know how to pass his handler to p/invoke. – Ben Voigt Oct 28 '13 at 00:12
  • @BenVoigt: I was under the impression OP wanted to convert that C++ code piece to VB.NET, in which case it should become a delegate in VB.NET. OP promised to provide more information on the matter. – Victor Zakharov Oct 28 '13 at 00:14
  • @Neolisk: I agree it should be a delegate, but your usage example shows how to attach a delegate to a managed event source. There is no managed event source. – Ben Voigt Oct 28 '13 at 00:19
  • ==>The usage example is completely wrong for this question... the C++ code is going to call the function, he needs to know how to pass his handler to p/invoke<== This is probably true, based on my best-guess of what I think I am trying to do. Also, I posted the second post before I read these comments. I appologize for not getting posts-vs-comments in order. – charliee1151 Oct 28 '13 at 00:19
  • @BenVoigt: You are right. With new information added by OP, I can now better see your point. It's just that I am more used to attaching delegates to events, rather than marshalling them to pass into non-managed code. – Victor Zakharov Oct 28 '13 at 00:27
  • {Usage example was removed} – Victor Zakharov Oct 28 '13 at 00:35