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
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With