How do I send a struct from C# to VB6, and from VB6 to C#?

匿名 (未验证) 提交于 2019-12-03 08:30:34

问题:

I need to send a struct from C# to a VB6 app, modify the data in VB6, and send the result back via windows messaging. How do I do this?

I am able to send basic ints back and forth with PostMessage (using DllImport in C# and registering the vb6 app with windows messaging), but need to send more structured data, consisting of strings, ints, and decimal.

Looking for the easiest solution to implement passing of structures data back and forth.

Basic sample of VB6 type

Public Type udtSessionData     SessionID As Integer     SessionName As String     MinVal As Currency     PctComplete As Double     NVal As Integer     ProcessedFlag As Boolean     ProcessedDate As Date     Length As Integer End Type 

回答1:

Caveat:

Before I begin, people may be interested in noting items from your other question.

REF: How do I send/receive windows messages from VB6 and C#?

As mentioned here and in your other post, you should really reconsider trying to make this work. In particular, even if you are fine with the techniques here, your coworkers or other people who may have to maintain your code will be in for a really bad time. Note that the debugging process is also very tough in VB6- save often and use lots of breakpoints! Expect lots of crashes if there are errors in your code.

Also, PostMessage should not be used with this technique. It will require a lot more cleanup.

Solution:

I've enclosed a sample that can pass back a structure containing only a string and integer type. Making this work requires a lot of moving parts. We'll cover going from C# to VB in depth, as that is more tricky. The reverse is not as hard once you know how to do this.

First, on the C# side, you should declare your structure. Packaging up the structure is actually not bad in C#. Here is a C# sample class that is COM-visible that demonstrates how to wrap the structure up. The key is to use Marshal.StructureToPtr and Marshal.DestroyStructure on opposing sides of your call. Depending on the types involved, you may not even have to write code to map types, either. Use the MarshalAs attribute to flag the correct mappings for VB6. Most of the items in the enum used in MarshalAs correspond to different variable types used in VARIANTs and COM automation.

The buffer that is used here is supported by a HGlobal, which needs to be freed after the call is over. It also may be possible to use GCHandle here, which also requires similar cleanup.

MarshalAsAttribute Class @ MSDN

Marshal.StructureToPtr Method @ MSDN
Marshal.DestroyStructure Method @ MSDN
Marshal.AllocHGlobal Method @ MSDN
Marshal.FreeHGlobal Method @ MSDN

using System; using System.Collections.Generic; using System.Text; using System.Runtime.InteropServices;  namespace HostLibrary {     public struct TestInfo     {         [MarshalAs(UnmanagedType.BStr)]         public string label;         [MarshalAs(UnmanagedType.I4)]         public int count;     }      [ComVisible(true)]     public interface ITestSender     {         int hostwindow {get; set;}         void DoTest(string someParameter);     }      [ComVisible(true)]     public class TestSender : ITestSender     {         public TestSender()         {             m_HostWindow = IntPtr.Zero;             m_count = 0;         }          IntPtr m_HostWindow;         int m_count;  #region ITestSender Members         public int hostwindow {              get { return (int)m_HostWindow; }              set { m_HostWindow = (IntPtr)value; } }          public void DoTest(string strParameter)         {             m_count++;             TestInfo inf;             inf.label = strParameter;             inf.count = m_count;              IntPtr lparam = IntPtr.Zero;             try             {                 lparam = Marshal.AllocHGlobal(Marshal.SizeOf(inf));                 Marshal.StructureToPtr(inf, lparam, false);                 // WM_APP is 0x8000                 IntPtr retval = SendMessage(                     m_HostWindow, 0x8000, IntPtr.Zero, lparam);             }             finally             {                 if (lparam != IntPtr.Zero)                 {                     Marshal.DestroyStructure(lparam, typeof(TestInfo));                     Marshal.FreeHGlobal(lparam);                 }             }         } #endregion          [DllImport("user32.dll", CharSet = CharSet.Auto)]         extern public static IntPtr SendMessage(             IntPtr hwnd, uint msg, IntPtr wparam, IntPtr lparam);     } } 

On the VB6 side, you will need to setup a mechanism to intercept messages. As the details are covered in your other question and in other places, I will skip over the topic of subclassing.

To unwrap a struct on the VB6 side, you will need to do this per-field, as there is no mechanism readily available to dereference a pointer value and cast it into a structure. Fortunately, you can expect field members to be aligned on 4-byte boundaries in VB6, provided you haven't specified otherwise in C#. This allows us to work field by field, mapping items from one representation to the other.

First, some module code to do all the support work. Here are functions and items of note.

TestInfo type - A mirror definition of the structure used on both sides.
CopyMemory - A win32 function that may be used to copy bytes.
ZeroMemory - A win32 function that resets memory to zero byte values.

In addition to those items, we make use of the undocumented VarPtr() function in VB6 to get the address of items. We can use this to index into the structure on the VB6 side. See the following link for details on this function.

How to get the Address of Variables in Visual Basic @ support.microsoft.com

Public Const WM_APP As Long = 32768 Private Const GWL_WNDPROC = (-4) Private procOld As Long  Type TestInfo     label As String     count As Integer End Type  Private Declare Function CallWindowProc Lib "USER32.DLL" Alias "CallWindowProcA" _     (ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal uMsg As Long, _     ByVal wParam As Long, ByVal lParam As Long) As Long  Private Declare Function SetWindowLong Lib "USER32.DLL" Alias "SetWindowLongA" _     (ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long  Private Declare Sub CopyMemory Lib "KERNEL32.DLL" Alias "RtlMoveMemory" _     (ByVal pDst As Long, ByVal pSrc As Long, ByVal ByteLen As Integer)  Private Declare Sub ZeroMemory Lib "KERNEL32.DLL" Alias "RtlZeroMemory" _     (ByVal pDst As Long, ByVal ByteLen As Integer)  Public Sub SubclassWindow(ByVal hWnd As Long)     procOld = SetWindowLong(hWnd, GWL_WNDPROC, AddressOf SubWndProc) End Sub  Public Sub UnsubclassWindow(ByVal hWnd As Long)     procOld = SetWindowLong(hWnd, GWL_WNDPROC, procOld) End Sub  Private Function SubWndProc( _         ByVal hWnd As Long, _         ByVal iMsg As Long, _         ByVal wParam As Long, _         ByVal lParam As Long) As Long      If hWnd = Form1.hWnd Then         If iMsg = WM_APP Then              Dim inf As TestInfo             ' Copy First Field (label)             Call CopyMemory(VarPtr(inf), lParam, 4)             ' Copy Second Field (count)             Call CopyMemory(VarPtr(inf) + 4, lParam + 4, 4)              Dim strInfo As String             strInfo = "label: " & inf.label & vbCrLf & "count: " & CStr(inf.count)              Call MsgBox(strInfo, vbOKOnly, "WM_APP Received!")              ' Clear the First Field (label) because it is a string             Call ZeroMemory(VarPtr(inf), 4)             ' Do not have to clear the 2nd field because it is an integer              SubWndProc = True             Exit Function         End If     End If      SubWndProc = CallWindowProc(procOld, hWnd, iMsg, wParam, lParam) End Function 

Note that this solution requires the cooperation of the sender and the recipient. Because we do not wish to free the string field twice, we empty out the copy made on the VB6 side before returning control. It is undefined what will happen here if you attempt to assign a new value to field members, so avoid editing fields in the structure.

In mapping fields, UnmanagedType.BStr in C# is directly analogous to string in VB6.
UnmanagedType.I4 maps to Integer and Long in VB6. The other fields you have specified in your UDT also have equivalents, although I am unsure about DateTime in VB6.

The remainder of the VB6 app (Form source code) is straightforward.

Dim CSharpClient As New HostLibrary.TestSender  Private Sub Command1_Click()     CSharpClient.DoTest ("Hello World from VB!") End Sub  Private Sub Form_Load()     CSharpClient.hostwindow = Form1.hWnd     Module1.SubclassWindow (Form1.hWnd) End Sub  Private Sub Form_Unload(Cancel As Integer)     CSharpClient.hostwindow = 0     Module1.UnsubclassWindow (Form1.hWnd) End Sub 

Now, in sending a structure from VB6 to C#, you need to do the reverse. For some simple structures, you may even be able to send just the address of the structure itself. If you need memberwise control, you can obtain suitable buffer memory by using GlobalAlloc, and then release it with GlobalFree. Memberwise copies may be performed the same way as parameters were unwrapped from C#, for each field. However, cleanup is simpler after the call. If you used a buffer, you only need to zero out the memory in the buffer before handing it over to GlobalFree.

GlobalAlloc Function (Windows) @ MSDN
GlobalFree Function (Windows) @ MSDN

When the message arrives on the C# side, use Marshal.PtrToStructure() to map an IntPtr into a .NET structure.

Marshal.PtrToStructure Method @ MSDN



回答2:

You have to assign GUIDs and use the MarshalAs attribute. .NET COM Interop handles the translation. Not too much different than a class. This series of posts illustrates what you need to do.



回答3:

By using P/Invoke on .NET and importing CopyMemory in VB6 you can make this work but this is so much of a maintenance disaster I'd recommend running from anything like this.



标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!