Einführung in die Programmiersprache C
Transcription
Einführung in die Programmiersprache C
Einführung in die Programmiersprache C 12 – Portable Programmierung Alexander Sczyrba Robert Homann Georg Sauthoff Universität Bielefeld, Technische Fakultät Warum auf portable Programmierung achten? • je besser ein Programm ist, desto mehr Leute wollen es benutzen → mehr Plattformen müssen unterstützt werden (neuere, ältere, andere als die Entwicklungsplattform) • spätere Anpassungen sind üblicherweise aufwendig • portabler Code ist häufig „besserer“ Code Freiheitsgrade für mögliche Zielplattform Zielplattformen lassen sich definieren über • verfügbare Compiler und C-Libraries, damit auch Einschränkung des Sprachstandards (C89, C99, C++) • CPU (Byte-Order, Assembler) • Datenmodell (ILP32, LP64, . . . ) • Betriebssystem • Verfügbarkeit bestimmter Libraries (zlib, liblzma, . . . ) • Verfügbarkeit bestimmter Tools (make, grep, sed, awk, . . . ) • plattformspezifisches Verhalten von Standard-Schnittstellen (a.k.a. Bugs) Wirklich portable Programmierung ist schwer, nicht nur in C. Portabilität à la ANSI/ISO-C-Standard Die C-Standards definieren unter anderen folgende Begriffe: implementation-defined Compiler-Autor definiert, was passiert unspecified korrektes Programm, aber keine weiteren Vorgaben (z.B. Auswertungreihenfolge bei Parametern) undefined fehlerhaftes Programm; der Standard macht keine Einschränkungen darüber, was passieren darf/muß (z.B. Verhalten bei Integer-Überlauf) constraint Einschränkung oder besondere Anforderung, die eingehalten werden muß • zu jedem Punkt im Standard gehört eine Liste von Constraints • Verletzung eines Constraints → „undefined behavior“, Fehlermeldung muß ausgegeben werden • „undefined behavior“ 6→ Verletzung eines Constraints Portabilität à la ANSI/ISO-C-Standard Probleme mit implementierungsdefiniertem, unspezifiziertem und/oder undefiniertem Verhalten: • laut Standard müssen Fehler nur bei Verletzung einer Syntaxregel oder eines Constraints ausgegeben werden → viele semantische Fehler werden akzeptiert • unterschiedliche Compiler verhalten sich unterschiedlich • nicht spezifiziertes Verhalten und herstellerspezifische Spracherweiterungen sind nicht standardisiert und können sich mit jeder Compiler-Version ändern → definitiv falsche Sichtweise: „Die Sprache ist das, was mein Compiler akzeptiert.“ • besser: jede Compiler-Warnung ist ein potentieller Fehler Allgemeine Regeln für portable C-Programme • Source-Code sollte in 7-Bit-ASCII gespeichert sein (keine Umlaute, erst recht kein UTF-8, UTF-16, etc.) • keine Konstrukte, Schlüsselwörter oder Funkionen aus der C-Library verwenden, die im Sprachstandard der Zielplattform nicht verfügbar sind • Rücksicht auf C++-Compiler nehmen: • Variablen nicht new, delete, mutable, friend, etc. nennen • Character-Literale wie ’a’ sind in C vom Typ int, aber in C++ vom Typ char (Probleme mit sizeof(’a’)) • Probleme mit //-Kommentaren (nur in C99 und C++ erlaubt) vermeiden: a = b //* let’s divide */ c; • nicht main() aus dem eigenen Code heraus aufrufen • Warnungen einschalten! Allgemeine Regeln für portable C-Programme • unabhängig von der Byte-Order programmieren Beispiel 1 2 3 4 5 6 7 8 9 /∗ e i n Big−E n d i a n 32− B i t −I n t e g e r e i n l e s e n ∗/ uint32_t g e t u i n t _ b e 3 2 ( v o i d ) { uint32_t i = ( g e t c h a r ( ) & 0 x f f ) < < 24; i | = ( g e t c h a r ( ) & 0 x f f ) < < 16; i | = ( g e t c h a r ( ) & 0 x f f ) < < 8; i | = ( g e t c h a r ( ) & 0 x f f ) < < 0; return i ; } • falls nicht möglich (sehr selten), dann den entsprechenden Code für beide Möglichkeiten schreiben und entweder • zur Laufzeit richtigen Code-Pfad wählen • oder zur Compile-Zeit per Präprozessor, falls das Programm sonst zu langsam werden würde Allgemeine Regeln für portable C-Programme • keine Annahmen über Datentypen machen – bei korrekter Verwendung der Integer- und Pointertypen ist das konkrete Datenmodell egal • Assembler-Code vermeiden – falls es sich lohnt, dann für die „wichtigen“ Plattformen bestimmte Programmteile in Assembler schreiben, für andere Plattformen ein äquivalentes C-Konstrukt (fallback) verwenden • wenn es eine Wahl zwischen funktional äquivalenten Libraries gibt, z.B. • OpenGL vs. Direct3D • SDL vs. DirectX • GTK+ vs. Motif vs. Qt vs. wxWidgets vs. FLTK vs. . . . dann sollte nach Möglichkeit die portablere Version gewählt werden Betriebssystem der Zielplattform • portable Verwendung der Programmiersprache C allein • • • → reicht nicht aus Interaktion mit dem Betriebssystem nötig entweder direkt über System-Calls oder indirekt über Libraries mehr oder weniger detailliertes Wissen über die möglichen Betriebssysteme der Zielplattformen nötig Desktop/Server: z.B. AIX, FreeBSD, Linux, Mac OS X, NetBSD, OpenBSD, OpenVMS, Solaris, Windows XP/Vista, . . . Embedded/Real-Time: z.B. Contiki, LynxOS, QNX, RTLinux, VxWorks, µITRON, . . . Betriebssystem der Zielplattform Wie portabel soll es sein? • alle Betriebssysteme unterscheiden sich zumindest in Details voneinander • Unterschiede werden teilweise durch Standard-C-Library aufgefangen • am einfachsten: genau eine Plattform wählen → nur in Ausnahmefällen sinnvoll • am schwierigsten: alle möglichen Plattformen unterstützen → nicht praktikabel Betriebssystemspezifikationen und -familien • viele Betriebsysteme halten sich an gewisse Standards oder Quasi-Standards → Betriebssystemspezifikationen bzw. Betriebssystemfamilien mögliche Strategien: • Unterstützung einer Betriebssystemfamilie, auf Einhaltung gewisser Konventionen innerhalb der Familie setzen • Voraussetzung einer Betriebssystemspezifikationen Strategie: Betriebssystemfamilie unterstützen Betriebssystemfamilien: • UNIX • VMS • DOS • NT • ... Beispiel 1 2 3 4 5 6 7 8 9 10 11 #i f d e f i n e d __unix__ | | d e f i n e d __unix | | d e f i n e d u n i x /∗ UNIX c o d e ∗/ # i f d e f __linux__ /∗ L i n u x s p e c i f i c t w e a k s ∗/ # endif #e l i f d e f i n e d _WIN32 | | d e f i n e d WIN32 | | /∗ . . . ∗/ /∗ Windows c o d e ∗/ # i f d e f i n e d _WIN64 | | d e f i n e d WIN64 /∗ 6 4 b i t t w e a k s ∗/ # endif #e n d i f Strategie: Betriebssystemfamilie unterstützen Probleme: • nur ein Teil der Eigenschaften der Zielplattform kann wirklich erfaßt werden, für die restlichen Eigenschaften werden Annahmen gemacht • Annahmen häufig falsch • häufig Sonderbehandlungen für bestimmte Familienmitglieder notwendig • teilweise unterschiedliche Konventionen bei unterschiedlichen Versionen des gleichen Betriebssystems • sehr hoher Wartungsaufwand • beim Linken weiterhin Probleme mit Libraries → sehr zerbrechliche Konstruktion Strategie: Betriebssystemspezifikation voraussetzen Betriebssystemspezifikationen definieren die Verfügbarkeit und Funktionalität von • System-Schnittstellen • Funktionen in der C-Library und anderen Libraries • Header-Files • Standardprogrammen (z.B. sh, grep, . . . ) • ... Strategie: Betriebssystemspezifikation voraussetzen wichtige Spezifikationen: • SUS (Single UNIX Specification) SUSv3 (2002), SUSv2 (1997), SUSv1 (1994) • POSIX (Portable Operating System Interface) POSIX.1, POSIX.2, seit 1988 • XPG (X/Open Portability Guide) XPG6 ≡ SUSv3 XPG5 ≡ SUSv2 XPG4v2 ⊂ SUSv1 XPG4 (1992), XPG3 (1989) siehe: $ man standards Strategie: Betriebssystemspezifikation voraussetzen um eine bestimmte Spezifikation zu verwenden, muß sie in der Regel per Präprozessorsymbol „freigeschaltet“ werden, z.B. • SUSv3: #define _XOPEN_SOURCE 600 • POSIX.1b-1993: #define _POSIX_C_SOURCE 199309L siehe: • Solaris: Abschnitt „Feature Test Macros“ in standards-man-page • Linux: $ man feature_test_macros Strategie: Betriebssystemspezifikation voraussetzen Vorteile: • besser als #ifdef-Orgien • weniger Wartungsaufwand Probleme: • die Standards widersprechen sich teilweise • einige Stellen sind zu ungenau spezifiziert • die Spezifikationen sind nicht immer komplett oder korrekt implementiert • standardkonforme Programme liegen nicht immer im Pfad → $PATH muss passend gesetzt sein (SUSv3 auf Solaris: /usr/xpg6/bin:/usr/xpg4/bin:/usr/ccs/bin:/usr/bin ) → der Benutzer muß das auch wissen Grenzen verbleibende Fragen: • Sind die vom Programm benötigten Libraries installiert? In der richtigen Version? • Sind alle benötigten Programme installiert, z.B. make, sed, python, perl, tcsh, bash? In der richtigen Version? • Welcher Compiler wird verwendet? Unterstützt er den vom Programm verwendeten Sprachstandard? • Welche Compiler-Flags müssen gesetzt werden, damit alle Header-Files und Libraries gefunden werden (-I, -L)? • Wie und wohin wird das Programm installiert? • ... Durch keinen plattformunabhängigen Standard abgedeckt! Andere Strategie Strategie: • wünschenswert: Charakteristika der Zielplattform ermitteln und verwenden, was verwendbar ist • Testprogramm schreiben, das restrict verwendet – ist es mit dem vorhandenen Compiler nicht übersetzbar, dann #define restrict • Testprogramm, mit dem man herausfindet, ob bestimmte Header-Files vorhanden sind • Testprogramm, das die Byte-Order feststellt (siehe erste Musterlösung zur MPEG-Frame-Header-Aufgabe) • Testprogramm, das feststellt, ob eine Funktion oder eine Typdefinition vorhanden ist • Testprogramm, mit dem festgestellt wird, ob für gewisse Funktionen gegen eine bestimmte Library gelinkt werden muß Andere Strategie • fehlt eine benötigte Funktionalität, dann kann eine Alternative versucht werden (z.B. bzero() vs. memset()) • gibt es keine Alternative, kann eine Ersatzimplementierung mitgeliefert werden • Programme können häufig auch auf gewisse Funktionalitäten verzichten und diese optional anbieten, z.B. • Kompression von erzeugten Daten nur, wenn libz verwendbar ist • Unterstützung nur für die Grafikformate, für die die entprechenden Libraries installiert sind GNU Autotools Umsetzung dieser Strategie: GNU Autotools Autoconf Erzeugung eines portablen Shell-Scripts („configure“), das alle benötigten Tests durchführt Automake Erzeugung von portablen Makefiles (optional) Libtool portables Bauen von dynamischen und statischen Libraries (optional) • nicht nur für C, sondern auch C++, Objective C, Fortran, Fortran 77 und Erlang • Achtung: umfangreich, sehr steile Lernkurve GNU Autotools Funktionsweise: 1 Vorbereitung: • vordefinierte Tests und eigene Tests werden in die Datei configure.ac geschrieben (erste Version: autoscan) • Bauanleitung für Programme (und Libraries) wird in Makefile.am geschrieben • das Programm autoreconf wird aufgerufen und erzeugt daraus configure, Makefile.in und einiges mehr 2 3 Konfiguration: $ configure [options] erzeugt Makefile, config.h und nach Belieben weitere Dateien (Header-Files, Shell-Scripte, etc.) Übersetzung und Installation: $ make $ make install die Autotools werden nur für den ersten Schritt benötigt GNU Autotools configure.ac 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # I n i t i a l i s i e r u n g nicht gezeigt AC_PROG_CC AC_PROG_CC_C99 AS_IF ( [ t e s t " $ { ac_cv_prog_cc_c99 } " = no ] , [ AC_MSG_ERROR ( [ C99 c o m p l i a n t c o m p i l e r r e q u i r e d . ] ) ] ) AC_USE_SYSTEM_EXTENSIONS AC_SYS_LARGEFILE AC_CHECK_HEADERS ( [ f c n t l . h s t d i n t . h s t r i n g . h u n i s t d . h s y s /mman . h ] ) AC_TYPE_SIZE_T AC_TYPE_UINT32_T AC_TYPE_UINT8_T AC_C_BIGENDIAN ( [ AC_DEFINE ( [ IS_BIG_ENDIAN ] , [ ] , [ D e f i n e d i f b i g e n d i a n . ] ) ] , [ AC_DEFINE ( [ IS_LITTLE_ENDIAN ] , [ ] , [ D e f i n e d i f l i t t l e e n d i a n . ] ) ] , [ AC_MSG_ERROR ( [ B yte o r d e r unknown . ] ) ] ) AC_FUNC_MMAP # Rest n i c h t g e z e i g t Makefile.am 1 bin_PROGRAMS = mpegframes 2 mpegframes_SOURCES = mpegframes . c mpegframe . h m a p f i l e . c m a p f i l e . h $ mkdir my_build_dir $ autoreconf -i && cd my_build_dir && ../configure GNU Autotools Ausgabe (neben dem Makefile): config.h 1 2 3 4 5 6 7 8 9 10 11 12 13 /∗ . . . ∗/ /∗ D e f i n e t o 1 i f you h a v e t h e < s y s / t y p e s . h> h e a d e r #d e f i n e HAVE_SYS_TYPES_H 1 /∗ D e f i n e t o 1 i f you h a v e t h e < u n i s t d . h> h e a d e r #d e f i n e HAVE_UNISTD_H 1 f i l e . ∗/ f i l e . ∗/ /∗ D e f i n e d i f b i g e n d i a n . ∗/ /∗ # u n d e f IS_BIG_ENDIAN ∗/ /∗ D e f i n e d i f l i t t l e e n d i a n . ∗/ #d e f i n e IS_LITTLE_ENDIAN /∗∗/ /∗ . . . ∗/ Benutzung: mapfile.c 1 2 3 4 5 6 7 #i f d e f HAVE_CONFIG_H #i n c l u d e " c o n f i g . h " #e n d i f #i f d e f HAVE_UNISTD_H #i n c l u d e < u n i s t d . h> #e n d i f /∗ . . . ∗/ /∗ d i e s e d r e i s o l l t e n immer d i e ∗/ /∗ a l l e r e r s t e n Z e i l e n s e i n , wenn ∗/ /∗ A u t o c o n f v e r w e n d e t w i r d ∗/ GNU Autotools geschenkt: • portables configure mit konsistenter Bedienung • portable Makefiles, nicht GNU-make-spezifisch • Berechnung von Abhängigkeiten, make -j funktioniert • automatisch vorhanden: make install, make dist, make distcheck, make clean, make distclean und noch einige mehr • Unterstützung für VPATH-Builds fast geschenkt: • Cross-Compilation • make check • Unterstützung unterschiedlicher Konventionen wie z.B. „/my/path/“ auf UNIX vs. „C:\my\path\“ auf DOS GNU Autotools zusätzlich frei verfügbar: • externe Sammlung von Autoconf-Macros: Autoconf Macro Archive auf http://ac-archive.sourceforge.net/ • Sammlung von Ersatzimplementierungen für fehlende Funktionen: Gnulib (GNU Portability Library) auf http://www.gnu.org/software/gnulib/ ausführliche Einführung in GNU Autotools: Tutorial von Alexandre Duret-Lutz auf http://www.lrde.epita.fr/~adl/autotools.html GNU Autotools „Konkurrenz“: • CMake • SCons • Cons • smake • ... meistens Ersatz für make, Benutzer müssen diese Tools erst installieren Those who do not understand Autoconf are condemned to reinvent it, poorly. (Autoconf Manual)