FHEM - Entwicklung > FHEM Development

FHEM und UNICODE, first aid

(1/23) > >>

herrmannj:
Leider eskalieren aktuell wieder die Probleme mit Sonderzeichen und fhem. Eine Mehrheit dies zu ändern, zeichnet sich nicht ab, daher Hilfe zur Selbsthilfe:

Begriffsbestimmung

Ganz früher™ hatte ein Computerbyte 8Bit, ein byte speicherte einen Buschstaben und Computer konnten maximal 256 Zeichen unterscheiden. Die Anzahl der Buchstaben in allen Sprachen der Welt, Sonderzeichen und Satzzeichen sind in Summe natürlich viel mehr. Also gab es Codepages die jeweils einem Ausschnitt entsprachen, zum Beispiel den in Westeuropa am häufigsten benötigten Zeichen.

https://de.wikipedia.org/wiki/Codepage_850

Mit 16, 32 oder 64 bit Computern kann man in einer „Speicherzelle“ deutlich mehr als 256 verschiedene Zeichen speichern. Speichern, im Zusammenhang mit Computern, bedeutet aber konkret nur, dass in einer Speicherzelle des RAM eine (neutrale) Zahl gespeichert wird. Welche Zahl welchem Zeichen entspricht, wird weiterhin per Konvention festgelegt. Das älteste Unicode-Kodierungsformat ist UTF-16, etabliert hat sich UTF-8.

https://de.wikipedia.org/wiki/UTF-8

Perl intern wird UNICODE verwendet: https://home.unicode.org/

Wenn ein perl Programm mit der Außenwelt kommuniziert, dann geschieht das klassisch über Strecken auf denen die Einheiten (byte, besser octet) nach wie vor aus 8bit bestehen. Irgendwie muss die große Anzahl verschiedener Buchstaben die perl kennt, in eine Folge von 8bit Zeichen konvertiert werden, wenn perl mit der Außenwelt spricht. Wenn etwas von außen kommt, entsprechend andersrum.  UTF-8 schreibt die Regeln dieser Konvertierung fest. UTF-8 ist die Standardkodierung von zB MQTT (Topics), JSON, Websocket.

Wenn perl das alles kann, und alle Konventionen klar sind, warum passiert das nicht alles automatisch? Der Teufel liegt im Detail. Als perl den UNICODE Support eingeführt hat, hat man sich aus gutem Grund entschieden, dass der Programmierer die entsprechenden Funktionen aktiv aktivieren muss. Andernfalls hätte das die Kompatibilität massiv gebrochen. Eine MQTT Verbindung kodiert die topics die UTF-8, hier ist eine Konvertierung erwünscht und erforderlich. Wenn es sich aber um die Übertragung einer Firmware Binary handelt, dann darf da natürlich nichts angefasst werden, die muss originalgetreu übermittelt werden. Perl kennt den Unterschied nicht, das weiß nur der Programmierer. Wenn perl selbst aktiv werden soll, was es sehr einfach könnte, dann muss das explizit ansagen.

UNICODE, UTF-8, UTF-8 octets. Kleine, aber feine Unterschiede

Ein 32bit Perl kennt also 2^32 Zeichen (Buchstaben). Ist jedes dieser Zeichen dann gleichzeitig ein UTF-8 Zeichen? Die Antwort ist nein. UTF-8 definiert Bereiche welche Sonderfunktionen haben. Nicht alle theoretisch für perl verfügbare UNICODE Zeichen sind also gleichzeitig gültige UTF-8 Zeichen. Wie bereits geschrieben legt UTF-8 auch die Konventionen fest, wie die Zeichen umgewandelt werden, wenn sie auf 8bit Medien übertragen oder gespeichert werden. Man spricht dann oft von Octets. Aus diesem Zeichen „😀“ wird dann zB eine Folge von 4 octects (a 8bit) gebildet. Lässt sich im Umkehrschluss jetzt aus jeder beliebigen Folge von octets ein UTF-8 Zeichen bilden? Die Antwort lautet wiederum nein. Nachschlagen kann man das in Tabellen wie der folgenden:

https://www.utf8-chartable.de/unicode-utf8-table.pl

Um perl den korrekten Umgang mit UTF-8 zu ermöglichen, haben die Entwickler verschiedene Möglichkeiten eingebaut. Beim Lesen eines Files von der Festplatte etwa, ist es sehr leicht möglich dem IO Layer mitzuteilen das es sich um eine Textdatei handelt deren Inhalt UTF-8 kodiert ist.
--- Code: ---open my $in,  '<:encoding(UTF-8)',  'input-file-name'  or die $!;
--- Ende Code ---
Zur Abgrenzung: natürlich gibt es etliche Gründe das nicht tun zu wollen. Zum Beispiel wenn die Konvertierung unbeabsichtigt binäre Daten, die nur wie UTF-8 aussehen, konvertiert. Deswegen muss man das explizit ansagen.

FHEM unterscheidet (per Konvention) dies alles nicht. Die Zeichen sollen so im Speicher landen, wie sie geliefert wurden. Im Falle des Smileys „😀“ liegt also nicht ein einzelnes Zeichen da, sondern vier. Das kann man überprüfen:

--- Code: ---my $uc = "😀";
print '0: '.length($uc)."\n";
--- Ende Code ---
Ergebnis: 4

Trotzdem ist das optisch häufig nur äußerst schwer zu erkennen, denn die Anzeige auf einem Monitor (des Smiley) funktioniert meistens trotzdem. Der Grund dafür ist, dass das entsprechende Ausgabemedium, also der Browser oder ein Code Editor, erkennt (oder sogar davon ausgeht), dass es sich um einen UTF-8 octet stream handelt, und das Zeichen daher korrekt darstellt. Im Gegenzug führt der richtige Umgang (UNICODE = U+1F600) dazu, dass Fehler auftreten. Die Meldung „illegal widechar in print“ zum Beispiel bedeutet, dass das zugehörige Filehandle nicht informiert wurde, dass es UTF-8 ausgeben soll. Wenn es das wüste, dann würde es klaglos konvertieren. So ist es ratlos und ruft nach Hilfe 😉

Was bedeutet das für den Entwickler?

In der Theorie sind daher alle strings in FHEM UTF-8 octet streams. Das bedeutet das, neben vielen andere Effekten, Funktionen (oder Routinen), welche die Länge von Strings verarbeitet, … “alternative Fakten“ abbilden.


--- Code: ---my $uc = "😀";
my $t = substr($uc, 0, 1);
print "=[$t]\n";
--- Ende Code ---

Das erste Zeichen des Strings mit dem Smiley sollte der Smiley sein. Stattdessen wird daraus ein „�“. Surpise. Gleichfalls führen Versuche den String zeichenweise zu verarbeitet zu unerwarteten Effekten. Das resultierende Array wird vom Programmierer mit einem Element erwartet (ein Buchstabe), hat aber vier.

--- Code: ---split(//, $uc)
--- Ende Code ---
Perfide dabei ist, dass solche Effekte nur auftreten, wenn auch wirklich Sonderzeichen verarbeitet werden. Wer sein eigenes Modul testet, und dabei anstelle des Smileys den Text „Foo“ verwendet, wird den Fehler nicht bemerken. Es wird perfider. Versprochen. Es passieren haufenweise implizite Konvertierungen. JSON, zum Beispiel, definiert UTF-8 per Konvention als Format. Wenn also JSON Daten verarbeitet werden, dann wir die Bibliothek, welche man einsetzt, das dankenswerterweise übernehmen. Wenn ein JSON Key also „Jörg“ lautet, dann wird der entsprechende Key für FHEM mit einer Länge von … 4 geliefert. Nicht 5. „Professionelle“ Bibliotheken für MQTT, Websocket und vieles andere machen das auch. Das Horneburger Schiessen 😉

Abhilfe.

Da also FHEM nicht also Boundary taugt, unvermeidlich aber das eine oder andere im FHEM vorzufinden ist, was tun. Die Grenze kann nur noch das eigene Modul sein. Zumindest wenn jenes potentiell mit „Sonderzeichen“, also alles was nicht [0-9a-zA-Z] ist, in Berührung kommen kann. Ein altes CUL oder TRX Modul eher nicht. Alles was Webkontakt, JSON, Benutzerfelder (Freitext) hat – vmtl ja.
Alles was von Seiten FHEM selbst, oder von außen kommt, sollte daher nach UNICODE (1 Smiley == 1 Zeichen) konvertiert werden, es sei denn die Bibliothek (JSON) selber macht das. Im Zweifel, checken.

So geht’s von UTF-8 ins perl UNICODE format:


--- Code: ---use Encode qw(encode decode find_encoding);
eval {$uc = decode(find_encoding('UTF-8'), $uc, Encode::FB_CROAK)};

--- Ende Code ---
Falls der zu konvertierende String bereits UNICODE ist, dann ist das ein no-op. Wozu das eval? Wie weiter oben geschrieben gibt es UTF-8 octet Sequenzen, welche sich nicht in UNICODE konvertieren lassen. Mit der gewählten option Encode::FB_CROAK wirft das eval in diesem Fall einen Fehler. Der String ($uc) passiert die Konstruktion in diesem Fall unverändert (unkonvertiert). Darauf kann man reagieren. Wie, das bestimmt der Kontext und den kennt nur der Programmierer.

Alles was vom Modul in Richtung FHEM „rausgeht“, unter anderem aber nicht abschliessend Log3, Readings, Attribute, DoTrigger, in die andere Richtung:

--- Code: ---eval {$uc = encode(find_encoding('UTF-8'), $uc, Encode::FB_CROAK)};

--- Ende Code ---
Gleicher Hintergrund. Wenn ein UNICODE Zeichen versucht wird zu konvertieren, das keine UTF-8 octet Entsprechung hat, wird ein Fehler ($@) geworfen. Do what you want then 😉

use utf8; Pragma, is_utf8()

Das use utf8 Pragma macht genau eins: es sagt perl dass der source in einer UTF-8 Kodierung vorliegt.
Is_utf8() ist eigentlich eine Funktion der perl Developer zum Debuggen von perl. Nicht mehr, nicht weniger. Was sie definitiv nicht ist: eine Funktion um UTF-8 octet strings von UNICODE strings zu unterscheiden.
Folgender Code demonstriert beides. Man beachte den Unterschied bei aus-kommentiertem oder aktiviertem use utf8;


--- Code: ---use utf8;
use Encode qw(encode decode find_encoding is_utf8);

my $uc = "😀";

print '0: '.length($uc)." flag: ".is_utf8($uc ,1)."\n";
eval {$uc = decode(find_encoding('UTF-8'), $uc, Encode::FB_CROAK)};
print '1: '.length($uc)." flag: ".is_utf8($uc ,1)."\n";
eval {$uc = encode(find_encoding('UTF-8'), $uc, Encode::FB_CROAK)};
print '3: '.length($uc)." flag: ".is_utf8($uc ,1)."\n";
eval { print "4: $uc\n"; };

--- Ende Code ---

Ausgabe mit use utf8:

--- Code: ---0: 1 flag: 1
1: 1 flag: 1
3: 4 flag:
4: 😀
--- Ende Code ---

Ausgabe ohne use utf8:

--- Code: ---0: 4 flag:
1: 1 flag: 1
3: 4 flag:
4: 😀

--- Ende Code ---

Adimarantis:
Zu dem Thema fällt mir gleich das FHEM Logfile ein. Wenn ich Unicode String logge bekomme ich eben genau „illegal widechar in print“, was ich umgehe indem ich das log mit

--- Code: ---binmode(LOG,"encoding(UTF-8)");
--- Ende Code ---
umschalte.

Wäre es denkbar das per Default zu machen?

timmib:
Siehe hierzu auch: https://forum.fhem.de/index.php/topic,125866.0.html

rudolfkoenig:

--- Zitat ---Wäre es denkbar das per Default zu machen?

--- Ende Zitat ---
Denkbar ist Vieles :)
Das wuerde implizieren, dass FHEM-Intern die Daten als Unicode vorliegen.
Aktuell setzen die meisten (alle?) FHEM-Schnittstellen vor, dass die Daten als utf-8 Bytefolge vorliegen, d.h. man wuerde von einer Fehlermeldung zum Naechsten kommen. Wenn wir umstellen, dann bitte mit einem Plan.


--- Zitat ---Is_utf8() ist eigentlich eine Funktion der perl Developer zum Debuggen von perl. Nicht mehr, nicht weniger. Was sie definitiv nicht ist: eine Funktion um UTF-8 octet strings von UNICODE strings zu unterscheiden.

--- Ende Zitat ---
Ich haenge nicht an is_utf8, ich brauche aber eine Methode, die Unicode-Strings erkennt, damit das Framework mit Modulen zusammenarbeiten kann, die Strings im Unicode-Format haben.  Bin noch nichtmal sicher, dass es theoretisch immer moeglich ist, mit dieser Methode hatte ich bisher am wenigsten Aerger.

Die Encoding-Probleme werden bleiben, auch wenn wir FHEM-Intern auf Unicode umstellen, da nicht alle Modulschreiber sagen koennen, ob das Input nun UTF-8 ist oder nicht. Und dann bleiben die, die es vergessen, so wie die, die jetzt das utf8::encode vergessen. Perl-Bibliotheken, die direkt mit der Aussenwelt reden, liefern Daten natuerlich im Unicode Format, eine Umstellung macht die Zusammenarbeit hier einfacher. Eine JSON Bibliothek wird das nicht tun, da sie schon fertig kodierte Daten bekommt.
Und natuerlich waere nach einer Umstellung einfacher zu pruefen, wieviele Smileys ein String enthaelt :)

In meinen Augen der groesste Gewinn der Umstellung waere, dass wir solche Diskussionen nicht fuehren muessen.
Das waere fuer mich schon was Wert.

Beta-User:
Ähm, stehe grade auf dem Schlauch mit einem Modul, konkret: msgDialog.

Da wird ein JSON für den betreffenden Dialog in die DEF geschrieben, also eigentlich seitens fhem.pl verwaltet und an das Modul übergeben. Beim Überarbeiten hatte ich mich dann etwas gewundert, warum da

--- Code: ---$DEF = eval{JSON->new->decode($DEF)};
--- Ende Code ---
steht, und nicht gleich ein "decode_json($DEF)" an das eval geht.

Wenn man das umstellt und das Beispiel aus https://wiki.fhem.de/wiki/MsgDialog#Programmierung_der_Waschmaschine dahingehend modifiziert, dass die Waschmaschine "Wäschmaschine" heißt, wird auch klar, warum: im Internal TRIGGER steht dann was, was nicht verwertbar ist:
--- Zitat ---TRIGGER W�schmaschine
--- Ende Zitat ---

Daher habe ich das erst mal so belassen, wie es war, allerdings wirft das die Frage auf, ob bzw. an welcher Stelle eigentlich ein zukunftsgerichtetes  Coding aussehen müßte...? Jedenfalls übergibt fhem.pl nach meinem Verständnis eben nicht UTF-8 im Rahmen von DefFn(), oder? (Schon gleich nicht, wenn man FileRead() verwendet).

Testsystem: OS: MSWin32 Perl: 5.32.1 (war aber nach meiner Erinnerung auch auf einem aktuellen Linux nicht anders, version liefert u.a. fhem.pl                          25644 2022-02-06 20:01:09Z rudolfkoenig)

Ergänzend: Den Code hatte ich dahingehend überarbeitet, dass die DEF per InternalVal() gelesen wird (nach $content), jetzt wollte ich den Testcode so einbauen:

--- Code: ---  if ( !eval{ $content = decode(find_encoding('UTF-8'), $content, Encode::FB_CROAK); 1;} ){
    Log3($hash, 2, "msgDialog ($name) - DEF or configFile is not decodable to UTF-8: $@");
    return("Usage: define <name> msgDialog {JSON}\n\n$@");
  }
--- Ende Code ---
Dann wirft aber bereits der unüberarbeitete Beispiel-Dialog aus dem Wiki einen Fehler, anscheinend kommt es bei "(bestätigen|zurück|abbrechen)" zu einem Problem:...

--- Zitat ---malformed UTF-8 character in JSON string, at character offset 1014 (before "\x{fffd}gen|zur\x{fffd}...") at ./FHEM/76_msgDialog.pm line 140.
--- Ende Zitat ---

Navigation

[0] Themen-Index

[#] Nächste Seite

Zur normalen Ansicht wechseln