Marshall


  • Administrator

    Sorry für die späte Antwort. Habe aktuell viel um die Ohren.

    Zu Pack:
    http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.structlayoutattribute.pack.aspx
    Standardmässig ist der Wert 0 und somit wird der für die Plattform übliche Wert genommen. Wenn du in C++ keine speziellen Kompileroptionen oder Anweisungen bei der Definition der Struktur verwendest, solltest du den Wert auf jedenfall bei 0 belassen. Andere Werte sollte man nur verwenden, wenn man weiss, was man macht.

    Zum Rest:
    Mein Beispiel sollte eigentlich schon fast die Lösung präsentieren, nicht? Gut, es fehlen noch die einen oder anderen Felder. Aber die scheinst du ja schon selbst herausgefunden zu haben. Es kann vielleicht noch von Vorteil sein, dass du ebenfalls struct in C# verwendest. Es gibt nur ein paar wenige Anwendungsfälle, wo man bei P/Invoke mit class einen Vorteil erhält. In den meisten Fällen sollte man besser struct verwenden.

    Eine möglicherweise etwas einfachere Lösung dürfte übrigens noch das folgende sein:

    #include <iostream>
    
    struct header
    {
      int size;
      char data[1];
    };
    
    struct tweet
    {
      char text[140];
      int textlength;
      int timestamp;
    };
    
    #define MY_EXPORT extern "C" __declspec(dllexport)
    
    MY_EXPORT void run_test(header* data)
    {
      std::cout << data->size << " - ";
    
      tweet* t = reinterpret_cast<tweet*>(&data->data[0]);
      std::cout << t->timestamp << " - ";
      std::cout << t->textlength << " - ";
      std::cout.write(t->text, t->textlength);
    
      std::cout << std::endl;
    }
    
    using System;
    using System.Text;
    using System.Runtime.InteropServices;
    
    namespace SharpTest
    {
      [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
      public struct Tweet
      {
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 140)]
        private String m_text;
    
        private int m_textLength;
        private int m_timeStamp;
    
        public String Text
        {
          get { return m_text; }
          set
          {
            m_text = value ?? String.Empty;
            m_textLength = m_text.Length;
          }
        }
    
        public int TimeStamp
        {
          get { return m_timeStamp; }
          set { m_timeStamp = value; }
        }
      }
    
      [StructLayout(LayoutKind.Sequential)]
      public class TweetPacket
      {
        // Hier ist class z.B. von Vorteil, da ich das Feld direkt initialisieren kann.
        // Mit struct wäre der nachfolgende Code nicht möglich.
        private int m_size = Marshal.SizeOf(typeof(Tweet));
        private Tweet m_tweet;
    
        public Tweet Tweet
        {
          get { return m_tweet; }
          set { m_tweet = value; }
        }
      }
    
      public class Program
      {
        [DllImport("NativeLibTest.dll", EntryPoint = "run_test", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
        private static extern void RunTest(IntPtr header);
    
        private static void RunTest(TweetPacket packet)
        {
          int size = Marshal.SizeOf(packet);
          IntPtr dataPtr = Marshal.AllocHGlobal(size);
    
          try
          {
            Marshal.StructureToPtr(packet, dataPtr, false);
            RunTest(dataPtr);
          }
          finally
          {
            Marshal.FreeHGlobal(dataPtr);
          }
        }
    
        private static int TimeStampNow()
        {
          return (int)(DateTimeOffset.UtcNow - new DateTimeOffset(1970, 1, 1, 0, 0, 0, new TimeSpan())).TotalSeconds;
        }
    
        public static void Main(string[] args)
        {
          const string text = "Hello world!";
    
          var packet = new TweetPacket()
          {
            Tweet = new Tweet()
            {
              Text = text,
              TimeStamp = TimeStampNow()
            }
          };
    
          RunTest(packet);
    
          Console.ReadKey(true);
        }
      }
    }
    

    Generics gehen in C# übrigens nicht. Du kannst also kein Packet<TData> oder sowas machen. Marshal.StructureToPtr wird dir da leider eine Exception an den Kopf werfen.

    Ich denke von hier aus, solltest du den Rest selber erarbeiten können? Falls du noch weitere Fragen hast, nur zu! Ich werde aber womöglich nächste Woche nur selten oder gar nicht Antworten können. Muss an eine verblödete Fortbildung...

    Grüssli



  • Danke nochmals für deine Hilfe. Leider fehlt mir aber das wichtigste.

    Und zwar immer noch meine byData. Du wandelst das ganze in deinem Beispiel in ein Intptr weil das deine Funktion Runtest erfordert.

    Bei mir ist das ja aber nicht der Fall. Meine Funktion erfordert eine struct die wiederum das bytearray erfordert.

    Keine Ahnung wie ich das machen soll.


  • Administrator

    Deine Funktion erwartet ein LPDM_SEND_DATA_STRUCTW und das ist nichts anderes als ein tagDM_SEND_DATA_STRUCTW* und ist somit ein Zeiger auf diese Struktur. Dadurch ist es genau das Gleiche.

    Ein Zeiger ist zudem ein Zeiger. Zur Laufzeit macht das keinen Unterschied. Der Typ eines Zeigers ist nur zur Kompilezeit von Relevanz. Wenn daher eine Funktion einen Zeiger erwartet, dann kannst du gerne IntPtr verwenden. Das ist kein Problem.

    Mein C++ Code ist da nur etwas vereinfacht, aber funktioniert genau gleich, wie das was du uns bisher gezeigt hast. Ich habe in der Struktur header ebenfalls ein Byte-Array mit einem Element.

    Kann es sein, dass dir nicht ganz klar ist, wie diese Strukturen in C++ abgebildet werden? Du hast am Ende nur ein einziges Byte-Array. Und in den ersten N Bytes hast du die Header-Struktur drin. In deinem Fall das tagDM_SEND_DATA_STRUCTW . Ab dem Feld byData kommen die restlichen Daten im Byte-Array. byData dient nur als Hilfsmittel, um den Header zu überspringen und keine Zeigeroperationen durchführen zu müssen. Schau dir diesbezüglich Zeile 25 meines letzten C++ Codes an.

    Grüssli


  • Administrator

    Ich wollte gerade ins Bett, da fiel mir auf, dass man den C# ja deutlich einfacher machen kann 😃

    using System;
    using System.Text;
    using System.Runtime.InteropServices;
    
    namespace SharpTest
    {
      [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
      public struct Tweet
      {
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 140)]
        private String m_text;
    
        private int m_textLength;
        private int m_timeStamp;
    
        public String Text
        {
          get { return m_text; }
          set
          {
            m_text = value ?? String.Empty;
            m_textLength = m_text.Length;
          }
        }
    
        public int TimeStamp
        {
          get { return m_timeStamp; }
          set { m_timeStamp = value; }
        }
      }
    
      [StructLayout(LayoutKind.Sequential)]
      public class TweetPacket
      {
        private int m_size = Marshal.SizeOf(typeof(Tweet));
        private Tweet m_tweet;
    
        public Tweet Tweet
        {
          get { return m_tweet; }
          set { m_tweet = value; }
        }
      }
    
      public class Program
      {
        [DllImport("NativeLibTest.dll", EntryPoint = "run_test", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
        private static extern void RunTest(TweetPacket header);
    
        // Die Zwischenfunktion ist gar nicht nötig!
    
        private static int TimeStampNow()
        {
          return (int)(DateTimeOffset.UtcNow - new DateTimeOffset(1970, 1, 1, 0, 0, 0, new TimeSpan())).TotalSeconds;
        }
    
        public static void Main(string[] args)
        {
          const string text = "Hello world!";
    
          var packet = new TweetPacket()
          {
            Tweet = new Tweet()
            {
              Text = text,
              TimeStamp = TimeStampNow()
            }
          };
    
          RunTest(packet);
    
          Console.ReadKey(true);
        }
      }
    }
    

    RunTest kann direkt das Objekt annehmen. Der Marshaler macht alles andere selber. Hier ist nun sehr wichtig, dass wir eine Klasse verwenden, da dies sicherstellt, dass es tatsächlich ein Zeiger auf das Objekt ist. Wenn wir für TweetPacket ein struct verwenden würden, würde es das Objekt als Wert übergeben. Zudem ist das ein OneWay-Ticket. Heisst die Daten werden nur zur Funktion hin kopiert aber nicht zurück!

    Ich hoffe mal, dass dich das jetzt nicht noch weiter verwirrt, aber ich konnte es einfach nicht so stehen lassen 🙄

    Grüssli



  • Hallo Dravere.

    Gut erklärt. Manches verstehe ich auch :-). Leider aber noch nicht alles.
    Stell hier einfach noch ein paar Fragen. Vieleicht kommst du ja dazu um sie zu beantworten. Wäre super.

    Zudem ist das ein OneWay-Ticket. Heisst die Daten werden nur zur Funktion hin kopiert aber nicht zurück!

    ?? Will ich ja. Daten rüber kopieren. Wieso sollte da was zurückkommen. Verstehe ich nicht recht.

    2. Mit der Deklaration des byData[1] stehe ich glaub auf dem Schlauch. Ich möchte eine Schnittstelle bereitstellen die es ermöglich die Funktionen in c# einfach zu verwenden. Der der genaue Typ der Daten ist mir also nicht bekannt. Als was muss ich also das byData in meiner c# Klasse deklarieren, dass es allgemein verwendet werden kann. Ich habe schon verstanden dass ich keine generics verwenden kann.
    Aber es muss doch möglich sein, hier einen allgemeinen Datentypen anzugeben. Byte array oder was ähnliches. Und dazu Hilfsfunktionen bereitstellen die mir dann bestimmte Objekte serialisiert um in dieser Variablen ablegen zu können.

    Du hast ja in deinen Beispielen immer direkt den konkretten Typen Tweet verwendet.


  • Administrator

    OrginellerName schrieb:

    Zudem ist das ein OneWay-Ticket. Heisst die Daten werden nur zur Funktion hin kopiert aber nicht zurück!

    ?? Will ich ja. Daten rüber kopieren. Wieso sollte da was zurückkommen. Verstehe ich nicht recht.

    Das war nur als Warnung gedacht, falls du das mal in einem anderen Fall verwenden würdest. Sollte man sich einfach im Hinterkopf behalten.

    OrginellerName schrieb:

    2. Mit der Deklaration des byData[1] stehe ich glaub auf dem Schlauch. Ich möchte eine Schnittstelle bereitstellen die es ermöglich die Funktionen in c# einfach zu verwenden. Der der genaue Typ der Daten ist mir also nicht bekannt. Als was muss ich also das byData in meiner c# Klasse deklarieren, dass es allgemein verwendet werden kann. Ich habe schon verstanden dass ich keine generics verwenden kann.
    Aber es muss doch möglich sein, hier einen allgemeinen Datentypen anzugeben. Byte array oder was ähnliches. Und dazu Hilfsfunktionen bereitstellen die mir dann bestimmte Objekte serialisiert um in dieser Variablen ablegen zu können.

    Du hast ja in deinen Beispielen immer direkt den konkretten Typen Tweet verwendet.

    Also ich empfehle dir, wenn es nicht zu viele unterschiedliche Typen sind, dann verwende jeweils einen konkreten Typ. Du kannst die Funktion in die DLL in C# überladen und damit andere Strukturen aktzeptieren. Der Vorteil davon ist, dass du z.B. die Integrität gewisser Felder besser garantieren kannst. Z.B. die Länge oder irgendwelche Datentypen-Angaben. Falls das aus irgendeinem Grund nicht möglich ist, dann hier noch ein weiteres Beispiel 🙂

    #include <iostream>
    
    namespace test {
    enum type
    {
      string,
      vector2d,
      tweet
    };
    }
    
    struct data_header
    {
      test::type data_type;
      unsigned int data_length;
      char data[1];
    };
    
    struct vector2d
    {
      double x;
      double y;
      double z;
    };
    
    struct tweet
    {
      int timestamp;
      char text[140];
      int text_length;
    };
    
    void print_header(data_header* header)
    {
      std::cout << header->data_type << " - " << header->data_length << "\n";
    }
    
    void process_string(data_header* data)
    {
      std::cout.write(data->data, data->data_length);
    }
    
    void process_vector2d(data_header* data)
    {
      vector2d* vec = reinterpret_cast<vector2d*>(&data->data[0]);
    
      std::cout << '('
        << vec->x << ','
        << vec->y << ','
        << vec->z << ')';
    }
    
    void process_tweet(data_header* data)
    {
      tweet* t = reinterpret_cast<tweet*>(&data->data[0]);
    
      std::cout << t->timestamp << " - " << t->text_length << "\n";
      std::cout.write(t->text, t->text_length);
    }
    
    extern "C" __declspec(dllexport) void process_data(data_header* data)
    {
      print_header(data);
    
      switch(data->data_type)
      {
      case test::string :
        process_string(data);
        break;
      case test::vector2d :
        process_vector2d(data);
        break;
      case test::tweet :
        process_tweet(data);
        break;
      default :
        std::cout << "Unkown data type!";
      }
    
      std::cout << std::endl;
    }
    
    using System;
    using System.Text;
    using System.Runtime.InteropServices;
    
    namespace PInvokeTest
    {
      enum DataType
      {
        String,
        Vector2D,
        Tweet,
      }
    
      [StructLayout(LayoutKind.Sequential)]
      struct DataHeader
      {
        public DataType dataType;
        public int dataLength;
        public byte data;
      }
    
      [StructLayout(LayoutKind.Sequential)]
      struct Vector2D
      {
        public double x;
        public double y;
        public double z;
      }
    
      [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
      struct Tweet
      {
        public int timestamp;
    
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 140)]
        public String text;
    
        public int textLength;
      }
    
      class Program
      {
        [DllImport("NativeLibTest.dll", EntryPoint = "process_data", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
        private static extern void ProcessData(IntPtr data);
    
        // T muss das StructLayoutAttribute haben
        private static void ProcessData<T>(DataType type, T data)
        {
          var header = new DataHeader();
          header.dataType = type;
    
          header.dataLength = Marshal.SizeOf(typeof(T));
    
          var memorySize = Marshal.SizeOf(header) + header.dataLength;
          var memoryPtr = Marshal.AllocHGlobal(memorySize);
    
          Marshal.StructureToPtr(header, memoryPtr, false);
    
          var dataOffset = Marshal.OffsetOf(typeof(DataHeader), "data");
          Marshal.StructureToPtr(data, memoryPtr + dataOffset.ToInt32(), false);
    
          ProcessData(memoryPtr);
    
          Marshal.FreeHGlobal(memoryPtr);
        }
    
        private static void ProcessText(String text)
        {
          var header = new DataHeader();
          header.dataType = DataType.String;
    
          var data = Encoding.ASCII.GetBytes(text);
          header.dataLength = data.Length;
    
          var memorySize = Marshal.SizeOf(header) + data.Length;
          var memoryPtr = Marshal.AllocHGlobal(memorySize);
    
          Marshal.StructureToPtr(header, memoryPtr, false);
    
          var dataOffset = Marshal.OffsetOf(typeof(DataHeader), "data");
          Marshal.Copy(data, 0, memoryPtr + dataOffset.ToInt32(), data.Length);
    
          ProcessData(memoryPtr);
    
          Marshal.FreeHGlobal(memoryPtr);
        }
    
        static void Main()
        {
          const string tweetText = "Tweet, Tweet, Tweet";
    
          ProcessText("Hello world!");
    
          ProcessData(DataType.Tweet, new Tweet()
          {
            text = tweetText,
            textLength = tweetText.Length,
            timestamp = 1234
          });
    
          ProcessData(DataType.Vector2D, new Vector2D()
          {
            x = 13.234,
            y = 122.3,
            z = 0.23
          });
    
          Console.ReadKey(true);
        }
      }
    }
    

    Der Trick dürfte hier Marshal.OffsetOf sein. Fragen? 😃

    Die entscheidenden Funktionen dürften hier in C# ProcessData und ProcessText sein. Das Vorgehen ist eigentlich sehr einfach. Wir reservieren genügend Speicher am Stück für den Header und die Daten. Und die Daten starten halt dort, wo im Header das Feld data liegt. In deinem Fall wäre dies das Feld byData , bzw. dessen eines Element. Ist wirklich keine Hexerei.

    Grüssli



  • Hallo Dravere.

    Ich glaub ich bin zu dumm um das ganze zu kapieren. In meinem c++ DLL kommt nur Müll an.

    Ich habe mir glaube ich zu viel auf einmal vorgenommen. Habe nun mal versucht nur einen String an c++ weiterzuleiten. Nicht mal das klappt.

    #define EXPORT extern "C" __declspec(dllexport)
    
    Export bool FWSendString(char text[140])
    {
    	return true;
    }
    
    [DllImport("Forwarder.dll", EntryPoint = "FWSendString", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Winapi)]
    public static extern bool FWSendString([MarshalAs(UnmanagedType.ByValTStr, SizeConst = 140)] string text);
    
    public void TestForwarder()
    {
        FWSendString("Hallo");
    }
    

    Fehler beim Ausgeben des System.Runtime.InteropServices.MarshalAsAttribute-Attributs -- "Specified unmanaged type is only valid on fields.".



  • Mich haut es von den Socken. Ich habs.

    Also mein eigentliches Problem. *Freu*

    Vielen Dank.

    Das mit dem einzelnen String versenden geht allerdings immer noch nicht. Irgendwas ist da noch falsch.


  • Administrator

    OrginellerName schrieb:

    Ich habe mir glaube ich zu viel auf einmal vorgenommen. Habe nun mal versucht nur einen String an c++ weiterzuleiten. Nicht mal das klappt.

    Vielleicht solltest du auch deine C und C++ Kentnisse etwas auffrischen.
    http://ideone.com/K8SpTI
    Frag dich hierzu z.B. mal, wieso ich an die Funktion einen Zeiger übergeben kann. Und wie muss daher die korrekte Signatur der Funktion sein?

    OrginellerName schrieb:

    [code="cs"][DllImport("Forwarder.dll", EntryPoint = "FWSendString", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Winapi)]
    public static extern bool FWSendString([MarshalAs(UnmanagedType.ByValTStr, SizeConst = 140)] string text);

    Und die CallingConvention ist hier ebenfalls verkehrt. Wie kommst du auf WinAPI? WinAPI bedeutet normalerweise nämlich stdcall. C setzt dagegen auf die cdecl Aufrufkonvention. Solche Dinge müssen unbedingt übereinstimmen!

    Aber freut mich, dass du dein eigentliches Problem gelöst hast.

    Grüssli



  • Wie kommst du auf WinAPI?
    

    Das wird in den Beispielen die ich habe verwendet. Es funktioniert mit dieser DLL auch nur mit WinAPI. Keine Ahnung warum.


Anmelden zum Antworten