#przenosnosc — Public Fediverse posts
Live and recent posts from across the Fediverse tagged #przenosnosc, aggregated by home.social.
-
Czy robimy teraz potencjalnie fajne rzeczy z CPythonem? Być może.
Parę testów sypało się z wirtualizacją `systemd-nspawn` przy `--suppress-sync=true`. Dlaczego? Bo ta opcja używa SecComp do nadpisania syscalli związanych z sync na takie, które nie robią nic — a zatem zawsze zwracają sukces, nawet kiedy argumenty są niepoprawne. A testy CPythona sprawdzały, czy przy błędnym argumencie wyjdzie błąd. Przyszło więc mi napisać funkcję, która wykrywa ten scenariusz i odpowiednie testy pomijać.
https://github.com/python/cpython/pull/124215
Kolejne testy z kolei zaliczały segfaulta na PPC64 (BE). Jak już wspominałem, nie mogłem dojść, co jest nie tak. GDB wskazywał, że segfault jest w malloc(). Jak dodałem `--with-pydebug` — znikał. Używałem ASAN-a? Znikał. W końcu @thesamesam odkrył, że mamy do czynienia z przepełnieniem stosu przy nadmiernej rekurencji. Co ciekawe, jeżeli przyjrzymy się kontekstowi łatki, dowiemy się, z czego wynikały wszystkie moje problemy. Otóż, Python używa mniejszego limitu niż normalnie, jeżeli używamy `--with-pydebug` albo załączamy ASAN!
W tej sytuacji ja się zmotywowałem, by przyjrzeć się rozmiarowi ramki stosu na architekturach. Z pomocą GodBolta odkryłem, że (z platform obsługiwanych przez GodBolta) tylko PPC64, s390 i SPARC mają absurdalnie wielkie ramki. Co ciekawe, PPC64LE ma ciut mniejsze niż PPC64 — jak podpowiedział pkubaj, różnica ta wynikała z tego, że LE domyślnie używa -mabi=elfv2, a BE elfv1.
https://github.com/python/cpython/pull/124264
Rozmyślałem też trochę nad zmianami koniecznymi do bezpiecznego przejścia na 64-bitowe time_t na 32-bitowych platformach — ale to już odrębna historia.
-
Wbrew wszelkiemu prawdopodobieństwu, 32-bitowe #time_t bywa pomocne.
I tak na przykład wczoraj znalazłem ciekawy błąd w bibliotece #Moto. Biblioteka przekazywała do botocore datę w formie zbliżonej do ISO-8601, ale bez separatorów, np. "20240907201715" — lecz botocore oczekiwało albo numerycznego znacznika czasu w stylu uniksowym, albo bardziej standardowego formatowania daty. Tak więc te połączone cyfry były błędnie interpretowane jako uniksowy znacznik czasu, co dawało rok 643378.
Podejrzewam, że gdyby nie to, że 32-bitowe time_t sypie się na tak wielkich znacznikach, to pewnie jeszcze długi czas nikt nie zauważyłby, że daty są błędnie interpretowane.
-
Powoli pracuję nad czymś nowym na bloga, a w międzyczasie trochę przyjrzałem się tematowi problemu roku 2038, i chciałbym podzielić się z wami kilkoma spostrzeżeniami.
Zdaje się, że najczęściej ten problem porusza się głównie jako "przekręcenie się licznika" w 32-bitowych programach — ot, nagle cofniemy się z 2038 roku do 1901, i będzie "śmiesznie". W praktyce jednak najprawdopodobniej zobaczymy trochę inne problemy.
Po pierwsze, `stat()` przestaje działać na plikach, których znaczniki czasu nie mieszczą się w 32-bitowym `time_t`; jednakże pliki w dalszym ciągu można otworzyć. Prowadzi to do dosyć nieintuicyjnej sytuacji — zwykle zakłada się, że to otwieranie plików jest bardziej restrykcyjne. W najbardziej absurdalnym przypadku, program będzie w stanie każdego pliku użyć tylko raz — po użyciu bowiem zaktualizowane zostaną znaczniki czasu i `stat()` przestanie działać.
Po drugie, `time()` zwraca błąd. Wielu programistów (ze mną włącznie) nawet nie bierze pod uwagę, że `time()` może zwracać błąd — a po 2038 każde wywołanie będzie zwracać `-1`. Co to oznacza w praktyce? "Dobrze" napisane programy po prostu prędzej czy później zwrócą błąd i przestaną działać. "Źle" napisane programy z kolei "ugrzęzną" sekundę przed północą 31 grudnia 1969. W praktyce będzie to gorsze niż zwracanie "przekręconego" licznika czasu, bowiem każde wywołanie zwracać będzie tę samą wartość.
Jakie to może mieć skutki? Zegar w programie będzie pokazywać niezmiennie tę samą godzinę i datę. Jakiekolwiek pętle oczekiwania oparte o zegar ścienny po prostu się zawieszą. A generatory liczb pseudolosowych, inicjowane zegarem (`srand(time(NULL))`) będą każdorazowo generować te same liczby.
Problem ten dotknie wszystkich własnościowych programów na 32-bitowe platformy. Jeżeli będziemy chcieli np. pograć w jakąś starą grę, będziemy musieli sporo się namęczyć. Co gorsza, nie wystarczą narzędzia typu faketime, które "oszukają" wskazania zegara, ale nie wpływają na znaczniki czasu na plikach.
-
Kolejny ciekawy problem z dziedziny przenośności: kodowania #UTF-16, UTF-32, UCS-2 i UCS-4 są zależne od kolejności bajtów. Oznacza to, że można je zakodować albo jako big endian, albo jako little endian. Kodując ciągi znaków, #Python używa kolejności bajtów systemu i dopisuje Byte Order Marker na początku pliku. Przy dekodowaniu, automatycznie odczytuje zapisany wcześniej BOM, by określić właściwą kolejność bajtów, dzięki czemu wszystko "po prostu działa".
Problemy zaczynają się, kiedy próbujemy porównać zakodowane dane na poziomie bajtów, np. porównując zapisany wcześniej jako UTF-16 plik z wynikiem wywołania `encode()`. Jeżeli plik był zapisany na systemie little endian (jak to zwykle bywa), a testy uruchamiane są na systemie big endian, nagle okaże się, że dostajemy dwa różne ciągi bajtów!
"Oczywistym" rozwiązaniem jest wymuszenie konkretnej kolejności bajtów, np. użyjąc kodowania `utf-16-le` zamiast `utf-16`. Tu jednak pojawia się kolejny problem — kiedy podajemy określoną kolejność bajtów, Python nie zapisuje już BOM — tak więc porównanie na poziomie bajtów wykaże różnicę w postaci brakującego BOM. Można to jednak rozwiązać prostą sztuczką — dopisując BOM (`\ufeff`) na początku kodowanego ciągu.
https://github.com/python/importlib_resources/pull/313/files
-
Co powszechnie uważa się za źródło problemów programów na 32-bitowych architekturach:
• inny rozmiar zmiennych całkowitoliczbowych (tak naprawdę, z tych standardowych typów, tylko `long` się różni, a tego się rzadko dziś używa)
Co tak naprawdę sprawia problemy programom na 32-bitowych architekturach:
• budowanie bez Large File Support (wsparcia dla plików >2 GiB, także wymaganego dla systemów plików, w których numery i-węzłów przekraczają 32 bity)
• 32-bitowy `time_t` (i wynikający z niego problem roku 2038)
• alokacja olbrzymich ilości pamięci (nawet jeśli nie będzie faktycznie wykorzystana, to przestrzeń adresowa jest ograniczona)
• (na x86) 80-bitowa arytmetyka zmiennoprzecinkowa (chyba że używa się `-mfpmath=sse`, to wtedy 64-bitowa, kiedy programy zakładały wyjątkowość x86…) -
Dziś użyłem #GDB. Z rozszerzeniem Pythona w #C. Na architekturze #SPARC.
Co to może być, jeżeli paczka sypie się na arytmetyce z liczbami całkowitymi na SPARC, a działa na PPC64 (też big endian)? Cóż, okazuje się, że problem w tym, że `char` na SPARC jest typem signed.
I wygląda na to, że autor postanowił sobie rzutować parametr typu `const unsigned char *` na zmienną `const char *`, bez widocznego powodu. Wzdych.