The "Arduino is C" Misconception: Understanding the Toolchain

A frequent search query among makers and students is "Arduino is C" or "Is Arduino programmed in C?". The confusion is understandable: the syntax resembles C, early educational materials rely on C-style procedural logic, and the core API uses C-like functions such as digitalWrite() and pinMode(). However, the reality of the Arduino build environment is more nuanced. Arduino sketches (.ino files) and core libraries are fundamentally compiled as C++.

When you click "Upload" in Arduino IDE 2.3.x, the IDE does not pass your code directly to a C compiler. Instead, it triggers a multi-stage build pipeline managed by the Arduino CLI build process. Your .ino file is concatenated, injected with #include <Arduino.h>, and saved as a .cpp translation unit before being handed to the avr-g++ compiler.

Problems arise when you introduce external modules, legacy C libraries, or custom .c files into your src/ folder. The boundary between the C and C++ toolchains is a primary source of cryptic compilation and linker errors. Below, we diagnose the most common errors caused by this C/C++ friction and provide exact, actionable solutions.

File Extension Routing in the AVR Toolchain

To diagnose errors, you must first understand how the IDE routes files based on their extensions. The AVR GCC toolchain treats C and C++ files differently, which impacts type checking, memory allocation, and symbol generation.

File Extension Compiler Invoked Language Standard (AVR Core 1.8.6+) Name Mangling
.ino avr-g++ C++14 / C++17 Yes
.cpp avr-g++ C++14 / C++17 Yes
.c avr-gcc C11 / C17 No

Error Diagnosis 1: The "Undefined Reference" Linker Nightmare

The Error Message:

/tmp/ccoXyZ.ltrans0.ltrans.o: In function `setup':
<artificial>:45: undefined reference to `my_c_function()'
collect2: error: ld returned 1 exit status

The Root Cause: C++ Name Mangling
If you write a custom hardware driver in a driver.c file and declare it in driver.h, including that header in your main .ino sketch will trigger this linker error. Because the .ino file is compiled as C++, the avr-g++ compiler applies name mangling to support function overloading. It transforms my_c_function() into a mangled symbol like _Z14my_c_functionv. However, your driver.c file was compiled by avr-gcc (the C compiler), which exports the symbol exactly as my_c_function. The linker (avr-ld) cannot reconcile the two.

The Fix: The extern "C" Wrapper
You must instruct the C++ compiler to use C-style linkage for the functions declared in your header. Modify your driver.h file to include the standard cross-compatibility boilerplate:

#ifndef DRIVER_H
#define DRIVER_H

#ifdef __cplusplus
extern "C" {
#endif

void my_c_function(void);

#ifdef __cplusplus
}
#endif

#endif // DRIVER_H

This ensures that when driver.h is included in your C++ sketch, the compiler looks for the unmangled C symbol, perfectly matching the object file generated from driver.c.

Error Diagnosis 2: Strict Typing and Void Pointer Casts

The Error Message:

error: invalid conversion from 'void*' to 'uint8_t*' {aka 'unsigned char*'} [-fpermissive]

The Root Cause: C vs C++ Pointer Rules
This error frequently occurs when porting legacy C code or using standard C memory allocation functions like malloc() or calloc() inside an Arduino sketch or .cpp file. In standard C, a void* pointer can be implicitly assigned to any other pointer type. C++ strictly forbids implicit void* conversions to enforce type safety.

The Fix: Explicit Casting or C++ Allocation
You have two options to resolve this. The first is to apply an explicit C-style cast:

// C-style explicit cast
uint8_t *buffer = (uint8_t *)malloc(64 * sizeof(uint8_t));

However, since your sketch is inherently C++, the best practice is to use the new operator, which handles type allocation natively and calls constructors if you are allocating objects:

// C++ native allocation
uint8_t *buffer = new uint8_t[64];

Expert Note on ATmega328P Memory: Be highly cautious with dynamic allocation on 8-bit AVR microcontrollers. The ATmega328P has only 2KB of SRAM. Repeatedly calling malloc() and free() (or new and delete) causes heap fragmentation, eventually leading to allocation failures and system crashes. Wherever possible, prefer statically allocated global arrays or memory pools.

Error Diagnosis 3: Missing Arduino API in Pure C Files

The Error Message:

error: implicit declaration of function 'digitalWrite' [-Werror=implicit-function-declaration]
error: 'Serial' undeclared (first use in this function)

The Root Cause: Scope and Header Isolation
If you create a pure .c file to handle math or signal processing, you might attempt to use Arduino API functions like digitalWrite() or the Serial object. Pure C files do not automatically inherit the Arduino environment. Furthermore, attempting to #include <Arduino.h> in a .c file will often result in a cascade of syntax errors because Arduino.h pulls in C++ specific classes (like HardwareSerial and String) that the avr-gcc C compiler cannot parse.

The Fix: The C-Wrapper Pattern
You cannot use C++ objects (like Serial) directly in a .c file. The industry-standard approach is to write a C-compatible wrapper in a .cpp file.

1. Create a uart_wrapper.cpp file:

#include <Arduino.h>

extern "C" void uart_send_byte(uint8_t data) {
    Serial.write(data);
}

2. Declare it in your .c file using the extern keyword:

extern void uart_send_byte(uint8_t data);

void process_signal(void) {
    // Pure C logic here
    uart_send_byte(0xFF);
}

This cleanly separates your hardware-dependent C++ code from your portable, testable C logic.

Advanced Diagnostic Flow: Isolating the Failure Stage

When faced with an unfamiliar error, use this step-by-step diagnostic checklist to isolate whether the issue lies in the pre-processor, the compiler, or the linker. For deeper insights into toolchain quirks, the AVR Libc C++ FAQ is an invaluable resource.

  1. Enable Verbose Output: In Arduino IDE 2.3.x, navigate to File > Preferences and check "Show verbose output during: compilation". This reveals the exact avr-gcc or avr-g++ commands being executed.
  2. Identify the Failing File: Look at the top of the error trace. If the error points to a .cpp or .ino file, it is a C++ syntax or type issue. If it points to a .c file, it is a strict C standard violation.
  3. Check for Linker Errors: If all files compile successfully but the build fails at the very end with collect2: error: ld returned 1 exit status, you have a linker error. This is almost always a missing implementation, a name mangling mismatch, or a missing library dependency in your platform.txt configuration.
  4. Inspect Object Symbols with avr-nm: If you are debugging a complex linker error, locate the temporary build folder (visible in the verbose output). Use the avr-nm tool on the compiled .o object files to inspect the exact symbols the compiler generated. This definitively proves whether a function was mangled or exported correctly.

Summary

While the phrase "Arduino is C" is a common shorthand, the underlying architecture is firmly rooted in C++. By understanding how the IDE preprocesses .ino files, how avr-g++ enforces strict type checking, and how name mangling impacts the linker, you can rapidly diagnose and resolve compilation errors. Always use extern "C" wrappers for cross-language headers, avoid dynamic memory allocation on 8-bit AVRs, and leverage verbose IDE outputs to trace the exact point of failure in the GCC toolchain. For a complete overview of built-in functions and their underlying types, refer to the official Arduino Language Reference.