UNIXwork

C Syntax Error

10. May 2021

Finde den Syntax-Fehler in folgendem C-Code:

#include <stdio.h>

int main(int argc, char **argv) {
    int a = 3;
    switch(a) {
        case 1:
            printf("1\n");
            break;
        case 2:
            printf("2\n");
            int c2 = 0;
            break;
        case 3:
            int c3 = 3;
            printf("%d\n", c3);
            break;
    }
    return 0;
}

Ein ähnlicher Fall wie hier im Beispiel hatte sich bei mir zugetragen. Der Compiler bemängelte die Variablendeklaration nach dem Case. Das heißt, hier im Beispiel ist die fehlerhafte Zeile die Variablendeklaration im Case 3.

Ich gebe zu, dass mich dies erstmal verwirrt hat und ich dachte, ich hätte versehentlich im C90-Modus kompiliert. Der Grund, wieso dies einen Fehler wirft, ist, dass ein Case nichts anderes als ein Label ist und Variablendeklarationen in C nicht mit einem Label versehen werden können.

Autor: Olaf | 0 Kommentare | Tags: c

WTF C Teil 3

16. January 2020

Aus der beliebten Serie WTF C:

bool trigraphs_are_available() {
    // Are trigraphs available??/
    return false;
    return true;
}

Was passiert hier? Zwei return-Statements hintereinander ergeben doch keinen Sinn! Kann die Funktion jemals true zurückgeben?

Auch dieses mal handelt es sich um standardkonformes C und es gibt auch Fälle, in denen die Funktion true liefert. Der Trick ist hier, dass im Kommentar etwas versteckt ist, nämlich am Ende ein Trigraph. Es ist nämlich in C möglich, manche Sonderzeichen durch alternative Zeichenketten auszudrücken. Am Ende des Kommentars steht der Trigraph ??/, der zu einem Backslash aufgelöst wird und ein Backslash am Ende eines einzeiligen Kommentars erweitert diesen auf die nächste Zeile. Wenn also der Compiler Trigraphen unterstützt, sind die ersten beiden Zeilen der Funktion ein Kommentar und übrig bleibt nur return true.

Gefunden habe ich das Beispiel hier.

Autor: Olaf | 0 Kommentare | Tags: c, wtf

Meine Lektionen aus der Entwicklung von dav 1.3

23. December 2019

Die Entwicklung von dav 1.3 ist ganz und gar nicht so verlaufen wie ich mir das vorgestellt hatte. Über ein Jahr später als geplant, konnte ich das Release dann vor kurzem endlich fertig stellen. Ich entwickel das jetzt allerdings im meiner Freizeit, daher variiert es natürlich, wie viel Zeit ich in die Entwicklung stecke. Trotzdem muss ich zugeben, dass die Zeit, die ich investiert habe, nicht unbedingt optimal genutzt wurde.

Schauen wir uns zunächst ein paar Zahlen der Entwicklung an.

version  date        loc          new-loc  n-month  lines/month
---------------------------------------------------------------
begin    2012-11-30      0 lines  0        0        0
1.0.0    2017-08-13  14633 lines  14633    57       256
1.1.0    2017-10-07  15416 lines  783      2        391      
1.2.0    2018-06-26  20465 lines  5049     8        631
1.3.0    2019-12-15  29743 lines  9278     18       515

Für die erste Version von dav habe ich natürlich relativ lange gebraucht. Es ist vermutlich deutlich einfacher, an eine bestehende Software, die man auch noch gut kennt, neues Zeug ranzubasteln, als etwas komplett neues aus dem Boden zu stampfen.

Bei der Entwicklung von dav 1.2 war ich am produktivsten. Aber wenn man nur nach lines/month geht, dann war dav 1.3 eigentlich kein Desaster (im Vergleich zu den anderen Releases). Was man aber sieht ist, dass sich die Code-Basis drastisch vergrößert hat. Viel zu viele neue Features haben den Weg in das Release gefunden. Genau das ist eines der Probleme.

Das größte Ärgernis dabei war, dass ich zwar relativ zügig neue Features implementiert hatte, aber sobald die minimal irgendwie funktioniert haben, bin ich zum nächsten neuen Feature übergegangen. Am Ende hatte ich haufen Neues, das aber nicht gut funktionierte.

Dazu kam auch ein größeres Refactoring der dav-sync Codebasis. Dieses hat natürlich bereits bestehende Funktionalität, die fehlerfrei lief, auch etwas zerstört.

Am Ende war ich Monatelang damit beschäftig, massenweise Tests zu schreiben, um überhaupt wieder Vertrauen in den Code zu entwickeln. Dies war jedoch recht erfolgreich, die Grundfunktionalität dürfte stabiler als vorher sein.

Was ich hoffentlich daraus gelernt habe: Dinge zuende entwickeln. Keine halben Sachen liegen lassen. Und am besten schließt man die Entwicklung eines neuen Features ab, in dem man diverse Tests dafür geschrieben hat, die alle problemlos durchlaufen.

Auch sollte man sich nicht zu viel auf einmal vornehmen. Ich hoffe ich kann den Spruch Release early, release often in Zukunft mehr anwenden. Das hat durchaus seine Vorteile.

Autor: Olaf | 0 Kommentare | Tags: dav, c

Kind-Prozesse erstellen mit posix_spawn

1. December 2019

Ich hatte mal eine Anleitung dazu geschrieben, wie mittels fork und exec Kind-Prozesse erstellt und Programme ausgeführt werden. In diesem Artikel möchte ich die Funktion posix_spawn vorstellen, die im Prinzip das gleiche kann und dabei ein paar Vorteile hat.

In Unix werden traditionell Programme ausgeführt, in dem der aktuelle Prozess geforked, also dupliziert, wird und im neuen Kind-Prozess wird ein anderes Programm-Image mittels exec geladen.

Das Problem an der Sache ist, dass fork mehr oder weniger Overhead haben kann, je nach Betriebsystem. Linux ist da recht effizient, es werden aber immer noch die Page Tables des Parent kopiert. Dazu kommt, dass es auch etwas umständlicher zu programmieren ist, als nur eine einzige Funktion aufzurufen.

Daher wurde irgendwann die Funktion posix_spawn eingeführt:

int posix_spawn(pid_t *pid, const char *path,
       const posix_spawn_file_actions_t *file_actions,
       const posix_spawnattr_t *attrp,
       char *const argv[], char *const envp[]);

Um z.B. das Programm /bin/echo in einem separaten Prozess auszuführen, reicht folgender Code:

char *args[3];
args[0] = "echo";
args[1] = "Hello World";
args[2] = NULL;

pid_t pid; // child pid
posix_spawn(&pid, "/bin/echo", NULL, NULL, args, NULL);

Das ist ein sehr einfaches Beispiel. Was man eigentlich noch bräuchte, ist eine Möglichkeit, die Filedescriptoren für stdin, stdout und stderr zu modifizieren. Hierfür gibt es den Parameter file_action. Um den zu verstehen, sollte man sich noch mal anschauen, wie Programme mittels fork und exec ausgeführt werden, daher noch mal eine kleine Zusammenfassung meines Artikels dazu.

Stdin, stdout und stderr sind Filedescriptoren, die immer die Nummern 0, 1 und 2 haben. Mit der Funktion dup2 können wir einen Filedescriptor duplizieren und der Kopie eine exakte Nummer zuweisen. Was man also machen muss ist, vor dem Erstellen des neuen Prozesses erstmal neue Filedescriptoren für stdin/stdout/stderr zu erstellen (z.B. mit pipe oder open) und nach dem Erstellen des Kind-Prozesses aber noch vor exec werden mittels dup2 die Filedescriptoren auf die Positionen 0, 1 und 2 gelegt.

Der file_actions Parameter bei posix_spawn ist genau dafür gedacht. Diesem können Aktionen wie dup2 oder auch close und open zugeordnet werden, die dann nach dem Erstellen des Kind-Prozesses verarbeitet werden.

Zuerst muss file_actions hierfür initialisiert werden:

posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);

Danach können Actions zugewiesen werden:

// stderr im Kind-Prozess schließen
posix_spawn_file_actions_addclose(&actions, 2);

// stdout in die Datei out.txt umleiten
posix_spawn_file_actions_addopen(&actions, 1, "out.txt", O_CREAT|O_WRONLY, 0644);

Nach dem Ausführen von posix_spawn sollte der Speicher wieder freigegeben werden:

posix_spawn_file_actions_destroy(&actions);

Ich habe auch ein ausführliches Beispiel geschrieben, in dem die Ausgabe in eine Pipe mit Hilfe von posix_spawn_file_actions_adddup2 umgeleitet wird, die dann im Parent ausgelesen wird.

Autor: Olaf | 0 Kommentare | Tags: unix, c

Linkdump

13. March 2019
Autor: Olaf | 0 Kommentare | Tags: links, shell, bash, c, sun
Weiter