Tab-local structs

I'm trying to have a tab that uses a struct, but the Arduino IDE is telling me it's out of scope within the same tab I'm declaring it... Why is this happening, and is there a good workaround? Here's a minimal example of the phenomenon.

scope_test.ino (project tab):

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}

second_tab.ino:

typedef struct str {
  char c;
  str* next;
} str_t;

void function(str_t* s) {
  return;
}

Error message:

second_tab:6:15: error: variable or field 'function' declared void
    6 | void function(str_t* s) {
      |               ^~~~~
second_tab:6:15: error: 'str_t' was not declared in this scope
second_tab:6:22: error: 's' was not declared in this scope; did you mean 'sq'?
    6 | void function(str_t* s) {
      |                      ^
      |                      sq
exit status 1
variable or field 'function' declared void

In the C++ programming language Arduino is based on, a name must be declared before it is referenced. Since a definition is also a declaration, you can accomplish this by ordering the definitions so that each name is defined before it is referenced in the program (as you have done). However, that approach is not always convenient for the organization of the program, and sometimes it's not even possible.

The alternative is to use function prototypes, where you declare only the signature of the function. So you only need to put all the prototypes at the top of your program and then you can arrange the definitions in any way you like.

This is a complex subject for the people Arduino is intended to make embedded systems accessible to, so the approach Arduino used is to automatically generate prototypes for all the functions in the .ino/.pde files of the sketch which don't already have a prototype, inserting those generated prototypes before the code is compiled as C++.

That prototype generation system works perfectly 99.99% of the time, but it turns out that it's actually a very complex task. So in some cases the prototype generation system doesn't get it right, which results in there being bugs in invisible code.

If you turn on verbose output during compilation and then check the last commands in the black console pane at the bottom of the Arduino IDE window after compiling, you'll see the path to the temporary build folder that contains the preprocessed sketch files. This is what your sketch looks like after preprocessing:

#include <Arduino.h>
#line 1 "E:\\scope_test\\scope_test.ino"
#line 1 "E:\\scope_test\\scope_test.ino"
void setup();
#line 6 "E:\\scope_test\\scope_test.ino"
void loop();
#line 6 "E:\\scope_test\\second_tab.ino"
void function(str_t* s);
#line 1 "E:\\scope_test\\scope_test.ino"
void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}

#line 1 "E:\\scope_test\\second_tab.ino"
typedef struct str {
  char c;
  str* next;
} str_t;

void function(str_t* s) {
  return;
}

This line is the generated prototype for your "function" function:

void function(str_t* s);

You can see it was added above the declaration of the str_t type. This is the cause of the error.

The sketch preprocessor only generates prototypes when the sketch author hasn't already provided their own, so the workaround for this issue is to add the prototype for your "function" function to your sketch at the appropriate location, after the declaration of the str_t type. Something like this:

typedef struct str {
  char c;
  str* next;
} str_t;

void function(str_t* s);

That prototype generation system works perfectly 99.99% of the time . . .

I think you may have to review the “99.99%” statistic. Users appear to get caught by this with increasing frequency. I did recently, incidentally, report a specific instance of this behavior:

Keep in mind that a huge number of people are using the Arduino IDE, etc. I keep an eye out for reports of this type of problem and I see maybe one a month.

This sort of issue has been happening from the start. Arduino did a big campaign to completely rewrite the prototype generation system when they moved it from the Arduino IDE's Java code base to the Golang-based arduino-builder. That was released in Arduino IDE 1.6.6 when it was not very mature, resulting in a huge increase in the frequency of these issues. Over the course of the rest of the Arduino IDE 1.6.x series, the prototype generation system was steadily improved. Finally it reached a state where it was significantly better than the pre-arduino-builder system. At that point, it sort of turned into a "whack-a-mole", where every fix caused another class of failures, and I think Arduino has decided it's good enough for now and no real work has been done on it for the last couple years.

So I don't see any reason why there would be an uptick. From my perspective, if you look at the long-term trend, going back to the 1.6.x series or any time before then, the frequency has significantly decreased.

That prototype generation system works perfectly 99.99% of the time . . .

There is something in the mechanism that insures it will fail at the very worst possible time during the project.

At a certain point, one just starts doing all the things that any normal C/C++ programmer has done since Hello World.

a7

That prototype generation system works perfectly 99.99% of the time .

I have started to use PlatformIO which requiredsfunction prototypes to be declared. At first I saw this as a disadvantage, but it is easy to do, and the code completion feature makes it almost painless to use

As I see it about the auto prototype generation bugs, the biggest issue is that it is not transparent and the error messages generated are misleading. It usually catches experienced users who might be using the sort of constructs that are not correctly handled by this "feature".

Of course it is not the only surprise which can affect even experienced users. The redefinition of, say the standard C/C++ function max() in Arduino.h can have unexpected side effects. Also, the use of "Arduino" functions in a class constructor can cause unexpected behaviour if these are overridden by the later invocation of init().

The renaming of the interrupts from the AVR standard to the Arduino standard in the the Arduino Mega is another one which causes lost time. The abstraction layer is good protection for new users, but not so nice when you have to go behind it to use platform native code.

Some things you just have to know.