0

I am developing an automation interface program and I looking to enhance capability with a machine software which uses a COPYDATA API aimed at C++. The goal is to control and report status of the machine through my own software.

The method uses pointers and memory allocation which I have not had any experience with thus far.

I have looked at a number of other sources, such as this with no luck at the moment. I have tried the following code to try and run a program on the machine software.

class Program
{
    [DllImport("User32.dll", SetLastError = true, EntryPoint = "FindWindow")]
    public static extern IntPtr FindWindow(String lpClassName, String lpWindowName);

    [DllImport("User32.dll", SetLastError = true, EntryPoint = "SendMessage")]
    public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, ref COPYDATASTRUCT lParam);

    [StructLayout(LayoutKind.Sequential)]
    public struct COPYDATASTRUCT
    {
        public IntPtr dwData;    // Any value the sender chooses.  Perhaps its main window handle?
        public int cbData;       // The count of bytes in the message.
        public IntPtr lpData;    // The address of the message.
    }

    const int WM_COPYDATA = 0x004A;
    const int EXTERNAL_CD_COMMAND_RUN_ASYNC = 0x8001;

    static void Main(string[] args)
    {
        Console.WriteLine("{0} bit process.", (IntPtr.Size == 4) ? "32" : "64");
        Console.Write("Press ENTER to run test.");
        Console.ReadLine();
        IntPtr hwnd = FindWindow(null, "InSpecAppFrame");
        Console.WriteLine("hwnd = {0:X}", hwnd.ToInt64());
        var cds = new COPYDATASTRUCT();
        byte[] buff = Encoding.ASCII.GetBytes("C:\\Users\\Desktop\\COPYDATATEST.iwp");
        cds.dwData = (IntPtr)EXTERNAL_CD_COMMAND_RUN_ASYNC;
        cds.lpData = Marshal.AllocHGlobal(buff.Length);
        Marshal.Copy(buff, 0, cds.lpData, buff.Length);
        cds.cbData = buff.Length;
        var ret = SendMessage(hwnd, WM_COPYDATA, 0, ref cds);
        Console.WriteLine("Return value is {0}", ret);
        Marshal.FreeHGlobal(cds.lpData);
        Console.ReadLine();
    }

}

Running this code returns 0 for both hwnd and ret and the machine software does not react.

Sending a command is the first step, the next will be to try and get a response so I can monitor machine statuses etc.

AeroMike
  • 15
  • 7
  • 1
    If `hwnd` is 0, it means you failed to find the window to send the message to, so let's fix that first. Just a guess, it looks like you mixed both params in the call to `FindWindow`, the first one is the class name and the second is the visible title. The parameters seems to be reversed. – Alejandro Dec 29 '20 at 18:58
  • oh, of course! I changed this and it worked! When you look at something for so long you can't see the wood for the trees! Any advice on how to construct a receiving method? – AeroMike Dec 29 '20 at 20:39
  • There is no need for a special receiving method or any callback. The WM_COPYDATA message receives, after returning, the data in the dwData member. The pointer there points to the data the other program left there, if there is any. – Alejandro Dec 29 '20 at 21:22

1 Answers1

2

As a sidenote to what Alejandro wrote (and that I think is correct), you can simplify a little the code, removing a copy of the data. You can directly "pin" your byte[]. It is important that you remember to "unpin" it (for this reason the try/finally block)

There is another potential problem in your code (a problem that I saw only on a second pass of the code): C strings must be \0 terminated (so "Foo" must be "Foo\0"). Your Encoding.ASCII doesn't guarantee a \0 termination. The classical way to do it is to make the byte[] "a little larger" than necessary. I've done the changes necessary.

[DllImport("User32.dll", SetLastError = true)]
public static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, ref COPYDATASTRUCT lParam);

[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

[StructLayout(LayoutKind.Sequential)]
public struct COPYDATASTRUCT
{
    public IntPtr dwData;    // Any value the sender chooses.  Perhaps its main window handle?
    public int cbData;       // The count of bytes in the message.
    public IntPtr lpData;    // The address of the message.
}

[StructLayout(LayoutKind.Sequential)]
public struct ExternalGetPositionType
{
    public double X;
    public double Y;
    public double Z;
    public double W;
}

const int WM_COPYDATA = 0x004A;
const int EXTERNAL_CD_COMMAND_RUN_ASYNC = 0x8001;
const int EXTERNAL_CD_GET_POSITION_PCS = 0x8011;
const int EXTERNAL_CD_GET_POSITION_MCS = 0x8012;

static void Main(string[] args)
{
    Console.WriteLine("{0} bit process.", (IntPtr.Size == 4) ? "32" : "64");
    Console.Write("Press ENTER to run test.");
    Console.ReadLine();

    IntPtr hwnd = FindWindow(null, "Form1");
    Console.WriteLine("hwnd = {0:X}", hwnd.ToInt64());

    if (hwnd == IntPtr.Zero)
    {
        throw new Exception("hwnd not found");
    }

    IntPtr ret = RunAsync(hwnd, @"C:\Users\Desktop\COPYDATATEST.iwp");
    Console.WriteLine($"Return value for EXTERNAL_CD_COMMAND_RUN_ASYNC is {ret}");

    ret = GetPosition(hwnd, true, new ExternalGetPositionType { X = 1, Y = 2, Z = 3, W = 4 });
    Console.WriteLine($"Return value for EXTERNAL_CD_GET_POSITION_PCS is {ret}");

    ret = GetPosition(hwnd, false, new ExternalGetPositionType { X = 10, Y = 20, Z = 30, W = 40 });
    Console.WriteLine($"Return value for EXTERNAL_CD_GET_POSITION_MCS is {ret}");

    Console.ReadLine();
}

public static IntPtr RunAsync(IntPtr hwnd, string str)
{
    // We have to add a \0 terminator, so len + 1 / len + 2 for Unicode
    int len = Encoding.Default.GetByteCount(str);
    var buff = new byte[len + 1]; // len + 2 for Unicode
    Encoding.Default.GetBytes(str, 0, str.Length, buff, 0);

    IntPtr ret;

    GCHandle h = default(GCHandle);

    try
    {
        h = GCHandle.Alloc(buff, GCHandleType.Pinned);

        var cds = new COPYDATASTRUCT();
        cds.dwData = (IntPtr)EXTERNAL_CD_COMMAND_RUN_ASYNC;
        cds.lpData = h.AddrOfPinnedObject();
        cds.cbData = buff.Length;

        ret = SendMessage(hwnd, WM_COPYDATA, 0, ref cds);
    }
    finally
    {
        if (h.IsAllocated)
        {
            h.Free();
        }
    }

    return ret;
}

public static IntPtr GetPosition(IntPtr hwnd, bool pcs, ExternalGetPositionType position)
{
    // We cheat here... It is much easier to pin an array than to copy around a struct
    var positions = new[]
    {
        position
    };

    IntPtr ret;

    GCHandle h = default(GCHandle);

    try
    {
        h = GCHandle.Alloc(positions, GCHandleType.Pinned);

        var cds = new COPYDATASTRUCT();
        cds.dwData = pcs ? (IntPtr)EXTERNAL_CD_GET_POSITION_PCS : (IntPtr)EXTERNAL_CD_GET_POSITION_MCS;
        cds.lpData = h.AddrOfPinnedObject();
        cds.cbData = Marshal.SizeOf<ExternalGetPositionType>();

        ret = SendMessage(hwnd, WM_COPYDATA, 0, ref cds);
    }
    finally
    {
        if (h.IsAllocated)
        {
            h.Free();
        }
    }

    return ret;
}

Note even that instead of ASCII you can use the Default encoding, that is a little better.

If you want to receive the messages, in your Winforms do:

protected override void WndProc(ref Message m)
{
    if (m.Msg == WM_COPYDATA)
    {
        COPYDATASTRUCT cds = Marshal.PtrToStructure<COPYDATASTRUCT>(m.LParam);

        if (cds.dwData == (IntPtr)EXTERNAL_CD_COMMAND_RUN_ASYNC)
        {
            string str = Marshal.PtrToStringAnsi(cds.lpData);

            Debug.WriteLine($"EXTERNAL_CD_COMMAND_RUN_ASYNC: {str}");

            m.Result = (IntPtr)100; // If you want to return a value
        }
        else if (cds.dwData == (IntPtr)EXTERNAL_CD_GET_POSITION_PCS)
        {
            if (cds.cbData >= Marshal.SizeOf<ExternalGetPositionType>())
            {
                var position = Marshal.PtrToStructure<ExternalGetPositionType>(cds.lpData);

                Debug.WriteLine($"EXTERNAL_CD_GET_POSITION_PCS: X = {position.X}, Y = {position.Y}, Z = {position.Z}, W = {position.W}");

                m.Result = (IntPtr)200;
            }
            else
            {
                m.Result = (IntPtr)0;
            }
        }
        else if (cds.dwData == (IntPtr)EXTERNAL_CD_GET_POSITION_MCS)
        {
            if (cds.cbData >= Marshal.SizeOf<ExternalGetPositionType>())
            {
                var position = Marshal.PtrToStructure<ExternalGetPositionType>(cds.lpData);

                Debug.WriteLine($"EXTERNAL_CD_GET_POSITION_MCS: X = {position.X}, Y = {position.Y}, Z = {position.Z}, W = {position.W}");

                m.Result = (IntPtr)300;
            }
            else
            {
                m.Result = (IntPtr)0;
            }
        }

        return;
    }

    base.WndProc(ref m);
}

Note that if you control both the sender AND the receiver, it is better much better to use Unicode for the string parameter. You'll have to modify both the sender and the receiver: Encoding.Unicode.GetByteCount/Encoding.Unicode.GetBytes, the +2 instead of +1 and Marshal.PtrToStringUni.

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • hey! thanks very much for this, this really helps. I've implemented the sender which works fine, however I am stuck on the receiver. the return result is a pointer, `(IntPtr)100`. how would I get the information out of this? From the API documentation it references other `structs`. I assume this is in the location of the pointer, so how to i extract that information? – AeroMike Dec 29 '20 at 21:23
  • 1
    @AeroMike Don't know the struct, so I don't know. In general the `WM_COPYDATA` shouldn't be able to return a whole struct, just a number. Under the hood, the WM_COPYDATA copies the memory pointed in `lpData` to another process, wait for the other process to receive the message and then frees the allocated copy. To do this it uses the `cbData` information for the length of the payload. So you see, a single `IntPtr` isn't enough to return a whole structure. You need **2** pieces of information to marshal a payload to another process. – xanatos Dec 29 '20 at 21:29
  • for instance, one of the return `struct` members is: `typedef struct { double x; double y; double z; double w; } ExternalGetPositionType; ` how would I get access to the values and say, display them on my winform? – AeroMike Dec 29 '20 at 21:43
  • 1
    @AeroMike The example given here https://amp.reddit.com/r/AutoHotkey/comments/gwht3g/windows_messaging_copydata_api_with_an_ahk_script/ is different: you pass in the `lpData` a `ExternalGetPositionType` with `dwData == EXTERNAL_CD_GET_POSITION_PCS` or `EXTERNAL_CD_GET_POSITION_MCS` and in the `ExternalGetPositionType` you get the data back. That I can do. – xanatos Dec 29 '20 at 21:47
  • @AeroMike I've implemented the message... But I have to say the truth, it isn't clear exactly how it should work... I think the program you send messages to can send WM_COPYDATA back... But without the documentation it isn't clear. You should read better the documentation – xanatos Dec 29 '20 at 22:17
  • 2
    @AeroMike In fact here https://www.reddit.com/r/AutoHotkey/comments/gwht3g/windows_messaging_copydata_api_with_an_ahk_script/?utm_source=amp&utm_medium=&utm_content=post_body they say "If InSpec software needs to return data, such as a request for position, it will send a message back to the same program with COPYDATA. This can be handled with a normal windows message handler.". So your program must be a Winforms that can send and accept WM_COPYDATA – xanatos Dec 29 '20 at 22:29
  • thanks so much for your help here. I really appreciate it and through it I feel I have learnt a lot! the sending methods work great, all returning 1, however the `WndProc` receiver method is not firing so it seems like nothing is getting through. I will continue this on another question as I feel this is a different issue all together. – AeroMike Dec 30 '20 at 13:30