Thread: Help understanding structuring multiple .c and .h files (intial linker process)

  1. #16
    Master Apprentice phantomotap's Avatar
    Join Date
    Jan 2008
    Posts
    5,108
    May I ask what the big problem with defining variables like this is?
    O_o

    Generally speaking, global variables themselves aren't bad; however, programmers using a lot of global variables will generally be using poor constructs that make reasoning about code with global variables more difficult. In other words, the "big problem" is the commonality of difficult to understand, maintain, and improve code using a lot of global variables.

    1): Global variables with external linkage have the largest scope possible; it is much easier to reason about code where most variables have the most limited scope possible. Consider trying to approach new code using several undecorated, unprotected global variables: there is simply no easy way to observe where such variables are used or mutated without significant code review.

    2): Global variables with external linkage tend to tightly bind, or couple, otherwise unrelated blocks of code; it is much easier to reason about code when different facilities are heavily isolated, having no interrelated dependencies. Consider trying to approach new code offering several interfaces all of which are bound by "side-effects" of operations upon several global variables: there is simply no easy way to isolate, improve, or even maintain a single function without significant code review.

    3): Global variables with external linkage may have only at most primitive access control and constraint checking; it is much easier to reason about code when "preconditions", "constraints", "postconditions", "invariants", "predicates", and all such similar notions of "value", "domain", and "range" are managed internally through interfaces without the need of client code to otherwise participate. Consider trying to approach new code using global variables which must always be valid: there is simply no easy way of confirming such a requirement without significant code review.

    You are using interrupts, so you must have some sort of data available to the interrupts. Consider instead of global variables with external linkage the consistency, clarity, and formal rigidity of a few global variables with internal linkage which are only ever accessed or mutated through an interface which exists within the same translation unit as the variables. You have, with internal linkage and controlling interface, significantly limited the scope of your variables, removed much of the possibility for complex interrelationships, and may enforce any requirements you like.

    Consider the example posted by rcgldr: is there a reason for `pfint0' to have external linkage or allow direct mutation through every `step? No. We are calling `pfint0' with no checks. Sure, we could add checks before calling it, but why would we when we can write our code to guarantee that `pfint0' is never null? With a trivial bit of code, we make our interrupt handler process for the motor more secure which means that the physical construction itself is likely to be more secure.

    This all, of course, is a trivial overview; there is a lot to be said for rigidly isolating modules and facilities so that you may simply "drive" with client code.

    Soma

  2. #17
    Registered User
    Join Date
    Apr 2013
    Posts
    1,658
    Quote Originally Posted by phantomotap View Post
    Consider the example posted by rcgldr: is there a reason for `pfint0' to have external linkage or allow direct mutation through every step?
    If a sequence of interrupts are part of a state machine that goes through a series of states from interrupt to interrupt, then updating a pointer to function, is a better alternative to using a switch case statement on a single common interrupt. The worst example of this was a "floppy tape" driver, that required 300+ cases to handle via a switch case. One issue was that receiving status required two interrupts per bit of received data, and there were either 9 or 17 bits of status to retrieve. Anytime a new series of steps was needed, a set of enums had to be inserted, as well as a set of case statement. It was just awkward. With the pointer to function method, I was able to implement a hierarchial set of "call back" pointers to functions. The lowest level was the interrupt itself. There was a call back for command complete, a call back for status complete, and the higher levels, like a call back for "wait for ready" (an interrupt driven loop that send command asking for status, receiving the status (this took 10 interrupts per loop until the ready status bit was set), and then a call back for higher level functions, such as rewind (sent a rewind command followed by a "wait for ready") process. The company I did this for changed several of their other drivers from the switch case method to the pointer to function method.

    I and others have also used the pointer to function method to handle interrupts on embedded systems, again as opposed to switch case.

    Quote Originally Posted by phantomotap View Post
    We are calling `pfint0' with no checks.
    The rule is that pfint0() is initialized to "unexpected()", a pointer to a function that handles unexpected interrupts. pfint0() is also set back to "unexpected()" whenever a sequence is complete and no further interrupts are expected. NULL is not a valid value for pfint0(). Otherwise, pfint0() reflects the current state of an interrupt driven process.

    Quote Originally Posted by phantomotap View Post
    global variables
    Using global (or static) variables in an interrupt driven process is fairly common, since the stack can't be used to store data between interrupts.
    Last edited by rcgldr; 06-12-2013 at 11:40 AM.

  3. #18
    Registered User
    Join Date
    Dec 2012
    Posts
    15
    Thanks a lot!!! I have been using the large switch statements and enums...but as you said it can become exhausting....needing cases inside cases at times. Lastly (going back to the original question) I understand that extern is simply a method of explicitly "declaring without defining." So I want to compile all source files which will require me to define numerous times, but the goal is to only define once. Why is it that in my main.c file, I have to place #define MAIN BEFORE all of my header files, and not after them? If I place #define MAIN after the header files, I get the "undefined reference" error. If I place it before them all (excluding var.h) everything is fine. I feel like I am missing something big in regards to how the compiler and linker puts everything together...because I think I am grasping the whole extern and define verses declare thing.

  4. #19
    Master Apprentice phantomotap's Avatar
    Join Date
    Jan 2008
    Posts
    5,108
    If a sequence of [...] function method.
    O_o

    Yes? And? Did I say anything about your use of a state machine?

    No. I did not. I made specific reference to your implementation.

    I and others have also used the pointer to function method to handle interrupts on embedded systems, again as opposed to switch case.
    Did I say you shouldn't be using function pointer?

    No. I did not. I made specific reference that one would continue using a function pointer.

    The rule is that pfint0() is initialized to "unexpected()", a pointer to a function that handles unexpected interrupts. pfint0() is also set back to "unexpected()" whenever a sequence is complete and no further interrupts are expected. NULL is not a valid value for pfint0(). Otherwise, pfint0() reflects the current state of an interrupt driven process.
    Did I say I didn't understand the requirements?

    No. I did not. I made specific reference to guaranteeing the requirements which you have not done.

    Using global (or static) variables in an interrupt driven process is fairly common, since the stack can't be used to store data between interrupts.
    Did I say that the use of global variables was uncommon?

    Did I say that interrupts could use stack variables for communication over multiple signals?

    No. I did not. I made specific reference to the requirement of a globally accessible variable.

    So, what we've learned from post #17 is that you don't understand how using global variables with external linkage is a poor implementation choice separate and distinct from design patterns you may wish to employ. You can still use the strategy of a pointer-based state machine without a single global variable with external linkage, magic "rules" for clients to obey, or binding each transition to any other over otherwise separate state.

    This is then your lesson for the thread: implementation and design need not be the same.

    Your lack of understanding good design practices places no burdens of incomprehension on others. In other words, you may not understand yet due to inexperience, but every programmer may learn the value of better implementation techniques and so may use them in any design.

    Soma

  5. #20
    Registered User
    Join Date
    Apr 2013
    Posts
    1,658
    Quote Originally Posted by DanielB33 View Post
    Thanks a lot!!! I have been using the large switch statements and enums...but as you said it can become exhausting....needing cases inside cases at times. Lastly (going back to the original question) I understand that extern is simply a method of explicitly "declaring without defining." So I want to compile all source files which will require me to define numerous times, but the goal is to only define once. Why is it that in my main.c file, I have to place #define MAIN BEFORE all of my header files, and not after them?
    One possibility is that there are multiple include files with references to variables that are to be defined in main.c. Another possibility is that several .c files contain global variables, which is common with embedded applications. For example, on a hard drive, global variables related to the SATA interface would be in some SATA .c file, global variables related to buffering would be in another .c file, global variables related to the head stepper in different .c file, global variables related to the kernel (operating system) in another .c file.

    The pointer to function method for handling interrupts makes it easier to maintain the code. If you need to add an extra step to an interrupt driven process, you normally just have to update a single .c file. Using the sample above, you could create a step1a() to go between step1() and step2(), or rename them to step1(), step2(), step3(). You don't have to edit other files to insert entries into a enum table and then insert a set of cases in a switch case statment.
    Last edited by rcgldr; 06-12-2013 at 12:45 PM.

  6. #21
    Registered User
    Join Date
    Dec 2012
    Posts
    15
    Thanks to everyone for the help. This has been very educational.

  7. #22
    Registered User
    Join Date
    Apr 2013
    Posts
    1,658
    Quote Originally Posted by phantomotap View Post
    I made specific reference to guaranteeing the requirements which you have not done.
    I stated that the requirements for the pointers to functions that they be set to unexpected interrupt handlers when not programmed to represent a specific state.

    Quote Originally Posted by phantomotap View Post
    You can still use the strategy of a pointer-based state machine without a single global variable with external linkage.
    The idea here is to allow the code for separate state sequences to be kept in separate source files. Functions could be added to the interrupt handler source file to set the pointer to functions, so that the pointers to functions could be static, but then those functions are global, so it doesn't seem like there's much benefit. If there's a trace feature on this processor, it's just as easy to trace on a write to a pointer to function as it is to trace on a call to a function to write to a pointer.
    Last edited by rcgldr; 06-12-2013 at 01:13 PM.

  8. #23
    Master Apprentice phantomotap's Avatar
    Join Date
    Jan 2008
    Posts
    5,108
    I stated that the requirements for the pointers to functions that they be set to unexpected interrupt handlers when not programmed to represent a specific state.
    O_o

    You used words in English to explain "rules" which you'd prefer client code to follow, and you only really did that much after I referenced the lacking requirements.

    You are only asking people to follow what is required, and that is but one of the problems with your use of simple global variables.

    I'm talking about using code to guarantee that the "rules" are followed.

    [Edit]
    I somehow managed to hit "save" instead of "go advanced" so posted something incomplete.
    [/Edit]

    [Edit]
    The issue here is that the lack of understanding is only the beginning of your problems.

    For example, you imply that simply debugging a common write would be easy given a facility that is common. You are correct. Now, what if there is an uncommon write? Trivial bugs such as what your code allows has lived in code which has been used, maintained, and updated for over a decade because some tiny bit of code misbehaves in a concatenation of events which can not possible be covered by a simple approach to debugging.

    Which brings us back to my reference of `gets'; you can certainly debug a poor use of `gets' which is the result of common and fixed code, but you can't cover all possible cases using `gets' so you must avoid `gets' entirely to get safe code.

    In other words, the idea here is to prevent not only the code for separate state sequences but also unrelated code from misbehaving as much as is possible.
    [/Edit]

    Soma
    Last edited by phantomotap; 06-12-2013 at 01:35 PM.

  9. #24
    Registered User
    Join Date
    Apr 2013
    Posts
    1,658
    Quote Originally Posted by phantomotap View Post
    I'm talking about using code to guarantee that the "rules" are followed.
    This would depend on what the "rules" include. As mentioned, calling functions to set call back pointers to functions or when possible, calling functions that both set the call back pointers and initiate the next interrupt driven sequence would allow the call back pointers to be static instead of global.

    The main point about this method is to replace the switch case statement in the main interrupt routine with a call back pointer to function. Then that same idea can be applied so that there are a hierarchical set if call back pointers to functions. How the pointers should get set is a separate issue. The "cleanest" method would be to use a function to set those call back pointers to functions. The reason I did not do that in my example code was to demonstrate the concept as simply as possible.

    Quote Originally Posted by phantomotap View Post
    For example, you imply that simply debugging a common write would be easy given a facility that is common. You are correct. Now, what if there is an uncommon write?
    It doesn't matter. The debugger tool would be set to capture a trace on any write to a specific address or set of addresses, similar to a data break point with a source level debugger. Some embedded debugger tools can trigger on external memory accesses, such as DMA.

    Quote Originally Posted by phantomotap View Post
    Trivial bugs such as what your code allows has lived in code which has been used, maintained, and updated for over a decade because some tiny bit of code misbehaves in a concatenation of events which can not possible be covered by a simple approach to debugging.
    I'm not sure of your point here. I agree that the unchecked setting of pointers or indexes increases the potential for possible bugs. The checking of indexes for a valid range doesn't add too much overhead, but I don't see a reasonable method to validate the setting of pointers to functions (using or indexing a table for pointers to functions would reintroduce the overhead of the switch case implementation).

    In this particular case of using pointers to functions to handle a sequence of interrupts, it isn't too difficult to validate that every possible call back function has been called (error situations may have to be simulated).

  10. #25
    Registered User
    Join Date
    Apr 2013
    Posts
    1,658
    Example code to explain what I mean by a hierarchical set of pointer to functions. pfint() is the lowest level call back pointer, and pframp() is a higher level call back pointer. In this example the call back pointer for startramp() is passed as a parameter. A function could also be used to set the call back pointer pfint() via a parameter.

    (Note - in the case of that floppy tape driver I worked on, there were 5 levels of pointers, for example, rewind() was high level, it would start a rewind operation, then call waitforready(), which in turn would call getstatus(). getstatus() would go through it's 10 interrupt sequence and then call back the function passed from waitforready(). The next step in waitforready() would keep repeating the interrupt driven sequence until ready or an error was detected, then waitforready would return to the call back function passed from rewind(), which would in turn call back the function passed to rewind() as a parameter.)

    Code:
    typedef void (*PFUN)(void);     /* typedef for generic pointer to function */
    
    void unexpected(void);          /* unexpected interrupt handler */
    void rstep0(void);              /* lowest level sequence */
    void rstep1(void);
    void rstep2(void);
    
    void ramp0(void);               /* higher level sequence */
    void ramp1(void);
    
    PFUN pfint  = unexpected;       /* ptr to call back for interrupt */
    PFUN pframp = unexpected;       /* ptr to call back for ramp up */
    
    void interrupt(void)            /* interrupt handler */
    {
        pfint();
    }
    
    void startramp(PFUN callback)
    {
        pframp = callback;          /* set callback ptr */
        pfint  = rstep0;            /* set ptr for next rstep */
    /*                              ** start ramp sequence */
    }
    
    void rstep0(void)               /* rstep 0 handler */
    {
        pfint = rstep1;             /* set ptr for next rstep */
    /* ...                          ** handle this rstep */
    }
    
    void rstep1(void)               /* rstep 1 handler */
    {
        pfint = rstep2;             /* set ptr for next rstep */
    /* ...                          ** handle this rstep */
    }
    
    void rstep2(void)               /* rstep 2 handler */
    {
        pfint = unexpected;         /* not expecting another interrupt */
    /* ...                          ** handle this rstep */
        pframp();                   /* call the call back handler */
    }
    
    void ramp0(void)                /* start a ramp up */
    {
        startramp(ramp1);
    }
    
    void ramp1(void)
    {
    /*                              ** ramp up is done */
    }
    Last edited by rcgldr; 06-12-2013 at 05:27 PM.

  11. #26
    Master Apprentice phantomotap's Avatar
    Join Date
    Jan 2008
    Posts
    5,108
    I'm not sure of your point here.
    O_o

    I am only too well aware that you don't understand, but I have truly grown tired of this unfulfilling discussion so I'm forced to tell you to search for I simply have lost the energy to deal with this further.

    It doesn't matter. The debugger tool would be set to capture a trace on any write to a specific address or set of addresses, similar to a data break point with a source level debugger. Some embedded debugger tools can trigger on external memory accesses, such as DMA.
    A debugger is a tool of science not magic; a debugger can not trace every potential write born of events that are not considered during debugging.

    I invite you to study the history of the profession by searching first for "Heisenbug" to follow the long trail.

    using or indexing a table for pointers to functions would reintroduce the overhead of the switch case implementation
    As before, implementation and design need not be the same; it would be trivial to develop a similar machine driven by similar state such that you would never need a `switch' case.

    I invite you to search for "Table-Driven Design" while realizing that pointers are just values in C.

    Soma

  12. #27
    Registered User
    Join Date
    Apr 2013
    Posts
    1,658
    Quote Originally Posted by phantomotap View Post
    "Table-Driven Design"
    The point here is to avoid the overhead of the table driven method which is similar to the overhead of the switch case method. With the pointer to function / call back method, if steps need to be added or inserted for a specific sequence, only a single source file needs to be updated. With the table or switch case methods, in addition to coding the new steps, the programmer has to add more entries to the table or switch case in another source file, plus update the enums or global declarations in yet another source file. It gets even worse when a hierarchical situation is involved, especially if a new level of hierarchy is to be added for a particular sequence.

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. Automatic variables default intial value
    By chibi.coder in forum C Programming
    Replies: 5
    Last Post: 12-20-2011, 01:53 AM
  2. Understanding the Linker Map File
    By Hops in forum C Programming
    Replies: 0
    Last Post: 01-14-2010, 05:40 PM
  3. unable to process multiple files
    By ss_ss in forum C Programming
    Replies: 4
    Last Post: 05-28-2009, 02:04 AM
  4. Multiple theads to process video files
    By matth in forum C++ Programming
    Replies: 0
    Last Post: 07-15-2005, 02:55 PM
  5. Linker errors - Multiple Source files
    By nkhambal in forum C Programming
    Replies: 3
    Last Post: 04-24-2005, 02:41 AM