1

I have this simple that is supposed to keep a log for me with a fixed number of entries. When I add to the log the oldest replaced is by the second oldest etc and the new entry added. Except when I add another entry both that and the one before have the same value (My C isn't any good, I'm more a ruby and lua guy).

What am I doing wrong here?

void setup(void) {
  Serial.begin(9600);
}

#define log_size 5
#define line_height 12
char *log_lines[log_size] = {"Hello", "World"};

void addToLog(char *text) {
  for(int i = log_size - 1; i > 0; i--) {
    log_lines[i] = log_lines[i-1];
  }
  log_lines[0] = text;
}

void printLog() {
  for(int i = 0; i < log_size; i++) {
    Serial.print(i);
    Serial.print(" ");
    Serial.println(log_lines[i]);
  }
}

void loop(void)
{
  char msg[20];
  sprintf(msg, "%d", millis());
  addToLog(msg);
  printLog();
  delay(1000);
}

The output after the first addToLog is

0 1000
1 Hello
2 World

second addToLog is

0 2001
1 2001
2 Hello
3 World

2 Answers 2

1

It is easy - in your log you store POINTERS to strings.

And you then create variable on the stack (ouch) and put its address to the log.

Then you leave the function (loop) (so that variable is forgotten and can be overwritten by anything). I will not say it is luck, that nothing uses the stack enought to make a problem before the loop is called again, as it disguise the problem. The POINTER in log_lines[0] points to UNALLOCATED place (where the variable msg once was).

Then the loop allocates variable msg (at the same place, as its previous run by the chance) so the POINTER stored in log_lines[0] (still pointing to the same place) points to the variable msg (and its content). By pure (bad) luck.

Then you overwrite the variable msg with a new value, so the log_lines[0] point to that new value.

Then you move the content of log_lines up, so the log_lines[1] now points to the variable msg (and its NEW value) as well as log_lines[0].

Then you assign log_lines[0] with address of variable msg (which it accidetally already contains)

Then you wonder, why variable msg accesed via log_lines[1] is exactly same as the (same) variable msg accesed vial log_lines[0] and why it contains the last (NEW) value, which you assigned into it.


Some of possible solutions are:

1) allocate place on heap (memaloc it) in the addToLog function, copy the text into it and store pointer to THIS place (and do not forget deallocate last log_lines before overwriting it - if it is not null (as untinitialized values are all 0)) which could lead to (relativelly) expensive heap manipulations and possible memory fragmentation.

2) allocate log_lines as array of char arrays of given len, copy the content of each line in another loop inside addToLog and then copy(and possibly truncate) the text to the log_line[0]

3) keep static index of last used log_line and in addToLog just increment it (wrapping around the len of the array), then overwrite the line pointed by index with (truncated) text and in printLog just start cycle on the index and climb (wrapping around len of the array) until you are again at index (which you would not print again and stop)

3
  • Thank you for pointing out those three solutions. The first on, in loop() was char *msg = (char *) malloc(20); and you're right, the memory did skyrocket without free(log_lines[log_size-1]); in the addToLog function. Thank you. Commented Jan 18, 2017 at 7:43
  • That is easy too - you created new variable each time you went to loop() (every second as the delay(1000) on the end), but did not free that memory on the end of loop(). This is why you should (in this variant) allocate memory in the addToLog, copy the text there and deallocate the last log_line there. Otherwise you are heading to problems if you call addToLog more times in the loop (double free ot he same variable, storing the same pointer multiple times and overwrite the logged messages) as well as if you are not calling it at all (not freeing memory at all,skyrocketing heap).
    – gilhad
    Commented Jan 18, 2017 at 8:38
  • Usually you should deallocate at the same level you allocate and each allocate should be paired with deallocate. Actually having msg as normal variable in loop() is good approach, allocate copy in addToLog (cause there are you copying a text to your array) and deallocate there the array rows from previous calls, when rolled out. This way you can use the log as it is meant - to log some history of interesting points and show it on request.
    – gilhad
    Commented Jan 18, 2017 at 8:43
0

Here is a solution which does not use dynamic allocation nor does it copy any log message. It's essentially a zero-copy implementation of gilhad's solution 3. The idea is: instead of having an addToLog() function that takes an already prepared message (which it would need to copy), you have a get_log_buffer() function that tells you where to write the message in the first place. This way you can snprintf() directly into the right place in memory.

const size_t LOG_SIZE = 5;
const size_t MSG_SIZE = 20;

char log_stack[LOG_SIZE][MSG_SIZE];  // circular log buffer
size_t log_index;  // next available slot in the circular buffer

// Return the address of the next available message slot.
char *get_log_buffer()
{
    char *buffer = log_stack[log_index];
    if (++log_index == LOG_SIZE) log_index = 0;  // increment index
    return buffer;
}

void print_log()
{
    Serial.println(F("--------"));
    for (size_t i = 0, j = log_index; i < LOG_SIZE; i++) {
        j = (j ? j : LOG_SIZE) - 1;  // decrement index
        if (log_stack[j][0]) {  // should be a non-empty message
            Serial.print(i);
            Serial.print(" ");
            Serial.println(log_stack[j]);
        }
    }
}

void setup()
{
    Serial.begin(9600);
}

void loop()
{
    snprintf(get_log_buffer(), MSG_SIZE, "%lu", millis());
    print_log();
    delay(1000);
}

Note that log_stack and log_index are zero-initialized by the C runtime. Note also the use of snprintf() instead of sprintf() in order to avoid buffer overruns.

4
  • The problem is, that user MUST limit the lenght of the message to the right size everytime. Which is possible to do right in small script, but it will be pain to do right if the log is in its own library and may dynamically react to aviable size of memory. Basically it is better, when functions/objects are responsible for thier internal state, than to rely on correct user input. One mistake then can do a lot of sideeffect, like overwriting other logs, other variables, stack and such, which are really hard to trace. So while this approach works, I would not recomend it if there is other way.
    – gilhad
    Commented Jan 18, 2017 at 21:46
  • @gilhad: It very much depends on the use case. Obviously, you won't use an arbitrary string limit in a general purpose library. But in a self-contained program, avoiding dynamic memory allocation can make a lot of sense. Commented Jan 19, 2017 at 8:26
  • Yes, but it is easier to check (and change) on the one place, where you store the message, than everywhere in the code, where you want to log something. So I do not forbid this approach, but I strongly suggest it, as is less error prone and commonly better practice for someone starting to write programs in C and have this kind of problems. And you can see, that 2 of 3 variants in my answer are based on static allocation. But the author have to know, if he needs variable size string, or if fixed leght works for him better. I just elaborate on what he (or you) provided.
    – gilhad
    Commented Jan 19, 2017 at 13:35
  • I personally would allocate just array of chars , trim messages to its lenght and manage two pointers on where the log starts and where it ends, deleting necessary number of lines to accomodate new message. So buffer would be used to max and history len would depend on size of respective messages. (and I would empty the buffer after dumping it to not have duplicate entries in the log print. And probabely started any log with timestamp too. But it is way more complicated to implement for someone not used to pointers and C strings)
    – gilhad
    Commented Jan 19, 2017 at 13:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.