tags:

views:

447

answers:

3

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  A: 

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.

RS Conley
Doesn't have to be done via COM. You can use regular "win32 pinvoke" style interop. This still requires the MarshalAs thing, but not GUIDs nor any COM stuff.
Cheeso
@Cheeso - no, you can't use regular "win32 pinvoke" to communicate with VB6, you have to use COM.
MarkJ
I am able to send basic messages from c# to vb6 with PostMessage, but right now I only know how to send ints.
mikeh
sure but if you want to send rich data (classes and structures) you should use com. VB6 is not forgiving in this regard.
RS Conley
At this point,being able to send a single string back and forth would even suffice (I could just parse the string on either end). I am looking for the simplest solution to pass more data than just an integer back and forth.
mikeh
The simplest solution is to use COM.
MarkJ
Still, the question was NOT "what is the simplest way to achieve X", but rather "how do I do X with Y".
Heinzi
I was focused on the next to last sentence "what is the simplest..."
RS Conley
good point......
Heinzi
+1  A: 

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.

Joshua
+1  A: 

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

meklarian
There's an easier and more "objecty" way to do the VB6 subclassing http://visualstudiomagazine.com/articles/2009/07/16/subclassing-the-xp-way.aspx
MarkJ