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 | extern unsigned char M25P16_SPI_COMM(unsigned char); |
welches die generische SPI-Routine ist. Dann "initialisiere" ich das im Hauptprogramm mit:
1 | unsigned char M25P16_SPI_COMM(unsigned char x) |
2 | {
|
3 | return comm_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
:
Bearbeitet durch User
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.
https://en.wikipedia.org/wiki/Inversion_of_control https://en.wikipedia.org/wiki/Multitier_architecture https://en.wikipedia.org/wiki/Dependency_injection https://en.wikipedia.org/wiki/Strategy_pattern https://en.wikipedia.org/wiki/Observer_pattern
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?!? > 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" Vincent H. schrieb: > Für die selbe Platform: Sicher für die selbe Paltform. ich möchte ja, wie geschrieben meine Lib dazu linken. > - bei Laufzeit-Abhängigkeiten -> Callbacks Mein Vorschlag. > - ohne Laufzeit-Abhängigkeiten -> Funktionsdeklarationen Das ist die Lösung von Joerg W., oder? Dr. Sommer schrieb: > https://en.wikipedia.org/wiki/Inversion_of_control > https://en.wikipedia.org/wiki/Multitier_architecture > https://en.wikipedia.org/wiki/Dependency_injection > https://en.wikipedia.org/wiki/Strategy_pattern > https://en.wikipedia.org/wiki/Observer_pattern uff Viel. Und auf English schauder. Werde es mir aber mal anschauen. Danke. VG Reddy
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:
1 | void (*printf_output) (unsigned char); |
2 | |
3 | //#######################################################################
|
4 | //# set printf channel
|
5 | //#######################################################################
|
6 | void set_outchar_routine(void (*outfunc)(unsigned char)) |
7 | {
|
8 | printf_output = outfunc; |
9 | }
|
10 | |
11 | void printf_outchar(unsigned char c) |
12 | {
|
13 | (*printf_output) (c); |
14 | }
|
15 | |
16 | //#######################################################################
|
17 | //# dummy output
|
18 | //#######################################################################
|
19 | void OUTPUT_NONE(unsigned char c) |
20 | {
|
21 | asm volatile("se_or 2,2"); |
22 | }
|
Bei der Initialisierung wird der Pointer mit OUTPUT_NONE (in diesem Fall PowerPC) vorbelegt.
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.
1 | extern void foo(void); |
und
1 | void foo(void); |
sind hundertprozentig das selbe.
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 | void foo_init(); |
2 | void foo_bar(); |
3 | void foo_baz(); |
4 | |
5 | // aplication MUST implement the following hook functions
|
6 | void foo_hook_rx(foo_packet_t* data); // implement this to process incoming data |
7 | void foo_hook_blinkenlight(bool on); // foo will call this hook to control (optional) blinkenlights |
:
Bearbeitet durch User
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.
Mit weak symbols und dem constructor attribut kann man auch einige interessante Dinge anstellen: https://www.mikrocontroller.net/articles/C_Linkerhacks_%2B_Modularisierung
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.
Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.