Summary
This incident examines a common C‑language pitfall: declaring an extern array in a header but misunderstanding how and where it must actually be defined and initialized. The pattern works, but only when the linkage rules are respected. When misapplied, it leads to linker errors, undefined behavior, or silent data corruption.
Root Cause
The failure stems from confusing declaration with definition.
Key issues:
externdeclares an array but does not allocate storage.- Only one source file must contain the actual definition of the array.
- If multiple files accidentally define the array, the linker reports duplicate symbols.
- If no file defines it, the linker reports an undefined reference.
- If initialization happens before the SPI‑population function runs, other modules may read uninitialized data.
Why This Happens in Real Systems
Real embedded systems often have:
- Multiple compilation units sharing buffers for DMA, SPI, I²C, or ISR‑driven data.
- Tight coupling between hardware‑driven population of arrays and application‑level consumption.
- Initialization order hazards, especially when startup code, interrupts, and tasks run concurrently.
- Developers assuming
externmagically synchronizes state, which it does not.
Real-World Impact
When this pattern is misused, systems experience:
- Random garbage data read by modules expecting valid SPI results.
- Race conditions if interrupts write to the array while another module reads it.
- Linker failures when the array is defined in multiple places.
- Hard‑to‑reproduce bugs because timing affects when the array is populated.
Example or Code (if necessary and relevant)
A correct pattern looks like this:
header.h
extern uint8_t spi_buffer[64];
spi.c
#include "header.h"
uint8_t spi_buffer[64];
void read_spi(void) {
// populate array
}
consumer.c
#include "header.h"
void process_data(void) {
uint8_t x = spi_buffer[0];
}
How Senior Engineers Fix It
Experienced engineers apply several practices:
- One definition rule: ensure exactly one
.cfile defines the array. - Clear ownership: the module that owns the data defines it; others only declare it.
- Initialization discipline: guarantee the array is populated before any consumer reads it.
- Concurrency protection:
- Disable interrupts during critical reads
- Use volatile qualifiers when hardware updates memory
- Add mutexes or memory barriers in RTOS environments
- Encapsulation: expose accessor functions instead of raw global arrays when possible.
Why Juniors Miss It
Common reasons include:
- Misunderstanding
extern—believing it creates a shared variable rather than just declaring one. - Assuming initialization order—not realizing other modules may run before the SPI routine.
- Overusing globals instead of designing proper module boundaries.
- Not recognizing concurrency hazards in embedded systems.
- Copy‑pasting definitions into multiple files, causing subtle linkage issues.
This pattern works—but only when the array is defined once, declared everywhere else, and populated before use.