Межпроцессная коммуникация посредством .NET - Как это работает
ОГЛАВЛЕНИЕ
Как это работает
Библиотека использует системное сообщение Windows типа WM_COPYDATA. Это системное сообщение позволяет передавать данные между несколькими приложениями путем переноса указателя на данные, которые надо скопировать, в данном случае строку. Она отправляется другим окнам с помощью SendMessage из Win32 API с PInvoke.
[StructLayout(LayoutKind.Sequential)]
public struct COPYDATASTRUCT
{
public IntPtr dwData;
public int cbData;
public IntPtr lpData;
}
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int SendMessage(IntPtr hwnd, int wMsg,
int wParam, ref COPYDATASTRUCT lParam);
Структура COPYDATASTRUCT содержит информацию о данных сообщения, которые надо передать другому приложению. На данные сообщения ссылаются члены lpData и dwData. lpData является указателем на строковые данные, хранящиеся в памяти. dwData – размер передаваемых данных. Член cdData не используется в данном случае. Чтобы передать данные, сначала надо закрепить за строкой сообщения адрес в памяти и получить указатель на эти данные. Чтобы сделать это, используется API Marshal следующим образом:
// Сериализовать неформатированные строковые данные в двоичный поток
BinaryFormatter b = new BinaryFormatter();
MemoryStream stream = new MemoryStream();
b.Serialize(stream, raw);
stream.Flush();
int dataSize = (int)stream.Length;
// Создать массив байтов и передать данные потока
byte[] bytes = new byte[dataSize];
stream.Seek(0, SeekOrigin.Begin);
stream.Read(bytes, 0, dataSize);
stream.Close();
// Выделить адрес памяти для массива байтов
IntPtr ptrData = Marshal.AllocCoTaskMem(dataSize);
// Скопировать данные в байтах в этот адрес памяти
Marshal.Copy(bytes, 0, ptrData, dataSize);
При наличии в памяти строковых данных, на которые ссылается указатель ptrData в коде выше, можно создать экземпляр COPYDATASTRUCT и заполнить члены lpData и dwData соответственно. Итак, теперь, когда сообщение обернуто как объект COPYDATASTRUCT, можно отправить его другому окну с помощью API SendMessage. Однако чтобы сделать это, сначала надо узнать, какие приложения должны принимать данные, т.е. какие слушают и на правильном канале. Так же, что если приложение не имеет описателя окна, которому можно отправить сообщение?
Для преодоления этого используются некоторые родные свойства окна и класс XDListener. При вызове экземпляра класса он создает невидимое окно на рабочем столе, действующее как слушатель для всех сообщений Windows. Это делается путем расширения класса NativeWindow из System.Windows.Forms. Переопределение метода WndProc позволяет фильтровать сообщения Windows и искать сообщение WM_COPYDATA, содержащее данные сообщения.
Класс XDListener также использует свойства окна для создания флагов свойств, указывающих, какие каналы слушает экземпляр, и, следовательно, какие сообщения он должен принимать. Когда сообщение передается, оно перечисляет все окна рабочего стола с помощью EnumChildWindows из Win32 API. Он ищет в окне флаг (имя свойства), обозначающий имя канала. Если находит – отправляет этому окну сообщение Windows WM_COPYDATA. После этого сообщение ловится и обрабатывается экземпляром XDListener, которому принадлежит невидимое окно. Для чтения данных сообщения используется lParam родного сообщения Windows, чтобы расширить экземпляр COPYDATASTRUCT. Из него можно найти и восстановить исходное строковое сообщение, сохраненное в памяти ранее.
// Переопределяется NativeWindow, чтобы фильтровать пакет WM_COPYDATA
protected override void WndProc(ref Message msg)
{
// Обрабатываются и передаются все системные сообщения
base.WndProc(ref msg);
// Если нужное сообщение
if (msg.Msg == Win32.WM_COPYDATA)
{
// msg.LParam содержит указатель на структуру COPYDATASTRUCT
Win32.COPYDATASTRUCT dataStruct =
(Win32.COPYDATASTRUCT)Marshal.PtrToStructure(
msg.LParam , typeof(Win32.COPYDATASTRUCT));
// Создается массив байтов для хранения данных
byte[] bytes = new byte[this.dataStruct.cbData];
// Копируются исходные данные, на которые ссылается
// структура COPYDATASTRUCT
Marshal.Copy(this.dataStruct.lpData, bytes, 0,
this.dataStruct.cbData);
// Данные десериализуются обратно в строку
MemoryStream stream = new MemoryStream(bytes);
BinaryFormatter b = new BinaryFormatter();
// Это сообщение, отправленное из другого приложения
string rawmessage = (string)b.Deserialize(stream);
// что-то делается с сообщением
}
}
Заметьте, что поскольку сообщение хранится в памяти во время его передачи другим приложениям, обязательно надо освободить эту память после отправки сообщения. Каждое окно, принимающее данные, сделает свою копию сообщения, поэтому исходные данные могут быть смело уничтожены сразу после отправки сообщений. Это необходимо, потому что данные хранятся в неуправляемой памяти и в противном случае могут привести к утечке памяти.
// Освобождается память, на которую ссылается данный указатель
Marshal.FreeCoTaskMem(lpData);
Ниже приведены другие методы PInvoke, используемые для установки и удаления свойств окна, а также для перечисления окон рабочего стола.
// Делегат, используемый при перечислении окон
public delegate int EnumWindowsProc(IntPtr hwnd, int lParam);
// Win32 API, используемый для перечисления потомков окна рабочего стола
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool EnumChildWindows(IntPtr hwndParent,
EnumWindowsProc lpEnumFunc, IntPtr lParam);
// API, используемый для поиска именованного свойства в окне
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int GetProp(IntPtr hwnd, string lpString);
// API, используемый для установки именованного свойства в окне, и,
//следовательно, регистрации канала обмена сообщениями
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int SetProp(IntPtr hwnd, string lpString, int hData);
// API, используемое для удаления свойства из окна, и, следовательно, для отмены
// регистрации канала обмена сообщениями
[DllImport("user32", CharSet = CharSet.Auto)]
public extern static int RemoveProp(IntPtr hwnd, string lpString);