Hallo!
Ich habe eine Frage bzgl. statischen Libs und Abhängigkeiten in C:
Wie löse ich folgendes Problem am geschicktesten:
Ich habe ein Modul X (bestehend aus mehreren C-Dateien), dass ich gerne
einmal als statische Lib kompilieren und später in meinen
unterschiedlichen Hauptprojekten einsetzen möchte.
Das Problem ist, dass das Modul X auf unterlagerten Funktionen zugreift,
die aber bei den Projekten unterschiedlich ausgeprägt sind.
Wie gestalte ich nun diese Schnittstelle am Besten?
Dachte an Registrierung von Callbacks. Aber es ist doch schon
ungewöhnlich, dass sich eine untere Schicht an der darüberliegenden
"registriert".
Zudem finde ich es umständlich immer auf != NULL zu prüfen, bevor ich
die Adresse anspringe ...
Danke.
VG
Reddy
Auch wenn es nicht exakt das gleiche Problem ist, ich mache so etwas
über Funktionspointer. Hauptsächlich dann, wenn eine "Higlevel-Funktion"
auf eine "Lowlevel-Funktion" aufsetzt und ich bei letzterer mehrere
Möglichkeiten habe.
Z.B. in einem Modul für SPI Flashes gibt es ein:
1
externunsignedcharM25P16_SPI_COMM(unsignedchar);
welches die generische SPI-Routine ist. Dann "initialisiere" ich das im
Hauptprogramm mit:
1
unsignedcharM25P16_SPI_COMM(unsignedcharx)
2
{
3
returncomm_spi0(x);
4
}
wobei das meist auch wieder auf Routinen innerhalb der Lib zeigt. Damit
kann ich zu Debug-Zwecken auch mal schnell auf die serielle
Schnittstelle des Controllers umleiten.
Sollte ich die Initialisierung im Hauptprogramm "vergessen", gibt es
einen Fehler beim Linken.
Jörg
Für unterschiedliche Platformen:
- gar nicht
Für die selbe Platform:
- bei Laufzeit-Abhängigkeiten -> Callbacks
- ohne Laufzeit-Abhängigkeiten -> Funktionsdeklarationen
@joergwolfram
Eine Funktionsdeklaration ist kein Funktionspointer.
Reddy schrieb:> Reicht es aus, dass ich in der zu erstellenden Lib das Headerfile von> der unterlagerten Schicht inkludiere und dort die Funktionen als extern> deklariere?
Ja. Funktionen werden erst beim Linken aufgelöst; ob Nutzung und
Definition der Funktion jetzt in einer statischen Library oder "direkt"
genutzten .o -Datei vorkommt ist egal. Statische Libraries (.a) sind
sowieso nur Archive an .o-Dateien. "extern" ist bei
Funktions-Deklarationen übrigens komplett überflüssig und macht
keinerlei Unterschied.
Reddy schrieb:> Viel. Und auf English schauder.
Informatik ist nicht umsonst ein Studiengang :-) Über solche Probleme
wurde schon viel nachgedacht und geforscht. Da gibt es viel zu lesen,
auch deutsche Literatur. Auf Englisch findet man halt mehr. UML kann man
sich bei der Gelegenheit auch mal ansehen.
@Vincent H.
Ja, da ist mir ein Fehler unterlaufen und ich hab das falsche Beispiel
genommen.
Bei den SPI/I2C... Routinen habe ich irgendwann die Funktionspointer
wieder rausgeschmissen, da ich das während der Laufzeit nie ummappen
muss. Hier noch das Beispiel für meine printf Ausgaberoutine:
Reddy schrieb:> Hallo!>> Danke für die Antworten!>> Joerg W. schrieb:>> Auch wenn es nicht exakt das gleiche Problem ist, ich mache so etwas>> über Funktionspointer.> Funktionspointer vs. Callbacks?!?
In C sind Callbacks immer Funktionspointer ja. Ersteres ist einfach nur
ein "Architekturbegriff" und zweiteres die Implementierung davon.
>> extern unsigned char M25P16_SPI_COMM(unsigned char);> Wie!?> Reicht es aus, dass ich in der zu erstellenden Lib das Headerfile von> der unterlagerten Schicht inkludiere und dort die Funktionen als extern> deklariere? Habe ich das nicht so verstanden, dass das implizit immer> eine Deklaration ist?> siehe: Beitrag "Re: Den Befehl extern"
Prinzipiell ja, es ist jedoch bad practice "business logic" (sprich
Header deiner anderen Projekte) in die Bibliothek einzubinden. Genau
dadurch entstehen Abhängigkeiten zwischen Projekt und Bibliothek die du
nicht haben willst. Um genau diese Probleme kümmert sich auch ein
Großteil der von Dr.Sommer geposteten "Patterns", die eben Vorschläge
beinhalten um jene Abhängigkeiten ausschließlich in eine Richtung zu
haben und so gering wie möglich zu halten.
In der Praxis wird das eher so gehandhabt, dass diverse Abhängikeiten in
der Doku vermerkt werden. Dort steht dann exlizit drin "dein Programm
muss die Funktion foo mit der Signatur void(int) beinhalten".
Reddy schrieb:> dort die Funktionen als extern> deklariere?
Das "extern" bei einer Funktionsdeklaration kann man sich schenken denn
es ist implizit bereits enthalten, es hat also keine zusätzliche
Wirkung wenn man es nochmal explizit hinschreibt.
Vincent H. schrieb:> In der Praxis wird das eher so gehandhabt, dass diverse Abhängikeiten in> der Doku vermerkt werden. Dort steht dann exlizit drin "dein Programm> muss die Funktion foo mit der Signatur void(int) beinhalten".
Das ist auch nichts ungewöhnliches, jeder kennt das. So muß zum Beispiel
jedes Programm welches die übliche standard library hinzulinkt eine
funktion mit der Signatur int main(void); enthalten sonst meckert der
Linker.
Man macht das dann einfach ohne groß drüber nachzudenken aber irgendwo
in den Tiefen der standardlibrary in irgendeinem header dort ist int
main(void); deklariert und die wird dann irgendwo aufgerufen und der
Linker sucht dann alles ab ob er irgendwo die Implementierung der
besagten main findet.
Auch zum Beispiel in der STM32 HAL gibt es solche hook-Funktionen mit
denen man sich zum Beispiel in die Interrupthandler der HAL reinhängen
kann.
Genauso kann man das mit eigenen Libs machen, man muß dann halt an
prominenter Stelle dokumentieren welche "hook"-Funktionen zwingend
implementiert werden müssen und was die machen sollen. Wenn man sich
dann noch ein durchgängiges Namensschema ausdenkt, zum Beispiel
name_der_lib Unterstrich hook Unterstrich name_der_funktion um solche
Funktionen deutlich als solche kenntlich zu machen geht das auch ganz
gut und dokumentiert sich fast von selbst.
foo.h
1
voidfoo_init();
2
voidfoo_bar();
3
voidfoo_baz();
4
5
// aplication MUST implement the following hook functions
6
voidfoo_hook_rx(foo_packet_t*data);// implement this to process incoming data
7
voidfoo_hook_blinkenlight(boolon);// foo will call this hook to control (optional) blinkenlights
Also wenn ich in meinem Modul X um ein Header-File erweitere, in der die
Funktionen der darunterliegenden Schicht deklariert werden, kann ich das
Modul als statische Lib bauen, ohne das die Funktionen zur Kompilierzeit
vorhanden, sprich definiert sind?
In meinem Hauptprojekt erstelle ich dann die darunterliegende Schicht.
Inkludiere dort das Interface-Headerfile von meinem Modul X, definiere
in einer c-Datei die Funktionen und befülle sie mit Leben und linke
final die statische Lib meines Modul X hinzu?!?
Joerg W. schrieb:> //###################################################################### #> //# set printf channel> //###################################################################### #> void set_outchar_routine(void (*outfunc)(unsigned char))> {> printf_output = outfunc;> }
Hallo Jörg,
sicherheitshalber solltest du wenigstens die Adresse von outfunc auf !=
NULL prüfen, bevor du sie setzt. :-)
Die Steigerung ist, dass du sogar den Speicherbereich auf Gültigkeit
prüfst (Also z.B. nur zwischen 0x8000000 und 0x8007FFFF, was der interne
Speicherbereich vom Flash ist).
Reddy schrieb:> Also wenn ich in meinem Modul X um ein Header-File erweitere, in der die> Funktionen der darunterliegenden Schicht deklariert werden, kann ich das> Modul als statische Lib bauen, ohne das die Funktionen zur Kompilierzeit> vorhanden, sprich definiert sind?
Zur Kompilierzeit müssen nur alle Deklarationen (Prototypen) da sein auf
die im Code Bezug genommen wird, die eigentlichen Definitionen
(Implementierungen) die an den betreffenden Stellen dann verwendet
werden sollen werden erst beim Linken gesucht. Also ja.
Das mit der Prüfung auf !=0 könnte man sicherlich noch einbauen,
allerdings fällt mir jetzt nicht ein, wie ich der Funktion
"versehentlich" eine 0 unterjubeln kann. Vielleicht mit einem Array von
Funktionspointern, von denen einige unbelegt sind.
Die Prüfung auf den Speicherbereich würde bei 0x8000000 und 0x8007FFFF
nur auf diverse ARMs zutreffen, dazu noch nur auf das Flash. Gerade
kleinere Programme kann man auch zum Testen schnell mal ins RAM schieben
(soweit der Controller das erlaubt) und die STM32 sind für mich nur eine
Controller-Familie unter vielen.
So habe ich aber ein generisches set_outchar_routine(), welches auf
allen derzeit 13 unterstützten Controller-Familien gleich funktioniert.
Ich benutze fast ausschließlich nur noch eine einzige Bibliothek, die
ich für jeden unterstützten Controller einzeln compilieren lasse
(automatisch). Die dafür zuständige Header-Datei mit den Prototypen
lasse ich natürlich auch automatisch erzeugen, bei derzeit teilweise
über 1100 Funktionen ist das manuell nicht mehr zu pflegen. Das extern,
auch wenn es eigentlich nicht gebraucht wird, verhindert so, dass
Routinen mehrfach in das Headerfile wandern. Dazu ignoriert das Script
einfach alle Zeilen, die mit "extern" beginnen. Und so habe ich mir das
halt so angewöhnt...
Jörg
Check!
Das mit den Header-Files funktioniert! ;)
Danke!
Joerg W. schrieb:> Die dafür zuständige Header-Datei mit den Prototypen> lasse ich natürlich auch automatisch erzeugen,
Wow! Und wie machst du das?
VG
Reddy
Grundvoraussetzung ist eine fixe Struktur für das Bauen der
Bibliotheken, die ich mittlerweile auch bei fast allen Projekten
einhalte. Das ist aber erst über die Jahre so "gewachsen", die unilib
ist sozusagen mein privater HAL.
1. Es gibt 3 Unterverzeichisse, src, inc und build
2. Standardisierte Makefiles, Includes und Linkerscripts, natürlich fast
alles via Perl-Scripten erzeugt.
3. Im src Verzeichnis gibt es wieder jede Menge Unterverzeichnisse, für
High-Level Funktionen sind das eigentlich nur Symlinks, da sie für alle
µC Familien weitestgehend identisch sind.
4. Ein Perl-Script liest erst eine Datei mit diversen Definitions ein
und arbeitet danach die ganzen Source-Dateien ab.
5. Die Quelltextdateien haben einen festen Aufbau:
Mit //# beginnt der Kommentar zu einer neuen öffentlichen Funktion. Die
beginnt in der Zeile nach dem letzten Kommentar. Ist die Funktion in ASM
realisiert, steht vor der Deklaration //ASM was beim Erstellen des
Headerfiles wieder entfernt wird. Außerdem gibt es noch //private und
//internal als Kommentare für Variablen. Das Ende einer Funktion ist
erreicht, wenn vorne eine schließende geschweifte Klammer steht.
Ziel des Ganzen ist, die Hardware soweit zu abstrahieren, dass Programme
praktisch ohne Änderungen auf unterschiedlichen Controller-Familien
laufen können. Dabei gibt es natürlich Einschränkungen, denn es werden
nur Grundfunktionen unterstützt. Aber damit komme ich bei den meisten
Projekten bereits aus. Zusammen mit meinem Universalprogrammer kann ich
mich dann auf die Entwicklung der eigentlichen Anwendung konzentrieren,
ein "make start" reicht aus, um das Programm zu compilieren, zu flashen
und dann den Controller via Programmer mit Strom zu versorgen. Oder ein
"make run", um es neu zu kompilieren, entsprechend zu linken und dann im
RAM zu starten.
Normalerweise liegen dann die ganzen Dateien unter
/usr/local/toolchain... , inzwischen kann ich aber auch (automatisiert)
portable Rumpf-Projekte generieren, bei denen nur noch der Pfad zur
Toolchain ins Makefile eingetragen werden muss. Bei Bedarf kann ich
gerne mal so ein Rumpf-Projekt hochladen.
Vor allem interessante Fehler, zB wenn man in einem C Modul die Funktion
aufruft und die in einem C++ Modul definiert (ohne extern C).
STM HAL nutzt das reichlich, ich würde das eher vermeiden.