DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Improving Java Code Security
  • Improving Unit Test Maintainability
  • 7 Awesome Libraries for Java Unit and Integration Testing
  • Introduction to iText 7

Trending

  • Modern Test Automation With AI (LLM) and Playwright MCP
  • Understanding the Shift: Why Companies Are Migrating From MongoDB to Aerospike Database?
  • Software Delivery at Scale: Centralized Jenkins Pipeline for Optimal Efficiency
  • Advancing Robot Vision and Control
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. Practical Use of Weak Symbols

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.

By 
Dinoja Padmanabhan user avatar
Dinoja Padmanabhan
·
Apr. 02, 25 · Analysis
Likes (2)
Comment
Save
Tweet
Share
2.1K Views

Join the DZone community and get the full member experience.

Join For Free

In 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. 

  1. Preprocessing. This is the initial stage where the preprocessor modifies the source code based on directives. 
  2. Compilation. In this stage, the preprocessed code is translated into assembly language. 
  3. Assembly. The assembler converts the assembly code into machine-readable code specific to the target architecture. 
  4. 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:

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.

Shell
 
#> 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.

Shell
 
#> 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.

Shell
 
#> 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.

Shell
 
#> 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:

C
 
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.

C
 
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.

C
 
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.

C
 
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. 

C
 
// 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.

Shell
 
#> gcc -o demo1 main.c

#> ./demo1
is lib linked? [0]


When init_lib() is defined and linked to main.c:

C
 
// lib.c
#include <stdio.h>

void init_lib(void) {
        printf("init lib\n");
}


It produces the following output:

Shell
 
#> 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.

C
 
// 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():

C
 
// 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.

Library Weak reference unit test

Opinions expressed by DZone contributors are their own.

Related

  • Improving Java Code Security
  • Improving Unit Test Maintainability
  • 7 Awesome Libraries for Java Unit and Integration Testing
  • Introduction to iText 7

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!