Artikel

Juli 2004 | Artikel

Bitweise heiter

(Link zum Artikel: http://www.it-republik.de/dotnet/artikel/0594)

Speicherzugriffe auf Bit-Ebene mit .NET

Text: von Udo Killermann
Dunkle Wolken ziehen mit der .NET-Architektur am Horizont der Systemprogrammierung auf, darüber sind sich die Experten einig. Viel zu leichtfertig gaben die Sprachdesigner gemeinsam mit den Entwicklern der Laufzeitumgebung eine wichtige Eigenschaft der C-Sprachen auf, die eine komfortable Bearbeitung von Bitfeldern erleichterte. C# kennt die Vereinbarung von Abschnitten in einem Maschinenwort nicht und so verlieren Programme, die auf diese Eigenschaften angewiesen sind, ihre Schlichtheit und werden schlechter lesbar. Stattdessen greifen die Systemprogrammierer in ihre Werkzeugkiste und holen Visual C++ Embedded hervor, das diesen Anforderungen spielend gerecht wird.

Ein Silberstreif am Horizont taucht in der jüngsten Zeit in den Newsgruppen auf, die darauf hinweisen, dass C# lernfähig ist und entweder durch Bitoperatoren oder Compiler-Meta-Tags die fehlenden Eigenschaften nachrüsten kann.Bis vor Kurzem beobachtete ich die aufziehenden Wolken aus der Ferne - mein letztes Projekt, das die genannten Eigenschaften benötigte, lag längere Zeit zurück. Dann aber klingelte das Telefon und am Ende des Gesprächs hatte ich den Auftrag angenommen, der mir die Kombination Pocket PC mit .NET Compact Framework und C# vorgab. Auf dieser Plattform sind eigentlich alle Programme systemnah, da das Betriebssystem dieser Smart Devices ihrer jeweiligen Hardware auf den Leib geschneidert wurde. Und in der .NET Compact Framework-Implementierung klaffen große Lücken in Bezug auf die Plattformintegration. So stand ich unvermittelt unter den dunklen Wolken und hoffte auf besseres Wetter.

Ausgangssituation
C und C++ kennen die einfache Vereinbarung von Bitfeldern innerhalb einer Struktur. Der Programmierer legt mit einem Doppelpunkt hinter einem Mitglied gefolgt von einer Ganzzahl fest, wie viele Bits dieses Mitglied innerhalb eines Maschinenwortes einnimmt. Listing 1 zeigt die Struktur eSymbol, die aus einer Terminalemulation stammt und die Speicheraufteilung von 16 Bit vereinbart. Die unteren 4 Bit belegen die Attribute fett, blinken, invers und unterstrichen mit je einem Bit, gefolgt von der Farbe des Zeichens in den darauf folgenden 4 Bit (0-15) und schließlich dem ASCII-Zeichen, das 8 Bit (0-255) beansprucht. Mitglieder dieser Bit-Struktur adressiert der Programmierer genauso wie jeden anderen Zugriff auf Mitglieder einer normalen Struktur. Lediglich bei der Vereinbarung der Struktur sieht der Leser des Quelltexts, dass er eine Bitstruktur vor sich hat, im weiteren Verlauf des Programms muss er an dieses Detail nicht mehr denken - der Compiler übernimmt hier die Kopfarbeit.

Listing 1
  1. // structCpp.cpp : Defines the entry point for the console application.
  2. //
  3. #include "stdafx.h"
  4. struct eSymbol {
  5. unsigned int fett:1;
  6. unsigned int blinken:1;
  7. unsigned int invers:1;
  8. unsigned int unterstrichen:1;
  9. unsigned int farbe:4;
  10. unsigned int symbol:8;
  11. };
  12. union uSymbol {
  13. struct eSymbol info;
  14. unsigned short wert;
  15. };
  16. void writeShort(unsigned short arg)
  17. {
  18. printf("argument: %x\n",arg);
  19. }
  20. unsigned short readShort(void)
  21. {
  22. return 0x46C5;
  23. }
  24. int _tmain(int argc, _TCHAR* argv[])
  25. {
  26. union uSymbol lSymbol;
  27. char c;
  28. lSymbol.wert=0;
  29. lSymbol.info.fett=1;
  30. lSymbol.info.farbe=12;
  31. lSymbol.info.symbol='A';
  32. writeShort(lSymbol.wert);
  33. lSymbol.wert=readShort();
  34. printf("\nSymbol: %x\n",lSymbol.wert);
  35. printf("Zeichen: %c\n",(lSymbol.info.symbol));
  36. printf("Farbe: %d\n",(lSymbol.info.farbe));
  37. if (lSymbol.info.fett)
  38. puts("FETT ist gesetzt");
  39. if(lSymbol.info.blinken)
  40. puts("BLINKEN ist gesetzt");
  41. if(lSymbol.info.invers)
  42. puts("INVERS ist gesetzt");
  43. if(lSymbol.info.unterstrichen)
  44. puts("UNTERSTRICHEN ist gesetzt");
  45. scanf("%c",&c);
  46. return 0;
  47. }
Hinzu kommt die Anforderung, die Speicherzelle mal als Bitstruktur und mal als Ganzzahl zu lesen. C/C++ bieten hierzu die so genannten Unions, mit denen die Überlappung von Speicherzellen vereinbart werden kann. So greift das Programm wie durch einen Filter mal auf den Speicher als 16 Bit breite unsigned int und mal auf die 16 Bit gemäß der Bitstruktur zu. Die union-Vereinbarung erzeugt unterschiedliche Schnittstellen, über die auf einen Speicherbereich jeweils passend für die aktuelle Anforderung zugegriffen werden kann. Die Union uSymbol (siehe Abb. 1) ermöglicht über das Mitglied wert den Zugriff auf die Bitstruktur eSymbol als unsigned int. Listing 1 vermittelt einen Eindruck von der Schlichtheit, mit der in C komplexe Operationen formuliert werden.
Kerninghan und Ritchie achteten sehr auf die Systemnähe des C-Compilers und spendierten ihrer neuen Sprache eine Reihe von Bit-Operatoren. So kann C alle Operationen beschreiben, die bis dahin Assembler vorbehalten waren. Die Autoren verfolgten das Ziel, dass 98 Prozent des Unix-Programmcodes in portablem C und lediglich die verbleibenden 2 Prozent für eine neue Prozessor- oder Systemarchitektur in Assembler erstellt werden mussten. C++, das genau wie C in den Bell Labs von AT&T das Licht der Welt erblickte, rettete alle systemnahen Eigenschaften seiner Vorgängerin und stellte so die Weiterverwendung bestehender C-Lösungen sicher. Java verabschiedete sich zwar von den Bitfeldern und überlappenden Speichervereinbarungen, behielt aber die mächtigen Bitoperatoren bei. Mit einigen Klimmzügen können Programme so formuliert werden, dass alle drei Sprachen die maschinennahen Programmzeilen ohne Änderung verstehen.
Talweg
Um dem Regen zu entkommen, stieg ich hinab ins Tal, kramte in meinem Werkzeugkasten und zog die Bitoperatoren hervor. Genauso behelfen sich die meisten C#-Programmierer, deren maschinennahen Module ich bei meiner Recherche fand. Der Kasten Bitoperatoren fasst die grundlegenden Operatoren im Schnelldurchgang zusammen. Für meine Zwecke benötige ich bitweises und (&), oder(|) und zu guter Letzt das Schieben von Bit nach links (<<) und rechts (>>). Der Kasten Schieberei vermittelt den Umgang mit Bit-Masken, die für die Bearbeitung von Bitmustern in einem Speicherbereich unbedingt erforderlich sind.

Bitoperatoren
BO interpretieren die Operanden auf Bit-Ebene. Ein Bit speichert entweder den Zustand gesetzt (1) oder gelöscht (0). Bit-Arithmetik basiert auf der Bool'schen Algebra, einer Aussagelogik, die mit WAHR oder FALSCH arbeitet. Die Wirkung der BO wird in Aussagen formuliert und in Wahrheitstablen notiert.Das Ergebnis einer UND-Verknüpfung (&-Operator) ist nur dann WAHR, wenn beide Operanden WAHR sind.Das Ergebnis einer ODER-Verknüpfung (|-Operator) ist nur dann FALSCH, wenn beide Operanden FALSCH sind.Das Ergebnis einer EXKLUSIV-ODER-Verknüpfung (^-Operator) ist WAHR, wenn entweder der eine oder der andere Operator WAHR ist.

A B & | ^
0 0 0 0 0
1 0 0 1 1
0 1 0 1 1
1 1 1 1 0


Das Ergebnis des NICHT-Operators (~) ist nur dann WAHR, wenn der Operand FALSCH ist.

A ~
0 1
1 0




Schiebe-Operatoren
Zwei absolute Exoten, die den langen Weg von C bis C# überlebt haben, sind die Schiebe-Operatoren, die das gesamte Bitmuster einer Speicherstelle um beliebige Positionen nach links (<<-Operator) oder nach rechts (>>-Operator) verschieben können. Wobei Bits nach links oder rechts aus der Speicherzelle herausfallen und auf der jeweils anderen Seite ein Bit mit dem Wert 0 in den Speicher geschoben wird.Werden die Bits einer Speicherstelle durch weiße (1) und schwarze (0) Perlen auf einem Draht dargestellt, wobei auf dem Draht genau 8 Perlen aufgefädelt werden können, so können die Schiebeoperationen einfacher begriffen werden. Werden die Perlen um eine Position nach links verschoben, so fällt die Perle ganz links vom Draht und dafür wird eine schwarze Perle rechts auf den Draht geschoben. Ebenso verhält es sich, wenn die Perlen um eine Position nach rechts verschoben werden und die Perle rechts außen vom Draht fällt, so muss ganz links eine neue schwarze Perle aufgefädelt werden.Eine Verschiebung über mehrere Positionen entspricht einer Wiederholung der einfachen Verschiebung und erfordert eine entsprechende Anzahl schwarzer Perlen.


Nachdem ich mit Masken festgelegt habe, auf welchen Bereich des 16 Bit-Werts meine Anweisungen wirken, kann ich mit dem &-Operator diesen Bereich isolieren und anschließend weiterverarbeiten. In Kombination mit dem |-Operator und einer Maske kopiere ich neue Werte in den Zielbereich, ohne die übrigen Bits zu verändern. Da in einer Speicherzelle immer nur ein Wert rechtsbündig in den unteren Bits des Speichers steht, bewegen die Schiebeoperatoren alle anderen Mitglieder in die jeweils richtige Position im Speicher.Listing 2 fasst diese Techniken exemplarisch zusammen und beweist, dass so Programme entstehen, die in ihrer Wirkung Listing 1 entsprechen. Genauso führt Listing 2 aber auch vor Augen, dass diese Bitfummelei mit einer verminderten Lesbarkeit des Programms einher geht. Nicht jeder C-Programmierer kennt die Bitoperatoren oder kann sie zweifels- und fehlerfrei anwenden.

Listing 2
  1. // bitOp.cpp : Defines the entry point for the console application.
  2. //
  3. #include "stdafx.h"
  4. enum masks {
  5. FETT = 0x01,
  6. PFETT = 0,
  7. BLINKEN=0x02,
  8. PBLINKEN=1,
  9. INVERS=0x04,
  10. PINVERS=2,
  11. UNTERSTRICHEN=0x08,
  12. PUNTERSTRICHEN=3,
  13. FARBE=0xF0,
  14. PFARBE=4,
  15. SYMBOL=0xFF00,
  16. PSYMBOL=8
  17. };
  18. void writeShort(unsigned short arg)
  19. {
  20. printf("argument: %x\n",arg);
  21. }
  22. unsigned short readShort(void)
  23. {
  24. return 0x46C5;
  25. }
  26. int _tmain(int argc, _TCHAR* argv[])
  27. {
  28. unsigned short lSymbol;
  29. unsigned short sFarbe;
  30. char c;
  31. lSymbol=0;
  32. sFarbe=12;
  33. lSymbol|=FETT;
  34. lSymbol|=(sFarbe<<PFARBE)&FARBE;
  35. lSymbol|='A'<<PSYMBOL;
  36. writeShort(lSymbol);
  37. lSymbol=readShort();
  38. printf("\nSymbol: %x\n",lSymbol);
  39. printf("Zeichen: %c\n",(lSymbol&SYMBOL)>>PSYMBOL);
  40. printf("Farbe: %d\n",(lSymbol&FARBE)>>PFARBE);
  41. if (lSymbol & FETT)
  42. puts("FETT ist gesetzt");
  43. if(lSymbol & BLINKEN)
  44. puts("BLINKEN ist gesetzt");
  45. if(lSymbol & INVERS)
  46. puts("INVERS ist gesetzt");
  47. if(lSymbol & UNTERSTRICHEN)
  48. puts("UNTERSTRICHEN ist gesetzt");
  49. scanf("%c",&c);
  50. return 0;
  51. }
Der gewundene Talweg schützte mich vor dem Regen, doch ist er umständlich und entbehrt der ursprünglichen Schlichtheit, die eine Kombination von Bitfeldern und unions bietet.

Maskerade
Masken werden in der Fertigungsindustrie genutzt und schützen die Bereiche eines Werkstücks, die nicht bearbeitet werden sollen. So deckt eine Rot-Maske alle Bereiche eines Fahrzeuges ab, die nicht rot lackiert werden sollen.In Analogie zur Fertigung trägt auch das Bitmuster, das den Arbeitsbereich isoliert oder eine Veränderung nur im Arbeitsbereich wirksam werden, lässt den Namen Bit-Maske.Wenn uns aus dem Bitmuster 01001101 lediglich der Wert interessiert, den die Bits 3, 4 und 5 ergeben, so legen wir eine Maske mit dem Muster 00011100 darüber. Eine bitweise und-Verknüpfung von Bitmuster und Maske ergibt den Wert 00001100, der anschließend noch um zwei Positionen nach rechts verschoben werden muss und schließlich 00000011 ergibt. Somit haben wir den Dezimalwert 3 isoliert, der nun im Programm weiter bearbeitet werden kann.
  1. Operand =0x4D; // 01001101
  2. Maske =0x1C; // 00011100
  3. Ergebnis = (Operand & Maske) >> 2;
  4. Console.WriteLine("Ergebnis: {0:x} ", Ergebnis); // Ergebnis:0x3
Wenn wir den Wert 5 in die Bitpositionen 3 bis 5 schreiben wollen, müssen wir ähnlich vorgehen: Zuerst maskieren wir den Arbeitsbereich, indem wir die Maske 01100011 wählen und diese mit dem ursprünglichen Wert über und verknüpfen. Diese Operation ergibt 1000001. So dann notieren wir die Dezimalzahl 5 als Binärzahl 00000101, die wir um zwei Positionen nach links auf die Bits 3 bis 5 verschieben müssen: 00010100. Abschließend verknüpfen wir die maskierte Darstellung mit der verschobenen 5 durch die oder-Operation und erhalten 01010101.
  1. Wert =0x05; // 00000101
  2. Maske=0x63; // 01100011
  3. Ergebnis = (Operand & Maske) | (Wert<<2);
  4. Console.WriteLine("Ergebnis: {0:x} ", Ergebnis); // Ergebnis: 0x55
Da der Compiler die Länge unserer Bitfelder nicht kennt, unternimmt er keine automatisierte Prüfung der Operanden. Wird 9 (1001) als Operand zwei an das Verfahren übergeben, so übersetzt der Compiler die Anweisungen ohne Beanstandung, doch überlappt das höchste Bit nach der zweifachen Schiebung in das ausmaskierte Bitmuster, das von dieser Operation nicht betroffen sein soll. Nach Abarbeitung der Anweisungen lautet das Ergebnis 1100101 und wer weiß, wofür das irrtümlich gesetzte Bit zuständig ist. Mit einer weiteren Maske können wir dieses Verfahren vor unerwünschten Seiteneffekten schützen. Diese Maske schneidet den zweiten Operator auf die zulässige Länge zurecht. Für unser Beispiel lautet diese Maske 111, da nur die unteren drei Bit des Operators berücksichtigt werden dürfen - mehr Platz hat das Bitmuster nicht für diesen Wert. 1001 wird mit und mit dieser Maske verknüpft und staucht das Bitmuster auf 001 zusammen.
  1. Wert =0x09; // 00001001
  2. Schnittt=0x07 // 00000111
  3. Wert = Wert & Schnitt; // 00000001
Bergtour
Vielleicht konnte ich dem Regen ebenso entkommen, wenn ich aus dem Kessel heraus und über die Wolken stiege, das Problem also auf der Meta-Ebene löste. Hier half mir die Online-Dokumentation des .NET Frameworks weiter, nachdem ich nach union gesucht und die Suche auf C# eingeschränkt hatte. Im Namensraum System.Runtime.InteropServices fand ich das Attribut [StructLayout], dem ich mit dem Argument LayoutKind.Explicit mitteilen kann, dass ich die Position der Mitglieder einer Struktur im verwalteten Code festlege und dies nicht der Laufzeitumgebung überlasse. Mit dem Attribut [FieldOffset(n)] gebe ich den Versatz der Mitglieder im verwalteten Speicher an. Der Versatz im Speicher bezieht sich immer auf den notwendigen Speicherbereich eines Datentyps. So zählt der Versatz für bool-Werte genau ein Bit weiter, während der Versatz für ushort 16 Bit weiterzählt. Hier muss der Programmierer konzentriert bei der Sache sein, da er sich sonst schnell den selbst verwalteten Speicher zerschießt. Die Interop Services unterstützen sogar überlappende Mitglieder innerhalb einer Struktur und die Dokumentation beschreibt diese Lösung als legitimen Nachfolger der unions unter C/C++. Besonders häufig benötigen Programmierer diese Eigenschaften, wenn sie Programme erstellen, die mit bereits vorhandenen Win32- oder COM-Modulen zusammenspielen sollen. Durch diese Meta-Tags, die ihren Weg in die .NET-Assembly finden, weiß die Laufzeitumgebung, wie sie die Mitglieder der Struktur in Reih und Glied (engl. to marshal) bringt, bevor diese an die Altumgebung übergeben werden und ebenso, wie sie die Ergebnisse des Aufrufs für die .NET-Anwendung aufbereiten muss.

Listing 3 zeigt die Vereinbarung der selbst verwalteten Struktur, die die gewünschte Anordnung der Struktur-Mitglieder im Speicher sicherstellt. Nachdem ich das Prinzip der Meta-Tags StructLayout und FieldOffset verstanden hatte, erhielt das Programm seine ursprüngliche Schlichtheit zurück. Wie das Mitglied farbe der Struktur zeigt, bewirken die Meta-Tags nicht wirklich eine beliebige Ausrichtung an beliebigen Bitgrenzen. Hier benötige ich trotzdem die Schiebeoperatoren und eine Maske, damit ich die 4 unteren Bits nicht überschreibe.

Listing 3
  1. using System;
  2. using System.Runtime.InteropServices;
  3. namespace structLayout
  4. {
  5. /// <summary>
  6. /// Summary description for Class1.
  7. /// </summary>
  8. class Class1
  9. {
  10. /// <summary>
  11. /// The main entry point for the application.
  12. /// </summary>
  13. // der Parameter von Offset bezieht sich immer auf die Länge des jeweiligen Datentyps:
  14. // - der Offset für bools wird immer um ein Bit weitergezählt
  15. // - der Offset für byte wird immer um 8 Bit weitergezählt
  16. // - der Offset für ushort wird immer um 16 Bit weitergezählt
  17. [StructLayout(LayoutKind.Explicit)]
  18. public struct eSymbol
  19. {
  20. [FieldOffset(0)] public bool fett;
  21. [FieldOffset(1)] public bool blinken;
  22. [FieldOffset(2)] public bool invers;
  23. [FieldOffset(3)] public bool unterstrichen;
  24. [FieldOffset(0)] public byte farbe;
  25. [FieldOffset(1)] public byte symbol; // oberen 8 Bit
  26. [FieldOffset(0)] public ushort wert;
  27. };
  28. [STAThread]
  29. static void Main(string[] args)
  30. {
  31. //
  32. // TODO: Add code to start application here
  33. //
  34. eSymbol lSymbol=new eSymbol();
  35. lSymbol.wert=0;
  36. lSymbol.fett=true;
  37. // hier muss geschoben werden, da keine Werte kleiner 8 Bit vereinbart werden
  38. // können, die Farbe aber nur die oberen 4 Bit belegen soll, schließlich sind
  39. // die untern 4 Bit bereits durch die Attribute belegt!
  40. lSymbol.farbe|=(12<<4);
  41. lSymbol.symbol=(byte)'A';
  42. Console.WriteLine("wert: {0:x}", lSymbol.wert);
  43. Console.ReadLine();
  44. }
  45. }
  46. }
Auch mit dieser Einschränkung isolieren die Meta-Tags die Besonderheiten der Struktur an genau einer Stelle im Code und verbergen diese im weiteren Programmverlauf. Somit stand der Umsetzung meines Projekts nichts mehr im Weg und ich konnte wieder einen robusten Codeausschnitt in meinen Werkzeugkasten stecken. Dies war schließlich der offizielle Weg, was sollte da noch schief gehen?Der Platzregen überraschte mich aus heiterem Himmel und war dafür um so heftiger: Meine Zielplattform ist ja nicht das ausgewachsene .NET Framework, sondern dessen leichtgewichtiger Cousin, der nicht umsonst den Beinamen Compact trägt. Compiler und Laufzeitumgebung arbeiten eng zusammen, um den selbst verwalteten Speicher zu unterstützen. Versteht die Laufzeitumgebung die Meta-Tags nicht, so kann sie die erforderliche Anordnung nicht sicherstellen. Der Compiler erfährt die Fähigkeiten der Laufzeitumgebung über die Informationen aus dem Namensraum System.Runtime.InteropServices und bemerkt, dass das Compact Framework diese Meta-Tags nicht im Vokabular hat und zeigt deren Verwendung als Syntax-Fehler im aktuellen Programmcode an.Die Erkenntnis It`s not a bug - it`s a feature! kostete mich einen Tag und brachte mich dem Ziel meines Projektes keinen Schritt näher. Ich war kurz davor, doch wieder Visual C++ Embedded zu starten und das Lastenheft des Auftrags an die Realität anzupassen.
Fernsicht
Regen in den Bergen kann so schnell vorübergehen und hinterlässt oftmals einen guten Fernblick. Vielleicht erkannte ich deshalb das Brett, das ich vor meinem Kopf hatte, indem ich die Lösung in der Sprache und nicht in der Laufzeitumgebung suchte. Die C#-Entwickler hatten gegenüber den Autoren von C und C++ den großen Vorteil, dass sie .NET in einer Art und Weise mitgestalten konnten, wie dies vorher nicht möglich war. Anders Hejlsberg und sein .NET-Team hatten beschlossen, dass die Manipulation von Bitmustern in jeder .NET-Sprache gleichermaßen unterstützt werden und die Unterteilung in maschinennahe und Rapid Prototyping-Programmiersprachen endgültig der Vergangenheit angehören soll. Die Festlegung einer Eigenschaft für genau eine Programmiersprache wäre in diesem Zusammenhang nicht sinnvoll, da die anderen Sprachen diese Eigenschaft dann nicht verstehen und damit die Forderung nach nahtloser Integration von .NET-Modulen verletzt würde.Meine erweiterte Suche nach dem Stichwort Bit ergab mehr als 500 Treffer im Suchraum .NET Framework. Bereits auf Platz 13 stieß ich auf die Struktur(!) BitVector32, die ausdrücklich auch für das Compact Framework vorhanden ist. Das aufgeführte Beispiel entsprach im Wesentlichen meinen Anforderungen, da es den Zugriff auf die einzelnen Bits eines 32 Bit-Speicherbereichs als Boolean und auf Bereiche derselben Bitfolge als kleine Ganzzahlen dokumentiert.

BitVector32 ist im Namensraum System.Collections.Specialized angesiedelt. Sie bietet die überlappende Vereinbarung von Abschnitten und macht damit die Forderung nach Unions im Sprachumfang von C# überflüssig. Der Name der Struktur verrät bereits, dass ein Element dieser Struktur exakt 32 Bit belegt. Durch die Vereinbarung als Struktur wird der notwendige Speicherbereich bereits bei der Vereinbarung auf dem Stapel reserviert und durch den Aufruf new initialisiert. Dieses Verhalten, das dem der integrierten Werttypen (z.B. short, byte) entspricht, bringt besonders für den Aufruf von Funktionen oder Methoden der Basisplattform über den P/Invoke-Mechanismus einen großen Vorteil: Der Speicherbereich eines BitVector32 kann ohne Konvertierung an API-Aufrufe übergeben werden und auch die Ergebnisse können nach dem Aufruf ohne Konvertierung wieder aus dem BitVector32 gelesen werden.

Listing 4 zeigt die Vereinbarung der Terminalstruktur über BitVector32 und verdeutlicht den Zugriff auf die Elemente des Vektors über den Index-Operator ([]). Auch hier ist die bit- und bereichsweise Segmentierung des Speicherbereichs an einer Stelle der Anwendung isoliert, der Zugriff auf den Speicher kann ohne Kenntnis dieser Besonderheiten erfolgen.

Listing 4
  1. using System;
  2. using System.Collections.Specialized;
  3. namespace termEmu
  4. {
  5. /// <summary>
  6. /// Summary description for Class1.
  7. /// </summary>
  8. class Class1
  9. {
  10. /// <summary>
  11. /// The main entry point for the application.
  12. /// </summary>
  13. [STAThread]
  14. static void Main(string[] args)
  15. {
  16. //
  17. // TODO: Add code to start application here
  18. //
  19. BitVector32 lSymbol;
  20. uint wert;
  21. int mFett, mBlinken;
  22. // Segment-Vereinbarungen
  23. BitVector32.Section fett=BitVector32.CreateSection(0x01);
  24. BitVector32.Section blinken=BitVector32.CreateSection(0x01,fett);
  25. BitVector32.Section invers=BitVector32.CreateSection(0x01,blinken);
  26. BitVector32.Section unterstrichen=BitVector32.CreateSection(0x01,invers);
  27. BitVector32.Section farbe=BitVector32.CreateSection (0x0f,unterstrichen);
  28. BitVector32.Section symbol=BitVector32.CreateSection(0xff,farbe);
  29. // cannot be more than 15 Bit cause of short argument!
  30. BitVector32.Section wertl=BitVector32.CreateSection(0x7fff);
  31. BitVector32.Section werth=BitVector32.CreateSection(0x1);
  32. // Masken-Vereinbarung
  33. mFett=BitVector32.CreateMask();
  34. mBlinken=BitVector32.CreateMask(mFett);
  35. // Initialisierung des BitVector32 Speichers
  36. lSymbol=new BitVector32(0);
  37. // Zugriff über Segmente
  38. lSymbol[wertl]=0;
  39. lSymbol[werth]=0;
  40. lSymbol[fett]=1;
  41. lSymbol[farbe]=12;
  42. lSymbol[symbol]='A'|0x80;
  43. // Zugriff über Maske
  44. lSymbol[mBlinken]=true;
  45. wert=(ushort)((uint)lSymbol[werth]<<15|(uint)lSymbol[wertl]);
  46. Console.WriteLine("Wert: {0:x}",wert);
  47. Console.ReadLine();
  48. }
  49. }
  50. }
Für die einfache Vereinbarung von Speicherbereichen hält die BitVector32-Struktur zwei Varianten der Methode BitVector32.CreateSection bereit. Diese statischen Methoden reservieren zusammenhängende Bit-Bereiche und liefern ihr Ergebnis als BitVector32.Section zurück. Eine Variable vom Type BitVector32.Section speichert Länge und Versatz der Bitfolge im Vektor. Die Vereinbarung benachbarter Bereiche erfolgt schrittweise, wobei BitVector32.CreateSection neben der gewünschten Länge den rechts angrenzenden Bereich erwartet. Die Länge wird nicht explizit übergeben, sondern der Aufrufer nennt die maximale Zahl, die in diesem Speicherbereich abgelegt werden soll. So reservieren die Methoden für die Zahl 7 (111) drei Bit und für die Zahl 10 (1010) 4 Bit.Die Speicherbereiche werden in Listing 4 von rechts außen (fett) nach links (symbol) vereinbart und zusätzlich wird wert mit 16 Bit über die gesamte Breite des segmentierten Speichers gelegt. Allerdings benötigt die Vereinbarung der 16 Bit zwei Bereiche, wertl und werth, die notwendig werden, weil der Zugriff über den Index-Operator einen vorzeichenbehafteten short-Wert erwartet und damit maximal 0x7FFF als Argument gestattet. werth vereinbart das verbleibende 16. Bit, das über den Schiebe-Operator (<<) an die gewünschte Position gebracht wird. Warum das Argument mit einem Vorzeichen versehen wurde, wissen sicherlich die .NET-Autoren, ich verstehe diese unschöne Begrenzung jedenfalls nicht.

Zusätzlich skizziert Listing 4 die Vereinbarung von Masken, die den Zugriff auf die einzelnen Bits des Vektors als bool ermöglichen. Analog zu CreateSection benötigt CreateMask den Verweis auf die rechts angrenzende Maske. Eine Länge wird nicht erwartet, da diese konstant 1 Bit beträgt. CreateMask liefert int-Werte zurück, die sich bei näherer Betrachtung als Zweierpotenzen (1,2,4,8,16...) entpuppen und den Bit-Masken aus dem Talweg entsprechen. Die Attribute fett und blinken können durch diese Vereinbarungen entweder als bool (true, false) oder short (1,0) manipuliert werden.Listing 5 (auf der beiliegenden CD) stellt die VB.NET-Variante der neuen Lösung vor, die gleichwertig zur C# Version aus Listing 4 ist.
Gipfelstürmer
Die neu erarbeitete Lösung mit der BitVector32-Struktur bietet einen gleichwertigen Ersatz für die ursprüngliche Kombination von Bitfeldern und unions. So habe ich die vorgestellten Konvertierungsschritte erfolgreich für die Klassen angewendet, die sich im openNETCF-Projekt (www.opennetcf.org) um die serielle Schnittstelle kümmern. Hierdurch ist der Quelltext gestrafft und auch für Neulinge besser verständlich geworden.

Ein letzter Schritt ist noch geblieben, den Sie nach Lektüre dieses Artikels selbst durchführen können: Indem Sie die BitVector32-Zugriffe in einer Struktur oder Klasse kapseln, können Sie sogar die eingangs dargestellte Formulierung für den Zugriff auf die Elemente der Bitstruktur wiederherstellen. Vorsicht: Beim Einsatz einer Klasse geht der Vorteil der automatischen Speicherbelegung auf dem Stapel verloren.Jetzt leg ich mich wieder in die Sonne


Anzeige

Kommentare

zurück zum Seitenanfang