Win32: SendMessage, die einen String erwartet, von C# an einen anderen Prozess schicken

Win32: SendMessage, die einen String erwartet, von C# an einen anderen Prozess schicken

Ich habe mir hier(SendMessage für generische Strukturen, die auch strings enthalten dürfen) sowie hier (SendMessage für einfache Strukturen) einige Gedanken (und ähnlich viel Mühe) zum Thema Marshalling, Interprozess-Kommunikation sowie unterschiedliche Behandlung verschiedener Typen gemacht.

Nachdem ich mehrmals darüber gestolpert bin, daß Strings bei SendMessage nur dann und wann gemarshallt werden, habe ich jetzt wieder eine extra Funktion geschrieben.

Immerhin funktioniert ja die naheliegende Variante mal wieder nicht:

1
2
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, StringBuilder sb);

Und – wie so oft liegt das daran, daß ich mir meine Handles aktuell viel zu gerne aus anderen Prozessen hole. Und das Betriebssystem hier nicht so ohne weiteres den Datentransfer übernehmen möchte.

Und.. nach kurzem Probieren und Abändern meiner vorhandenen Lösungen habe ich auch eine funktionierende Variation gefunden.

Ich habe mich dafür entschieden aus einem String ein byte [] zu machen (entspricht einem char* unter C), anstelle von einem char [], was ein wchar* bzw. ein Unicode-Array wäre. Dieser läßt sich mittels Marshal.Copy einfach 1:1 in den fremden Prozess kopieren und anschließend wieder auslesen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/// <summary>
/// works for simple structs T (not containing references/pointers/strings)
/// </summary>
static IntPtr SendMessageString(int handle, uint messageId, int wParam, ref string lParam, SendMessageDirection smd)
{
    int maxCnt = lParam.Length;
    if (maxCnt < 50)
        maxCnt = 50;
    if (lParam.Length < maxCnt)
        lParam = lParam + new String('x', maxCnt - lParam.Length);
    uint pid;
    // get process to hwnd - we need process to allocate memory in the right context
    ExternUser32.GetWindowThreadProcessId((IntPtr)handle, out pid);
    IntPtr process = ExternKernel.OpenProcess(ExternKernel.ProcessAccessFlags.VMOperation | ExternKernel.ProcessAccessFlags.VMRead |
        ExternKernel.ProcessAccessFlags.VMWrite | ExternKernel.ProcessAccessFlags.QueryInformation, false, pid);
 
    // allocate memory - pointer is valid in context of other process, not our own!
    IntPtr lParamPtrForeign = ExternKernel.VirtualAllocEx(process, IntPtr.Zero, (uint)maxCnt,
        ExternKernel.AllocationType.Commit, ExternKernel.MemoryProtection.ReadWrite);
 
    if ((smd & SendMessageDirection._in) > 0)
    {
        byte[] ch = Encoding.ASCII.GetBytes(lParam);
        IntPtr marshalPtrLocal = Marshal.AllocHGlobal(maxCnt);
        Marshal.Copy(ch, 0, marshalPtrLocal, maxCnt);
        ExternKernel.WriteProcessMemory(process, lParamPtrForeign, marshalPtrLocal, maxCnt, IntPtr.Zero);
        Marshal.FreeHGlobal(marshalPtrLocal);
    }
 
    IntPtr res = ExternMessage.SendMessage(handle, messageId, wParam, lParamPtrForeign);
 
    if ((smd & SendMessageDirection._out) > 0)
    {
        int read;
        // marshal memory back: first create memory to receive result from other process
        IntPtr readbackPtrLocal = Marshal.AllocHGlobal(maxCnt);
        // get memory from other process into our own space (newly allocated memory)
        bool __res = ExternKernel.ReadProcessMemory(process, lParamPtrForeign, readbackPtrLocal, maxCnt, out read);
        // marshal back into our struct
        byte[] ch = new byte[maxCnt]; // (byte[])Marshal.PtrToStructure(readbackPtrLocal, typeof(char[]));
        Marshal.Copy(readbackPtrLocal, ch, 0, maxCnt);
        // find first char that is 0
        int len;
        for (len = 0; len < maxCnt; ++len)
            if (ch[len] == 0)
                break;
        lParam = Encoding.ASCII.GetString(ch); // new String(ch);
        lParam = lParam.Substring(0, len);
        Marshal.FreeHGlobal(readbackPtrLocal);
    }
    // clean up foreign memory in foreign process
    ExternKernel.VirtualFreeEx(process, lParamPtrForeign, 0, ExternKernel.FreeType.Release);
 
    return res;
}

Zunächst wird über die Länge des eingehende Strings auch die maximale Länge der erhaltenen Boschaft bestimmt: C Strings haben ja eine fixe Maximallänge. Diese wird anschließend auf mindestens 50 Zeichen angehoben.

Dann erfolgt das übliche Allokieren lokalen Speichers sowie von Speicher im anderen Prozess, das Kopieren, der Aufruf mit Pointer relativ zum anderen Prozess, und anschließend wieder das Auslesen in den lokalen Speicher (jetzt in umgekehrter Reihenfolge).

Ferner ist noch zu beachten, daß die Initialisierung eines Strings durch ein Encoding.ASCII.GetString(ch) die komplette Länge des Arrays in den String schreibt, inkl. 0-Werte. Diese sind zu C-Zeiten jedoch als Ende des Strings gedacht, der endgültige String wird also noch einmal beschnitten.

Und.. voilà.

Weiterführend

Eine gute Übersicht über die numerischen Werte von Windowsnachrichten, die sich oft nur als Text in der Dokumentation finden lassen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.