Practical Use of Weak Symbols
Learn about weak symbols, what they are, how they can be used in system software development, and some practical uses and examples.
Join the DZone community and get the full member experience.
Join For FreeIn today's software development, flexibility, maintainability, and performance are paramount, especially in systems software programming, embedded systems development, and library design. A lesser-known technique, yet a powerful tool to achieve these goals, is the weak symbol. For developers building frameworks and platform libraries, weak symbols provide a clean way to define default behaviors and enable optional functionality — all without complex build configurations or runtime hacks.
Let’s explore what weak symbols are, how they work, and where they can unlock better architecture patterns.
What Are Weak Symbols?
To understand what weak symbols are, let's revisit the four stages of compilation of a C program.
- Preprocessing. This is the initial stage where the preprocessor modifies the source code based on directives.
- Compilation. In this stage, the preprocessed code is translated into assembly language.
- Assembly. The assembler converts the assembly code into machine-readable code specific to the target architecture.
- Linking. The linker combines the object code with necessary libraries and resolves external references, such as function calls to
printf()
or other library functions. It produces the final executable file.
The symbols are nothing but named functions or variables recognized by the linker. By default, symbols are “strong,” meaning if there are two definitions with the same name, then the linker will raise an error.
Weak symbols are different: they allow multiple definitions without errors. If a strong symbol exists, it overrides the weak one. If not, the weak symbol becomes the fallback.
For example, I have two .c files: weak.c
and strong.c
, as follows, followed by main.c
:
// weak.c
#include <stdio.h>
#include "common.h"
int __attribute__((weak)) config_var = 10;
void __attribute__((weak)) init_service(void) {
printf("Default service initialized with config [%d]\n", config_var);
}
///////////////////////////////////////////////////////////////////////////////
// strong.c
#include <stdio.h>
#include "common.h"
int config_var = 40;
void init_service(void) {
printf("Custom service initialized with config_var [%d]\n", config_var);
}
///////////////////////////////////////////////////////////////////////////////
// main.c
#include <stdio.h>
#include "common.h"
int main(void) {
init_service();
return (0);
}
Now, if I create a demo application called demo1
and try to run it, though there are two definitions of config_var
and init_service()
, only the strong definition is picked.
#> gcc -o demo1 weak.c strong.c main.c
#> ./demo1
Custom service initialized with config_var [40]
If I create a demo application called demo2
without compiling in strong.c
, then the weak definition is used.
#> gcc -o demo2 weak.c main.c
#> ./demo2
Default service initialized with config_var [10]
To understand this a little bit better, if we try to examine demo1
and demo2
using the nm tool, which lists all the symbols in the object file, and grep for the interesting symbols, we can find out what they are defined as.
In demo1
, config_var
is defined as a valid global symbol, and init_service()
is defined as a global text symbol.
#> nm demo1 | grep config_var
0000000000600b5c D config_var
#> nm demo1 | grep init_service
00000000004007b3 T init_service
Whereas in demo2
, config_var
is defined as a weak object, and init_service()
is defined as a weak reference.
#> nm demo2 | grep config_var
0000000000600ad8 V config_var
#> nm demo2 | grep init_service
0000000000400795 W init_service
Please note each type of symbol and its definition from the man pages of the nm utility:
- D – A global symbol naming initialized data
- T – A global text symbol
- V – A weak object
- W – A weak reference
Why Weak Symbols Matter
Weak symbols provide more than just linker tricks — they enable cleaner architecture patterns in system-level software, libraries, and extensible applications. Key benefits include:
- Default implementations. Deliver built-in behaviors that users can replace without modifying the source code.
- Optional feature hooks. Enable debugging, tracing, or analytics only when strong overrides are present.
- Optional definition for a weak reference. A Weak reference doesn't necessarily require a definition.
- Testability. Mock system functions for faster, isolated unit testing.
Let’s dive into some practical examples.
1. Default Implementations in Libraries
Libraries often require user-provided implementations like custom memory allocators. Weak symbols allow a default while letting users override functionality cleanly:
void *addr;
void __attribute__((weak)) init_hardware() {
printf("Default hardware setup\n");
addr = malloc(1MB * 1KB); // uses malloc to allocate 1G memory
}
void system_start() {
init_hardware();
printf("System running\n");
}
Custom initializations become easy; one could provide a custom memory allocator instead of the system default malloc()
call.
void init_hardware() {
printf("Custom hardware setup\n");
addr = my_alloc(1MB * 1KB); // uses a custom my_alloc() to allocate 1G memory
}
The library works with or without user-provided implementation.
2. Optional Debug Hooks
Debugging often requires extra output without bloating the production code. Weak symbols enable debug hooks without conditional compilation.
void _attribute_((weak)) log_event(const char* event __attribute__((__unused__))) {
// Default: No-op
}
void handle_action() {
log_event("Action triggered");
}
Developers add a strong override for debugging.
void log_event(const char* event) {
printf("Event: %s\n", event);
}
One could extend the benefit to other types of hooks, such as for analytics and tracing.
3. Optional Definition for a Weak Reference
A program can have a weak reference without any real definition. This can be used to determine whether the program is linked with a custom library. In the following example, a given library has an initialization function called init_lib()
, which is a weak reference in itself.
// lib.h
#ifndef _LIB_H_
#define _LIB_H_
extern void __attribute__((weak)) init_lib(void);
// returns 1 if the library is linked otherwise 0
extern int is_lib_linked(void);
extern int is_lib_linked(void) {
return (((void *)&init_lib) != NULL);
}
#endif /* _LIB_H_ */
///////////////////////////////////////////////////////////////////////////////
// main.c
#include <stdio.h>
#include "lib.h"
int main(void) {
printf("is lib linked? [%d]\n", is_lib_linked());
return (0);
}
It produces the following output.
#> gcc -o demo1 main.c
#> ./demo1
is lib linked? [0]
When init_lib()
is defined and linked to main.c
:
// lib.c
#include <stdio.h>
void init_lib(void) {
printf("init lib\n");
}
It produces the following output:
#> gcc -o demo2 lib.c main.c
#> ./demo2
is lib linked? [1]
4. Unit Test Mocks
Weak symbols simplify unit testing by enabling easy function replacement. This can be used as an alternative to wrap functions (--wrap
) for mocking.
// hdr.h
#ifndef _HDR_H_
#define _HDR_H_
int __attribute__((weak)) get_val(void);
#endif
//////////////////////////////////////////////////////////////////////////////
// main.c
#include <stdio.h>
#include "hdr.h"
int get_val(void) {
return 10;
}
int main(void) {
printf("val [%d]\n", get_val());
return (0);
}
The unit test code below defines a mock get_val()
:
// test.c
#include <stdio.h>
#include "hdr.h"
int get_val(void) {
return 100;
}
No need for runtime mocking libraries or complex dependency injection setups.
Final Thoughts
Weak symbols provide a seamless way to build systems that are more resilient, testable, and maintainable, making them an ideal choice for systems programming, embedded software, and extensible libraries.
Developers of frameworks and runtime-modifiable systems can benefit from the often-overlooked power of weak symbols. By leveraging weak symbols, you can achieve a clean, modular design without incurring runtime penalties, resulting in a more efficient and scalable system. Your linker — and your codebase — will appreciate the simplicity and flexibility that weak symbols bring.
Opinions expressed by DZone contributors are their own.
Comments