Thomas Wölfers Baustatik-Blog

Thomas Wölfers Baustatik Blog

Von Managed C++ nach C++/CLI


In VS 2008 ist die Syntax von Managed C++ bereits deprecated - höchste Zeit, auf C++/CLI umzustellen. Die ausführliche Referenz dazu befindet sich hier: C++/CLI Migration Primer - ich denke für die meinsten Fälle kommt man aber mit einer Kurzfassung aus:

Zeiger
Ein Vorteil von C++/CLI über die "managed Extensions" ist die Tatsache, das viele Dinge viel schöner hingeschrieben werden können - und auch besser zu erkennen sind. Das geht mit Zeigern los: Ein Zeiger in den Managed Heap wird nun mit ^ statt mit * markiert:  Faselwurst^ pWurst zeigt also auf eine Instanz vom Typ Faselwurst, die sich im Managed Heap befindet.

Typen
Bisher wurde eine Klasse als gc__ class TypeName definiert - jetzt heist es statt dessen "ref class TypeName".

Arrays
Bisher war ein Array aus Instanzen im Managed Heap ein gc__[] variablenName TypeName. Das heist nun array<Type>(Dimension). Ein Zeiger auf so ein Array ist also ein array<Type>^ varName.

Properties
Die neue Syntax für Properties ähnelt doch sehr stark der von C#:
property int Zahl
{
    int get()
    {
    }
    void set( int v)
    {
        member = v;
    }
}

Interface-Implementierungen
Implementiert eine Klasse ein Interface, dann müssen die implementierten Member nun "virtual" sein.

Die bisher aufgeführten Änderungen sind weiter nicht dramatisch, sonfern man weiss, wie man die Dinge hinschreiben muss: Wirklich falsch machen kann man eigentlich nichts, denn der Compiler bewirft einen schon mit passenden Fehlermeldungen. Das ist in einem besonderen Fall aber nicht so, und dabei geht es um die Destruktoren. Das Problem: Der Compiler meckert hier nichts an, denn man kann zwar weiterhin die "alte" Destruktor-Notation verwenden: Dummerweise hat sich aber deren Semantik verändert. Macht man die Sache aber falsch und reagiert nicht auf die Semantik-Änderung, gibt es Speicher-Lecks im nativen Heap. Das passiert, wenn man eine C++/CLI Instanz hat, die als Member Zeiger auf den nativen Heap hat. Die Instanz an sich wird bei Notwendigkeit brav vom GC eingesammelt und verworfen. Dabei wird der normale Destruktor aber nicht unbedingt aufgerufen - zumindest nicht, wenn man nicht weiter tätig wird. Resultat: Leck. Das war früher nicht so - daher muss man auf diese Problematik gesondert achten. Die ausführliche Referenz zum Thema findet sich hier.

Im Wesentlichen läuft es aber auf folgendes hinaus: Damit die Destruktoren der C++/CLI Typen "richtig" aufgerufen werden, braucht man immer ein Konstrukt dieser Art:

ref class MyType
{
private:
   int* pNative;

// ..... weitere implementierungs-Details in deren Zuge pNative auf allozierten nativen Speicher zeigt.

public:
   // Das wird im IL ein Dispose()
   ~MyType() { Destruct(); }
   // Das wird im IL ein Finalizer.
   !MyType() { Destruct(); }

private:
   void Destruct()
   {
      if( pNative != 0)
      {
           delete pNative;
           pNative = 0;
      }
   }
};

Nur als Hinweis: Das ist so nicht unbedingt die effizienteste Methode, die Sache zu regeln - daher ist es u.U. ganz sinnvoll den oben verlinkten Artikel doch einmal genauer zu studieren.


Lastweiterleitung im Faltwerk


Beim Faltwerk in der Baustatik ist die Lastweiterleitung eigentlich nicht vorgesehen - schließlich kann man da ja das komplette Gebäude geschlossen eingeben; eine Lastweiterleitung ist also gar nicht notwendig. Nun hatten wir aber kürzlich einen Fall wo das anders war: Die Konstruktion wurde so umfangreich, das sie zusammen mit der Berechnung gar nicht mehr in den Speicher passte - das Gebäude musste also in Teile zerlegt werden. Das ist dann auch passiert, und zwar in diese Dachkonstruktion und den Rest des Gebäudes.

Nun hat die Dachkonstruktion aber knapp 60 Auflager für die im Rahmen einer nichtlinearen Berechnung die Ergebnisse aus circa 180 nichtlinearen Lastfallgruppen ermittelt werden - und alle diese Ergebnisse mussten nun auf die "Dachfläche" des darunter befindlichen Systems übertragen werden. Viel Handarbeit - oder eine passende Stelle um ein Makro einzusetzen. Das im folgenden beschriebene Makro tut dann auch genau das - es überträgt die Auflagerkräfte aus der Berechnung des einen Systems in das andere System. Dabei verwendet es zwar ein paar spezielle Merkmale der Beispielsysteme; das Makro kann aber leicht an andere Systeme angepasst werden.

Um das Makro einfach nur zu verwenden oder einzusehen und abzuändern können Sie einfach die ZIP-Datei herunterladen und auspacken. Darin befindet sich eine .CS-Datei, die Sie zum Beispiel mit Notepad öffnen können. Dann wählen Sie den Befehl "Optionen -> Makro -> Makro erstellen" und geben danach einen passenden Namen für das neue Makro an. Zum Beispiel "Kraftübertragung". Als Programmiersprache wählen Sie "CSharp".

Daraufhin öffnet sich ein Editor-Fenster, in dem schon einige immer benötigte Quellcode-Teile eingetragen sind: Die brauchen Sie in diesem Fall aber nicht und können die bereits vorhandenen Passagen einfach löschen. Danach kopieren Sie den kompletten Makro-Quellcode aus dem Editor (Notepad) in den Makro-Editor. Das wars schon: Sie können das Makro nun benutzen - wenn Sie es nicht noch an Ihre Gegebenheiten anpassen müssen.

So passt man das Makro an

Das Makro kann mit Hilfe einiger Variablen an lokale Gegebenheiten angepasst werden. Diese Variable finden sich in den Zeile 30 - 37.

Die ersten beiden Variablen müssen die Namen der betroffenen Dokumente enthalten: Das eine ist das Dokument aus dem die Auflagerkräfte entnommen werden sollen, das ander ist das Dokument, das neue Einzeleinwirkungen erhalten soll.

Das Makro überträgt die Einzel-Auflagerkräfte aus einem Dokument als Faltwerk-Einzeleinwirkung in ein anderes Dokument. Dabei werden aber nicht pauschal alle Auflagerkräfte übertragen, sondern nur die, die zu Lagern bei einer bestimmten Z-Position gehören.  Diese Position muss man ein Zeile 32 angeben; von Haus aus steht da der Wert "0", weil das die Höhe der Lager in der Beispieldatei war.

Beim übertragen der Lagerkräfte passieren verschiedene Dinge: Zum einen wird für jede nichtlineare Lastfallgruppe im Quelldokument ein Lastfall im Zieldokument erzeugt. Damit man diese neuen Lastfälle einfacher wiederfinden kann, erhalten die als Namen den Namen der ursprünglichen nichtlinearen Lastfallgruppe, mit dem Prefix "I_" (für "i"mportiert). Dieses Prefix kann man ändern, und zwar in Zeile 33.

Dann wird für jedes Auflager im Quelldokument eine Faltwerks-Einzeleinwirkung im Zieldokument angelegt. Die Faltwerkselement-Einzeleinwirkungen benötigen als Parameter ein Faltwerkselement: Das hatte bei der Beispieldatei den Namen "6OG Decke" - und darum ist das der Namen, der in Zeile 34 angegeben wurde: Bei einem etwas "vollständigerem" Beispiel würde man statt dessen vielleicht das "richtige" Faltwerkselement noch programmatisch suchen - im Beispiel ist das aber nicht notwendig. Zusätzlich zum Faltwerkselement brauchen die Einzeleinwirkungen auch noch einen Knoten: Alle Knoten haben dabei logischerweise die gleiche Höhe - im Beispiel ist die bei "-21.4 m": Diesen Wert kann man in Zeile 35 abändern.

Und schließlich müssen die Knoten natürlich auch eine X- und eine Y-Position haben. Nun müssen aber die X/Y Koordinaten des Quellsystem nicht unbedingt an der gleichen Stelle liegen, wie die des Zielsystems: Für diesen Fall sieht das Makro in Zeile 36 und 37 noch einen Offset vor, um den die Positionen verschoben werden können.

So benutzt man das Makro

Das Makro operiert mit zwei Dokumenten: Dem "Quelldokument" das die Auflagerkräfte enthält und das "Zieldokument" in dem die neuen Einzeleinwirkungen landen. Beide Dokumente müssen zum Zeitpunkt der Ausführung des Makros geladen sein, sich also in einem Arbeitsfenster befinden. Es muss auch möglich sein, das Quelldokument zu berechnen. Ist dies gegben und sind alle lokalen Gegebenheiten über die weiter oben erwähnten Variablen angepasst, so kann man das Makro einfach ausführen.

Wer mehr über den Programmcode des Makros wissen möchte kann sich den Quellcode ansehen: Der ist recht umfangreich mit Kommentaren dokumentiert.

makro_kraftuebertragung.zip (1.74 KB)

Ein Witz


int GetRandomNumber()
{
   return 5; // mit wuerfel ermittelt und garantiert zufaellig.
}


Merkwürdigkeiten beim .Net Garbage Collector


Irgendwie scheint der .Net 2.0 Garbage-Collector einige Dinge anders zu tun, als ich bisher gedacht habe. Vermutlich liegt das daran, das ich irgendwas falsch mache, aber zur Zeit sieht es eher danach aus, das er auf einer Single-Core Maschine nicht richtig funktioniert... Zur Erklärung hier ein paar Graphiken:

Die drei Graphiken geben den Informationen über den Speicherverbrauch der Baustatik im Zuge von etwa 350 durchgeführten Testcases wieder. Jedes Pixel in X steht dabei für einen Testcase. (Die dritte Graphik ist ein bisschen kürzer, weil aufgrund von Speichermangel nicht alle Testcases durchgeführt werden können - das Programm stürzt mit einer OutOfMemory Exception ab.)

Das Programm arbeitet also eine Reihe von Fällen ab. Am Ende jedes Falles werden 3 Daten erhoben:

  • Die Speicherauslastung bezogen auf den verfügbaren Speicher. (Blaue Kurve)
  • Das Working-Set (Grüne Kurve [ Environment.WorkingSet ])
  • Die Speichermenge, die der GarbageCollector verwaltet. (Rote Kurve [GC.GetTotalMemory(false) ])

In allen drei Fällen wurde das exakt gleiche Binary und die exakt gleichen Testcases durchgeführt. Die erste Graphik kam dabei auf einem Quad-Core System zustande, die zwei auf einem Dual-Core und die dritte auf einem Single-Core System. Wie man leicht sehen kann, gibt es einen unmittelbaren Zusammenhang zwischen dem Working-Set und dem GC-Speicher. Was man auch sehen kann ist die Tatsache, das sich das Programm auf den beiden Multi-Core Systemen exakt so verhält, wie man es erwarten würde: Zwar gibt es an einigen Stellen ein Wachstum im Speicherbedarf, am im großen und ganzen ist der konstant - durchbrochen von leichten Sprüngen, die daraus resultieren, das der GC hin- und wieder eingreift und Speicher freigibt.

Soweit, sogut. Nicht so gut: Auf der Single-Core Maschine wird offensichtlich überhaupt kein Speicher freigegeben. Es wird immer mehr und mehr alloziert, bis das Programm schließlich per OutOfMemory Exception stirbt.

Nun war eine erste (nicht ganz unplausible) Vermutung die, das der GC-Thread auf der Single-Core Maschine einfach nie "dran" kam, weil das durchführen der Testcases einfach zu viel Last machte. Darum habe ich zu Testzwecken nach der Durchführung jedes Testcases und vor dem ermitteln der Speicherauslastung noch folgenden Code eingebaut:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Das sollte nach meinem dafürhalten und auf Basis der Dokumentation dazu führen, das auf jeden Fall ein Collect über alle Generationen durchgeführt und auch Finalizer aufgerufen werden. Und auf den Multicore-Systemen hat das auch die erwarteten Auswirkungen: (Hier nur eines der beiden Bilder)

Wenn man hier den Speicher ansieht, der dem GC unterliegt (rote "Kurve"), dann verhält sich er so wie man das erwarten würden: Nach jedem Collect() wird die gleiche Menge benötigt. Dummerweise sieht das auf der Single-Core Maschine anders aus:

Hier wird weiter fröhlich Speicher alloziert und nicht wieder freigegeben - und zwar noch schneller, als ohne das Collect() - die Anwendung stürzt noch deutlich früher mit einer OutOfMemory Exception ab.

Wäre sehr dankbar, wenn mir das jemand erklären könnte.


PDC 2008


Die ersten Keynotes und Session-Videos sind nun On-Demand verfügbar. (Für die Sessions muss man in den Session-Kalender, dann die Session auswählen, und dann im Bereich rechts der Beschreibung den Link "Video" anklicken. Es sind natürlich noch nicht alle Sessions verfügbar... schließlich läuft die Veranstaltung noch bis morgen. Die Sessions vom 1. Tag sind aber bereits da.)


Gestern in München:Dirk Primbs Vortrag zu WPF, der entfesselte Client reloaded


Angesetzt war die Sache von 18.00 bis 22.00 - und ich muss gestehen, ich bin gegen 20.30 gegangen. Dirk war (wie immer) sehr unterhaltsam, aber inhaltlich kam einfach nicht wirklich viel neues rüber. Er sage von vornherein das es sich um einen Vortrag handeln würde, den er vor 2 (3?) Jahren schon mal gehalten hätte und das der aktuelle im wesentlichen das neue Toolset (VS 2008, Expression) berücksichtigen würde - aber ich hatte mir dann doch ein wenig mehr vorgestellt.

Wer sich noch nie mit XAML, den Expression-Tools oder WPF auseinandergesetzt hat, der war auf der Veranstaltung ganz sicher gut aufgehoben - und mein Eindruck war, das traf auf fast alle Anwesenden auch zu. Aber: Wenn man nur ein paar grundlegende Dinge zum Thema schon wusste, dann war der Vortrag einfach nicht interessant. Natürlich war es ganz nett zu sehen, das man Dinge nun mit "richtigen" Tools statt mit einem Texteditor in XAML anstellen kann - aber das geht auch, indem man sich ein bischen mit dem frei verfügbarem Toolset oder den Demos von Blend auseinandersetzt.

In kürze: Ich empfand meine Zeit auf der Veranstaltung nicht gut eingesetzt, hatte aber den Eindruck, das es anderen Besuchern anders ging. Was mich, um ehrlich zu sein, wunderte. Beim nächsten Mal muss ich vielleicht ein bisschen genauer auf die Beschreibung der Veranstaltung sehen, bevor ich die Zeit dafür vorsehe.


Alter cast oder neuer cast ?


In C# gibt es neben dem C-Style cast "(Foo)" auch die Möglichkeit mit dem "as" Operator eine Typumwandlung durchzuführen. Wann verwendet man aber welchen? Gute Frage - es gibt meiner Ansicht nach aber eine einfache Regel: Wenn die Typumwandlung auf jeden Fall möglich sein muss, dann will man den "alten" cast, gibt es im Code später hingegen noch eine Behandlung, ob die Umwandlung mit "as" geklappt hat - und nur dann - verwendet man den "as".

Zum Beispiel:

// Hier ist es zwingend, das o ein BarObj ist.
void foo( object o)
{
   BarObj bar = (BarObj)o; // <- Wenn das nicht geht, gibt es an dieser Stelle eine Exception.

   // .... mehr code
}

// Hier wird erwartet, das auch was anderes ankommen kann
void foo2( object o)
{
   BarObj bar = o as BarObj;

   if( bar != null)
   {
       // Ohne einen solchen Vergleich auf null ist der "as" nicht angebracht.
   }
}


Bleche im Stahlprofileditor verschieben und kopieren


Im Faltwerk gehts, im Stahlprofileditor fehlt er leider, der Befehl "Geometrie verschieben". Das ist lästig, wenn man zusammengesetzte Querschnitte aus importierten Querschnitten anlegen will, denn mit einem einfachen "Kopier" Befehl geht diese Arbeit viel leichter von der Hand.

Als Lösung bis der "richtige" Befehl verfügbar wird, hier ein kleines Makro, das die zur Zeit ausgewählten Bleche um ein Delta in Y und Z verschiebt, und dabei Kopien anlegt. Knoten werden bei Bedarf ebenfalls kopiert. (Bitte Achtung: Kein offizielles Programmstück, nicht supportet oder gewartet und schon gar nicht ausführlich getestet... Sollte aber gehen :-) )

--

using System;
using DIE.Framework.ApplicationModell.Scripting;
using DIE.Framework.ApplicationModell.Commands;
using DIE.Framework.DocumentModell;
using DIE.Framework.ObjectModell;
using DIE.Geometry;

using DIE.Applications.ProfileEditor.Objects.Nodes;
using DIE.Applications.ProfileEditor.Objects.Beams;

namespace MeineMakros
{
 public class SPE : UserCommandBase
 {
    private class Dyz
    {
   private double y,z;
       public double Y { get { return y;} set { y = value; }}
       public double Z { get { return z;} set { z = value; }}
    }
 
    /*
       * Initialisierung des Makrobefehls.
       */
  public SPE() : base(@"spe", @"Kopiert und verschiebt ausgewählte Bleche im Stahlprofileditor")
  {
  }
  
  /*
   * Einsprungsfunktion des Makros
   */
  public override void Execute(IExecuteCommandContext context)
  {
   System.Windows.Forms.MessageBox.Show(this.TooltipText, this.CommandName);
   
   Dyz dyz = new Dyz();
   MacroTools.EditValue( dyz);
   XVector3d delta = new XVector3d( 0, dyz.Y, dyz.Z);
   
   IDocObjectCollection beams = context.TargetDocument.GlobalSelection.GetObjects( typeof( Beam ));
   if( beams.Count == 0)
   {
    System.Windows.Forms.MessageBox.Show("Es sind keine Bleche ausgewählt.");
    return;
   }
   
   foreach( Beam beam in beams)
   {
      Beam copy = Beam.CreateNamed( context.TargetDocument );
      copy.Dicke = beam.Dicke;
     
      XPoint3d pntStart = beam.Anfangsknoten.Punkt;
      XPoint3d pntEnd = beam.Endknoten.Punkt;
     
      Node nodeBegin = Node.Find( context.TargetDocument, pntStart + delta);
      if( nodeBegin == null)
      {
         nodeBegin = Node.CreateNamed( context.TargetDocument, pntStart + delta );
         context.TargetDocument.AddObject( nodeBegin );
    }
     
      Node nodeEnd = Node.Find( context.TargetDocument, pntEnd + delta);
      if( nodeEnd == null)
      {
     nodeEnd = Node.CreateNamed( context.TargetDocument, pntEnd + delta);
     context.TargetDocument.AddObject( nodeEnd );
      }
     
      copy.Anfangsknoten = nodeBegin;
      copy.Endknoten = nodeEnd;
     
      context.TargetDocument.AddObject( copy );
   }
  }
 }
}

--