Struct senden per WM_COPYData



  • Hallo

    Ich versuche eine struct per WM_COPYDATA an eine MFC Applikation zu senden.
    Dazu habe ich schon viele Beispiele gefunden. Leider aber immer nur wie man einen String versendet oder einen Integer.

    Doch ich möchte eine Struktur versenden. Und alle versuche sind bisher gescheitert:

    Hier mal Code:

    // für WM_COPYDATA
    public struct COPYDATASTRUCT
    {
      public IntPtr dwData;
      public int cbData;
      public object lpData;
    }
    
    // meine Struktur mal stark vereinfacht. 
    // es fehlen noch enums und weitere Strukturen und ein Timestamp
    // aber nicht mal die einfach Struktur bekomme ich übertragen
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct IpcArgs
    {
       [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 2048)]
       public string message;
    
       public int code;
    
    }
    
    public void SendData(int hwnd, IpcArgs args)
    {
         var cpd = new COPYDATASTRUCT();
         cpd.dwData = (IntPtr)0;
         cpd.lpData = args;
    
         // Wird diese Zeile ausgeführt empfängt meine MFC Applikation gar nichts mehr
         // andernfalls kommt nur Müll an.
         //cpd.cbData = Marshal.SizeOf(args);
    
         SendMessage(hwnd, WM_COPYDATA, 0, ref cpd);
    }
    

    Hoffe mir kann man noch helfen 🙂


  • Administrator

    Sag uns am besten, wie die Struktur in C aussieht, dann können wir dir am besten helfen. Du musst schliesslich die C Struktur in C# nachbauen.

    Informationen über P/Invoke findest du hier:
    http://msdn.microsoft.com/en-us/library/fzhhdwae.aspx
    (aber ist ein knietiefer Sumpf ;))

    Viele Beispiele und bereits fertige Lösungen für die WinAPI findest du hier:
    http://pinvoke.net/

    Grüssli



  • hier ist meine struct mit ihren benötigten Komponenten.

    //-----------------------------------------------------------------------------
    ///  Event Komponente
    //-----------------------------------------------------------------------------
    struct ComponentIdent
    {
    	/// Zeile
    	int line;
    	/// Datei 
    	char file[2048];
    	/// Funktion
    	char proc[256];
    	/// Sektion
    	char section[256];
    };
    
    //-----------------------------------------------------------------------------
    /// Event Stufe
    //-----------------------------------------------------------------------------
    enum Severity
    {
    	/// Information
    	SEV_INFO,
    	/// Warnung
    	SEV_WARNING,
    	/// Fehler
    	SEV_ERROR
    };
    
    //-----------------------------------------------------------------------------
    /// Debug Level
    //-----------------------------------------------------------------------------
    enum DebugLevel
    {
    	/// Bediener
    	DBL_USER,
    	/// Debug Level 1
    	DBL_1,
    	/// Debug Level 2
    	DBL_2,
    };
    
    //-----------------------------------------------------------------------------
    ///  Event Argumente
    //-----------------------------------------------------------------------------
    struct IPCArgs
    {
    	/// Indentifiaktion der Komponente die das Event ausgelöst hat
    	ComponentIdent reporter;
    	/// Indentifiaktion der Komponente in der das Event entstanden ist
    	ComponentIdent source;
    	/// Zeitstempel
    	CTime timestamp;
    	/// Nachricht 
    	char message[2048];
    	/// Code
    	int code;
    	/// Stufe
    	Severity severity;
    	/// DebugLevel
    	DebugLevel debugLevel;
    };
    

    Mein Problem besteht aber wohl eher darin wie ich die Instanz von der IPCArgs Struct in die COYDATASTRUCT bekomme.

    Denn ich schaffe es nicht mal die Struktur die ich bis auf einen integer heruntergebrochen habe zu übertragen.


  • Administrator

    Das CTime in der IPCArgs Struktur kannst du vergessen. Eine C++ Klasse kannst du nicht per P/Invoke übertragen. Ich habe es hier mal durch einen Int64 ersetzt, heisst du kannst z.B. einen Unix-Timestamp nehmen.

    [StructLayout(LayoutKind.Sequential)]
    struct COPYDATASTRUCT
    {
    	public IntPtr dwData;
    	public Int32 cbData;
    	public IntPtr lpData;
    }
    
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    struct ComponentIdent
    {
    	public Int32 line;
    	[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 2048)]
    	public string file;
    	[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
    	public string proc;
    	[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
    	public string section;
    }
    
    enum Severity : Int32
    {
    	SEV_INFO,
    	SEV_WARNING,
    	SEV_ERROR,
    }
    
    enum DebugLevel : Int32
    {
    	DBL_USER,
    	DBL_1,
    	DBL_2,
    }
    
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    struct IPCArgs
    {
    	public ComponentIdent reporter;
    	public ComponentIdent source;
    	public Int64 timestamp; // CTime ist eine Klasse und kann nicht übertragen werden.
    	[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 2048)]
    	public string message;
    	public Int32 code;
    	public Severity severity;
    	public DebugLevel debugLevel;
    }
    
    // ...
    
    [DllImport("coredll.dll")]
    private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
    
    // ...
    
    var args = new IPCArgs();
    // Fülle die Daten
    
    var cd = new COPYDATASTRUCT();
    cd.dwData = IntPtr.Zero;
    cd.cbData = Marshal.SizeOf(args);
    
    var argsHandle = GCHandle.Alloc(args, GCHandleType.Pinned);
    cd.lpData = argsHandle.ToIntPtr();
    
    var cdHandle = GCHandle.Alloc(cd, GCHandleType.Pinned);
    
    SendMessage(hwnd, WM_COPYDATA, IntPtr.Zero, cdHandle.ToIntPtr());
    
    cdHandle.Free();
    argsHandle.Free();
    

    Es ist alles ungetestet und einfach mal so schnell während der Vorlesung eingetippt. Aber ich hoffe es hilft dir schonmal weiter 😉

    Wichtig wäre wohl noch zu sagen, dass du bei den GCHandle.Alloc nicht die Objekte selbst pinst sondern eine Kopie von diesen, da GCHandle.Alloc ein object erwartet und die Strukturen daher vorher ein Boxing durchlaufen. Heisst du musst sie zuvor füllen und danach pinnen. Wenn du es umgekehrt machst, werden deine Daten nicht übertragen.

    Es ist zudem zu empfehlen, das Ganze exceptionsicher zu machen, heisst dass die Handles immer freigegeben werden, sonst hast du hier ein Memory-Leak. Freigeben solltest du die Objekte natürlich erst, wenn sie nicht mehr verwendet werden.

    Grüssli



  • Hallo Dravere.

    Danke dir schon mal.

    Ein paar Dinge dazu:

    Du hast folgende Funktion verwendet:

    [DllImport("coredll.dll")]
    private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

    Ich hatte diese verwendet.

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

    Dachte hier wird schon direkt COPYDATASTRUCT verwendet.

    Aber im Grunde eigentlich egal. Hauptsache es funktioniert. Tut es leider aber in beiden Fällen noch nicht.

    Folgende Probleme.

    1. Bei der von mir verwendeten Funktion kann man für das hwnd ein int übergeben. Die habe ich mir bisher über FindWindow() geholt. Bei deiner verwendeten ein IntPtr. Ich weiß es gibt noch eine FindWindoxEx. Dass ist aber um einiges komplizierter. Mit Foreach über Process.GetProcessesByName("Prozessname");

    2. Die Deklaration der enums mit "public enum Severity : Int32" geht nicht. Hier gibts ein Fehler bei Int32: Typ "byte", "sbyte", "short", "ushort", "int", "uint", "long" oder "ulong" erwartet.

    3. Bei den 2 aufrufen von ToIntPtr wird ein Argeument erwartet. Bin hier immer noch am überlegen was man da übergeben muss.

    Gruß


  • Administrator

    Marshall schrieb:

    1. Bei der von mir verwendeten Funktion kann man für das hwnd ein int übergeben. Die habe ich mir bisher über FindWindow() geholt. Bei deiner verwendeten ein IntPtr. Ich weiß es gibt noch eine FindWindoxEx. Dass ist aber um einiges komplizierter. Mit Foreach über Process.GetProcessesByName("Prozessname");

    FindWindow gibt dir ein HWND zurück und das ist schlussendlich ein void* . Daher ist wohl die Deklaration deiner FindWindow Funktion falsch. Siehe:
    http://pinvoke.net/default.aspx/user32/FindWindow.html

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

    Marshall schrieb:

    2. Die Deklaration der enums mit "public enum Severity : Int32" geht nicht. Hier gibts ein Fehler bei Int32: Typ "byte", "sbyte", "short", "ushort", "int", "uint", "long" oder "ulong" erwartet.

    int entspricht in C# einem Int32 . Ersetze einfach den Int32 durch ein int . Ich mag es, genau zu sein, bei P/Invoke, da es dort sehr wichtig ist, ob nun etwas ein Int32 oder Int64 oder sonst was ist. Grundsätzlich kannst du aber aber den Teil auch ganz weglassen, da standardmässig ein Enum sowieso einem int entspricht und somit einem Int32 .

    Marshall schrieb:

    3. Bei den 2 aufrufen von ToIntPtr wird ein Argeument erwartet. Bin hier immer noch am überlegen was man da übergeben muss.

    Wups. ToIntPtr ist ja eine statische Funktion...

    GCHandle.ToIntPtr(cdHandle)
    // bzw.
    GCHandle.ToIntPtr(argsHandle)
    

    Siehe dazu auch die Dokumentation.

    Grüssli


  • Administrator

    So, jetzt konnte ich noch kurz selber das durchtesten, noch ein paar Korrekturen sind notwendig. Hab zwei Dinge vergessen:
    1. Beim ersten Objekt darf man nicht GCHandle verwenden, da du darin Strings hast, welche durch den Marshaler umgewandelt werden müssen. Wir können somit nicht einfach das C# Objekt pinnen und dieses verwenden, sondern müssen Speicher reservieren und die Struktur dort reinkopieren. Danach den Zeiger auf diesen Speicherbereich übergeben.
    2. Nicht ToIntPtr verwenden sondern AddrOfPinnedObject , blöder Anfängerfehler 🙄

    var args = new IPCArgs();
    args.reporter.line = 1;
    args.reporter.file = "filereporter";
    args.reporter.proc = "procreporter";
    args.reporter.section = "sectionreporter";
    
    args.source.line = 2;
    args.source.file = "filesource";
    args.source.proc = "procsource";
    args.source.section = "sectionsource";
    
    args.timestamp = 1234;
    args.message = "this is a test";
    args.code = -1;
    args.severity = Severity.SEV_WARNING;
    args.debugLevel = DebugLevel.DBL_2;
    
    var cd = new COPYDATASTRUCT();
    cd.dwData = IntPtr.Zero;
    cd.cbData = Marshal.SizeOf(args);
    
    var argsBuffer = Marshal.AllocHGlobal(cd.cbData);
    Marshal.StructureToPtr(args, argsBuffer, false);
    cd.lpData = argsBuffer;
    
    var cdHandle = GCHandle.Alloc(cd, GCHandleType.Pinned);
    
    SendMessage(hwnd, WM_COPYDATA, IntPtr.Zero, cdHandle.AddrOfPinnedObject());
    
    cdHandle.Free();
    Marshal.FreeHGlobal(argsBuffer);
    

    Das hat bei mir in einem kleinen Testprogramm funktioniert.

    Falls weitere Fragen bestehen, einfach fragen 🙂

    Grüssli



  • Hallo.

    Nochmals vielen Danke für deine Hilfe.

    FindWindow gibt dir ein HWND zurück und das ist schlussendlich ein void*. Daher ist wohl die Deklaration deiner FindWindow Funktion falsch.

    Ok. Hatte ich von dieser Seite hier:

    http://boycook.wordpress.com/2008/07/29/c-win32-messaging-with-sendmessage-and-wm_copydata/

    Hat aber auch Funktioniert. Seltsam.

    Aber nun zu deiner Lösung. Beim Ausführen der Funktion SendMessage erhalte ich den Fehler dass das Modul coredll.dll nicht geladen werden kann.

    Habe nun die SendMessage Funktion aus der user32.dll verwendet. Das funktioniert. Und auch der Rest. Supi.

    Noch ne Frage:
    Hätte es eigentlich mit der Funktion die ich verwendet habe, bei der man direkt die CopyDataStruct übergeben kann auch funktioniert?

    Und noch eine:
    Wo eignet man sich so wissen über das P/Invoke Zeugs an. Gibts da ne Schulung dafür, oder alles selber beibringen?



  • Und noch eine Frage. Ich hoffe es werden nicht zuviel.

    Und zwar zu der CTime Geschichte. Welcher Datentyp wäre hier denn am besten geeignet für beide Seiten bzw. für alle 3 Seiten.

    3 Seiten weil: Momentan sende ich von einem MFC Fenster an ein anderes MFC Fenster und hier hat CTime natürlcih funktioniert. Nun kommt das WPF Fenster hinzu das zusätlich an das MFC Fenster sendet.

    Gibt es einen Datentyp den man in c++ sowie auch in c# verwenden kann um die Zeit + Datum zu verwalten


  • Administrator

    Marshall schrieb:

    Ok. Hatte ich von dieser Seite hier:

    http://boycook.wordpress.com/2008/07/29/c-win32-messaging-with-sendmessage-and-wm_copydata/

    Hat aber auch Funktioniert. Seltsam.

    Nicht wirklich seltsam. Unter einem 32-Bit System ist ein int gleich gross wie ein void* . Und ein Zeiger ist schliesslich nicht viel anderes als eine Zahl. Unter 64-Bit würdest du aber unter Umständen in Probleme reinkommen.

    Marshall schrieb:

    Aber nun zu deiner Lösung. Beim Ausführen der Funktion SendMessage erhalte ich den Fehler dass das Modul coredll.dll nicht geladen werden kann.

    Habe nun die SendMessage Funktion aus der user32.dll verwendet.

    Das ist auch absolut richtig. Das war ein Copy&Paste Fehler von meiner Seite.

    Marshall schrieb:

    Hätte es eigentlich mit der Funktion die ich verwendet habe, bei der man direkt die CopyDataStruct übergeben kann auch funktioniert?

    Ja, wahrscheinlich schon. ref hätte dafür gesorgt, dass ein Zeiger draus wird und dass das InAttribute und OutAttribute angewendet werden. Dadurch würden die Daten korrekt übergeben und es würde sogar dafür gesorgt, dass sie bei Änderungen korrekt zurückgeschrieben werden. Da die Struktur zudem Blittable ist (heisst man kann die Struktur einfach Bit für Bit kopieren und erhält das gleiche, sie besteht ausschliesslich aus primitiven Typen) dürfte der Marshaler dann das gleiche machen, dass er das Objekt einfach im Speicher für den Aufruf pinnt.

    Marshall schrieb:

    Wo eignet man sich so wissen über das P/Invoke Zeugs an. Gibts da ne Schulung dafür, oder alles selber beibringen?

    Ich habe mir alles selber beigebracht über die MSDN Seiten und ein paar wenigen anderen Seiten. Habe dann sogar, um mich selber weiterzubilden, ein Tutorial dazu geschrieben. Aber nie fertiggestellt ... wurde auch viel zu gross. Und aktuell sowieso keine Zeit mehr dazu.

    Marshall schrieb:

    Und noch eine Frage. Ich hoffe es werden nicht zuviel.

    Naja, muss bald auf den Zug 😃

    Marshall schrieb:

    Und zwar zu der CTime Geschichte. Welcher Datentyp wäre hier denn am besten geeignet für beide Seiten bzw. für alle 3 Seiten. ...

    Das Problem, welches du hier hast, ist die Schnittstelle per P/Invoke. Diese kann nur C Datentypen durchreichen. Daher dürfte es wohl am einfachsten sein, wenn du mit einem Unix-Timestamp fährst:
    http://de.wikipedia.org/wiki/Unix-Timestamp

    Den Unix-Timestamp kannst du dann in C# in ein DateTimeOffset umwandeln, bzw. in C++ und MFC dann in ein CTime . CTime aktzeptiert z.B. direkt über den Konstruktur einen Unix-Timestamp:
    http://msdn.microsoft.com/en-US/library/b6989cds.aspx

    Über CTime::GetTime erhälst du den Unix-Timestamp.

    Bei DateTimeOffset musst du TheEpoch (1.1.1970 00:00:00) erstellen und die Sekunden dazurechnen oder die Differenz bilden und dann das Total der Sekunden nehmen.

    Grüssli



  • Hallo Dravere

    Es hat jetzt ziemlich lange gedauert. Aber ich musste das zunächst mal hinten anstellen.

    Ich mache es nun wie folgt:

    DateTime timeStamp = DateTime.Now
    
    Int64 timestamp = (long) (timeStamp - new DateTime(1970, 1, 1).ToLocalTime()).TotalSeconds
    

    Auf meiner c++ Seite habe ich gar nichts geändert. Muss ich ja nicht hast ja geschrieben dass der Konstruktor von CTime einen UnixTimestamp akzeptiert.

    Es sieht nun gar nicht schlecht aus. Bis auf die Tatsache das die angezeigt Zeit nun um eine Stunde verschoben ist. Also wenn ich 9.00 sende kommt 10.00 an.

    Vieleicht hast du mir ja nochmal eine Idee woran das liegen kann.



  • So nochmal ich.

    Habe es gelöst:

    timestamp = (long)(reportServiceEventArgs.TimeStamp.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds
    

    muss meinen lokalen Timestamp erst mal in UTC wandeln. 🙂


Anmelden zum Antworten