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