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)