std.conv — Zahlensysteme und Bitoperationen
Zwei Themen: Zahlen zwischen Textform und Wert wandeln (hexadezimal, binär, oktal, mit Tausendertrennzeichen) und auf einzelnen Bits arbeiten — Bits setzen und lesen, Bitfelder ausschneiden, Nullbits zählen, Byte-Reihenfolge tauschen, Vorzeichen erweitern, Adressen ausrichten.
Alles rechnet auf int64. Die Unit ist der Werkzeugkasten für hardwarenahe Arbeit: Protokollfelder, Register, Binärformate, Fehlersuche.
Autor: Andreas Röne
Copyright: 2024–2025 Andreas Röne
Quelle: std/conv.lyx
<WRAP alert>
RotateLeft32 maskiert nicht auf 32 Bit — 0x80000000 «< 1 ergibt 4294967297 statt 1 (#1495). RotateRight32 arbeitet korrekt.
ParseHex, ParseBin und ParseOct melden ungültige Eingaben nicht und behandeln Störzeichen uneinheitlich (#1500). Details unter Fallstricke.
</WRAP>
Import
import std.conv;
Die Operatoren der Sprache
Für die Grundrechenarten auf Bits braucht es keine Unit — Lyx hat sie eingebaut:
| Operator | Bedeutung | Beispiel |
|---|---|---|
& | UND | 12 & 10 = 8 |
| | ODER | 12 | 10 = 14 |
^ | XOR | 12 ^ 10 = 6 |
~ | Komplement | ~12 = −13 |
<< | Linksschieben | 1 << 10 = 1024 |
>> | Rechtsschieben | 1024 >> 3 = 128 |
|~ | NOR | 12 |~ 10 = −15 |
Zwei Eigenheiten, die man kennen muss:
>> schiebt arithmetisch. Das Vorzeichenbit wird nachgezogen: −16 >> 2 ergibt −4, −1 >> 1 bleibt −1. Für ein logisches Rechtsschieben — bei dem Nullen nachrücken — muss der Wert vorher umgedeutet werden:
var logisch: int64 := (wert as uint64) >> n;
Genau so macht es diese Unit intern in SwapEndian* und RotateRight32.
Schiebeweiten werden modulo 64 genommen. 1 << 64 ergibt 1, 1 << 65 ergibt 2, 1 << −1 ergibt das Vorzeichenbit. Das entspricht dem Verhalten der Prozessorbefehle, wird aber leicht übersehen, wenn die Schiebeweite aus einer Rechnung stammt. Wer sie nicht selbst in der Hand hat, prüft sie vorher gegen 0…63.
Zahlensysteme
Ausgeben
| Signatur | Beschreibung |
|---|---|
IntToHex(val, digits: int64): pchar | Hexadezimal mit fester Stellenzahl, Großbuchstaben |
IntToHex8 · IntToHex16 · IntToHex32 · IntToHex64 | 2, 4, 8 bzw. 16 Stellen |
IntToBin(val, bits: int64): pchar | Binär mit fester Stellenzahl |
IntToBin8 · IntToBin16 · IntToBin32 · IntToBin64 | 8, 16, 32 bzw. 64 Stellen |
IntToOct(val, digits: int64): pchar | Oktal mit fester Stellenzahl |
Negative Werte erscheinen im Zweierkomplement: IntToHex64(−1) ergibt FFFFFFFFFFFFFFFF, IntToBin8(−1) ergibt 11111111.
Reicht die Stellenzahl nicht, werden die höherwertigen Stellen abgeschnitten — IntToHex(255, 1) ergibt F. Es gibt keine Meldung, die Zahl passt also stillschweigend nicht mehr.
Einlesen
| Signatur | Beschreibung | Status |
|---|---|---|
ParseHex(s: pchar): int64 | Hexadezimal; erkennt 0x-Präfix, Groß- und Kleinschreibung | Fehler nicht meldbar (#1500) |
ParseBin(s: pchar): int64 | Binär; erkennt 0b-Präfix | dito |
ParseOct(s: pchar): int64 | Oktal; führende 0 zulässig | dito |
Der Rundlauf funktioniert: ParseHex(IntToHex32(x)) und ParseBin(IntToBin16(x)) liefern den Ausgangswert zurück.
Lesbare Zahlen
| Signatur | Beschreibung |
|---|---|
IntToStrWithSeparator(n, sep: int64): pchar | Dreiergruppen mit frei wählbarem Trennzeichen (Zeichencode) |
IntToStrWithComma(n: int64): pchar | 1,234,567 |
IntToStrWithUnderscore(n: int64): pchar | 1_234_567 |
Für die deutsche Schreibweise IntToStrWithSeparator(n, 46) — 46 ist der Punkt. Negative Zahlen und Werte unter 1000 werden korrekt behandelt.
Bitoperationen
Einzelne Bits
| Signatur | Beschreibung |
|---|---|
GetBit(val, bit_pos: int64): bool | Ist das Bit gesetzt? |
SetBit(val, bit_pos: int64): int64 | Bit setzen |
ClearBit(val, bit_pos: int64): int64 | Bit löschen |
ToggleBit(val, bit_pos: int64): int64 | Bit umschalten |
Alle vier prüfen die Position gegen 0…63. Außerhalb liefert GetBit false, die übrigen geben den Wert unverändert zurück — ein Tippfehler bei der Position bleibt damit folgenlos, aber auch unbemerkt.
Bitfelder
| Signatur | Beschreibung |
|---|---|
ExtractBits(val, start, count: int64): int64 | count Bits ab Position start herausziehen |
InsertBits(val, bits, start, count: int64): int64 | count Bits ab start ersetzen |
Beide prüfen, dass start + count innerhalb von 64 Bit bleibt; sonst kommt 0 bzw. der unveränderte Wert zurück.
Zählen
| Signatur | Beschreibung |
|---|---|
CountLeadingZeros(val: int64): int64 | Führende Nullbits; für 0 sind es 64 |
CountTrailingZeros(val: int64): int64 | Nullbits am unteren Ende; für 0 sind es 64 |
Ein PopCount — die Zahl der gesetzten Bits — gibt es hier nicht, sondern in std.math; dort ist es allerdings für negative Werte fehlerhaft (#1448).
Bytes und Byte-Reihenfolge
| Signatur | Beschreibung |
|---|---|
SwapEndian16(val: int64): int64 | Zwei Bytes tauschen |
SwapEndian32(val: int64): int64 | Vier Bytes umdrehen |
SwapEndian64(val: int64): int64 | Acht Bytes umdrehen |
GetByte(val, byte_pos: int64): int64 | Einzelnes Byte, Position 0 ist das niederwertigste |
SetByte(val, byte_pos, byte_val: int64): int64 | Einzelnes Byte ersetzen |
Rotation
| Signatur | Beschreibung | Status |
|---|---|---|
RotateLeft32(val, n: int64): int64 | Linksrotation über 32 Bit | überschreitet 32 Bit (#1495) |
RotateRight32(val, n: int64): int64 | Rechtsrotation über 32 Bit | ok |
64-Bit-Rotationen gibt es nicht.
Breiten und Ausrichtung
| Signatur | Beschreibung |
|---|---|
SignExtend8 · SignExtend16 · SignExtend32 | Kleinere Breite vorzeichenrichtig auf 64 Bit bringen: SignExtend8(255) = −1 |
ZeroExtend8 · ZeroExtend16 · ZeroExtend32 | Nur die unteren Bits behalten: ZeroExtend8(−1) = 255 |
ClampUint8 · ClampUint16 · ClampUint32 | Auf den vorzeichenlosen Bereich begrenzen statt abzuschneiden |
AlignUp(addr, alignment: int64): int64 | Nächstes Vielfaches nach oben |
AlignDown(addr, alignment: int64): int64 | Nächstes Vielfaches nach unten |
IsAligned(addr, alignment: int64): bool | Bereits ausgerichtet? |
Der Unterschied zwischen ZeroExtend8 und ClampUint8 ist wesentlich: Ersteres schneidet ab (300 wird zu 44), Letzteres begrenzt (300 wird zu 255).
Beispiel
import std.conv;
fn main(): int64 {
PrintLn("-- Zahlensysteme:");
PrintLn("IntToHex8(255) = ", IntToHex8(255));
PrintLn("IntToHex32(305419896) = ", IntToHex32(305419896));
PrintLn("IntToBin8(5) = ", IntToBin8(5));
PrintLn("IntToOct(511, 3) = ", IntToOct(511, 3));
PrintLn("IntToHex64(-1) = ", IntToHex64(-1), " (Zweierkomplement)");
PrintLn("-- und zurueck:");
PrintLn("ParseHex('DEADBEEF') = ", IntToStr(ParseHex("DEADBEEF")));
PrintLn("ParseHex('0xFF') = ", IntToStr(ParseHex("0xFF")));
PrintLn("ParseBin('0b1010') = ", IntToStr(ParseBin("0b1010")));
PrintLn("ParseOct('0777') = ", IntToStr(ParseOct("0777")));
PrintLn("-- Einzelne Bits:");
var flags: int64 := 0;
flags := SetBit(flags, 0);
flags := SetBit(flags, 3);
PrintLn("nach SetBit(0) und SetBit(3): ", IntToBin8(flags), " = ", IntToStr(flags));
PrintLn("GetBit(3) = ", IntToStr(GetBit(flags, 3) as int64));
flags := ClearBit(flags, 0);
PrintLn("nach ClearBit(0): ", IntToBin8(flags));
flags := ToggleBit(flags, 7);
PrintLn("nach ToggleBit(7): ", IntToBin8(flags));
PrintLn("-- Bitfelder lesen und schreiben:");
var wort: int64 := 0;
wort := InsertBits(wort, 5, 4, 4); // 4 Bits ab Position 4
wort := InsertBits(wort, 3, 0, 4); // 4 Bits ab Position 0
PrintLn("zusammengesetzt: ", IntToBin8(wort), " = ", IntToStr(wort));
PrintLn("oberes Nibble = ", IntToStr(ExtractBits(wort, 4, 4)));
PrintLn("unteres Nibble = ", IntToStr(ExtractBits(wort, 0, 4)));
PrintLn("-- Bits zaehlen:");
PrintLn("CountLeadingZeros(255) = ", IntToStr(CountLeadingZeros(255)));
PrintLn("CountTrailingZeros(8) = ", IntToStr(CountTrailingZeros(8)));
PrintLn("-- Byte-Reihenfolge und Bytes:");
PrintLn("SwapEndian32(0x12345678) = ", IntToHex32(SwapEndian32(305419896)));
PrintLn("GetByte(0x12345678, 0) = ", IntToHex8(GetByte(305419896, 0)));
PrintLn("GetByte(0x12345678, 3) = ", IntToHex8(GetByte(305419896, 3)));
PrintLn("-- Breiten und Ausrichtung:");
PrintLn("SignExtend8(255) = ", IntToStr(SignExtend8(255)));
PrintLn("ZeroExtend8(-1) = ", IntToStr(ZeroExtend8(-1)));
PrintLn("ClampUint8(300) = ", IntToStr(ClampUint8(300)));
PrintLn("AlignUp(100, 16) = ", IntToStr(AlignUp(100, 16)));
PrintLn("IsAligned(96, 16) = ", IntToStr(IsAligned(96, 16) as int64));
PrintLn("-- Lesbare Zahlen:");
PrintLn("IntToStrWithComma(1234567) = ", IntToStrWithComma(1234567));
PrintLn("IntToStrWithSeparator(1234567, 46) = ", IntToStrWithSeparator(1234567, 46));
return 0;
}
Ausgabe:
-- Zahlensysteme:
IntToHex8(255) = FF
IntToHex32(305419896) = 12345678
IntToBin8(5) = 00000101
IntToOct(511, 3) = 777
IntToHex64(-1) = FFFFFFFFFFFFFFFF (Zweierkomplement)
-- und zurueck:
ParseHex('DEADBEEF') = 3735928559
ParseHex('0xFF') = 255
ParseBin('0b1010') = 10
ParseOct('0777') = 511
-- Einzelne Bits:
nach SetBit(0) und SetBit(3): 00001001 = 9
GetBit(3) = 1
nach ClearBit(0): 00001000
nach ToggleBit(7): 10001000
-- Bitfelder lesen und schreiben:
zusammengesetzt: 01010011 = 83
oberes Nibble = 5
unteres Nibble = 3
-- Bits zaehlen:
CountLeadingZeros(255) = 56
CountTrailingZeros(8) = 3
-- Byte-Reihenfolge und Bytes:
SwapEndian32(0x12345678) = 78563412
GetByte(0x12345678, 0) = 78
GetByte(0x12345678, 3) = 12
-- Breiten und Ausrichtung:
SignExtend8(255) = -1
ZeroExtend8(-1) = 255
ClampUint8(300) = 255
AlignUp(100, 16) = 112
IsAligned(96, 16) = 1
-- Lesbare Zahlen:
IntToStrWithComma(1234567) = 1,234,567
IntToStrWithSeparator(1234567, 46) = 1.234.567
Das Zusammenspiel von InsertBits und IntToBin8 ist beim Bauen von Protokollfeldern hilfreich: man sieht das Bitmuster unmittelbar.
Fallstricke
Geprüft mit lyxc 1.0.21A.
RotateLeft32 verlässt den 32-Bit-Bereich (#1495)
| Aufruf | geliefert | erwartet |
|---|---|---|
0x80000000 «< 1 | 4294967297 | 1 |
0xFFFFFFFF «< 4 | 68719476735 | 4294967295 |
0x12345678 «< 8 | 78187493394 | 878082066 |
Die herausgeschobenen Bits fallen nicht weg, sondern bleiben im int64 stehen. RotateRight32 ist davon nicht betroffen. Das trifft insbesondere Hashverfahren wie MD5, SHA-1 oder ChaCha, deren Kern die 32-Bit-Linksrotation ist. Bis zum Fix selbst maskieren:
var rotiert: int64 := RotateLeft32(wert, n) & 4294967295;
Die Parser melden keine Fehler (#1500)
| Eingabe | Ergebnis | Verhalten |
|---|---|---|
ParseHex(„FFxy“) | 255 | bricht bei x ab |
ParseHex(„12 34“) | 4660 | überspringt das Leerzeichen und liest 1234 |
ParseHex(„-FF“) | 255 | Minuszeichen wird ignoriert |
ParseBin(„1010abc“) | 10 | bricht ab |
ParseOct(„7778“) | 511 | bricht bei der 8 ab |
ParseHex(„ZZ“) | 0 | nichts gelesen |
ParseHex(„0“) | 0 | gültige Eingabe — nicht unterscheidbar |
Zwei Dinge fallen zusammen: eine fehlgeschlagene Umwandlung ist am Rückgabewert nicht erkennbar, und Störzeichen führen mal zum Abbruch, mal werden sie übersprungen. Wer fremde Eingaben verarbeitet, prüft die Zeichen deshalb vorher selbst.
Zu wenige Stellen schneiden ab
IntToHex(255, 1) ergibt F, IntToBin(255, 4) ergibt 1111. Die höherwertigen Stellen entfallen ohne Hinweis. Die festen Varianten (IntToHex8 und Verwandte) sind hier die sicherere Wahl, weil die Breite zum Wertebereich passt.
Positionsprüfungen schlucken Tippfehler
SetBit(wert, 99) gibt den Wert unverändert zurück, GetBit(wert, 64) liefert false. Das verhindert Speicherfehler, verbirgt aber einen falschen Index — die Rückgabe sieht wie ein gültiges Ergebnis aus.
Verwandte Units
- std.buffer — Hex-Umwandlung ganzer Puffer statt einzelner Zahlen
- std.pack — Zahlen mit fester Breite in Byte-Puffer schreiben
- Syntax — die Operatoren der Sprache im Überblick
