Welcome to the next lesson in our project. In our last session, we successfully connected the public API to your library's powerful internals, implementing the core functions like hnsw_load, hnsw_insert, and hnsw_search. This brought your vector database to life, but a truly professional library requires more than just a working implementationโit needs a robust, distributable interface.
This lesson focuses on finalizing the hnsw.h header file, transforming it from a simple declaration list into a professional contract that can be safely used across different projects and even with different programming languages. We will tackle two critical aspects of header file design: preventing multiple inclusion errors with "include guards" and ensuring C++ compatibility using the extern "C" linkage specifier. These details are fundamental in systems programming and are hallmarks of high-quality, reusable C libraries.
The Header as a Public Contract
Before diving into the technical details, it's useful to formalize the role of our header file. A .h file is more than just a list of functions; it is the public Application Programming Interface (API) for a "module." It defines the contract, telling other parts of a program what functionality is available without exposing the implementation details hidden in the corresponding .c file.
C "Modules" - Tutorial on .h Header Files, Include Guards, .o Object Code, & Incremental Compilation
This video from Kris Jordan provides a great conceptual overview of how header and source files work together to create modules in C.
Watch the segment from Header as Interface, where the presenter explains how a header file serves as the public-facing API, while the source file contains the private "black box" implementation. This distinction is central to building modular and maintainable systems.
Problem 1: Multiple Inclusions
The C preprocessor's #include directive is a blunt instrument: it literally copies and pastes the entire content of the specified file into the location of the directive. Now, consider a scenario where hnsw.h is included in hnsw.c, main.c, and perhaps another utility file, utils.c.
When the compiler processes the final "translation unit" for your executable, it will see the definitions from hnsw.hโlike typedef struct HnswIndex HnswIndex;โmultiple times. This will result in a compile-time error, typically "redeclaration of 'HnswIndex'" or "redefinition of 'struct HnswIndex'".
The solution to this is to make the header file idempotent, meaning it can be included multiple times with no negative side effects. We achieve this with include guards.
Include guards are a preprocessor trick that uses conditional compilation. The structure is simple:
- At the very top of the header, check if a unique macro is NOT defined (
#ifndef). - If it isn't, define that unique macro (
#define). - Place all the header content after the
#define. - At the very bottom of the file, close the conditional block with
#endif.
The first time the preprocessor sees the header, the macro is undefined, so it defines it and processes the file's content. On any subsequent inclusion within the same compilation unit, the macro is now defined, so the #ifndef check fails, and the preprocessor skips the entire block of content.
Libraries and Headers in C: A Tutorial
This article from Nuts & Volts provides a clear, practical explanation of include guards.
In the article, find the section titled "Guard Your Libraries." Read from the explanation of how include guards work. The example uses servo_H as the unique macro for servo.h; for our library, we will use HNSW_H.
Applying this to our header, the structure will look like this:
#ifndef HNSW_H
#define HNSW_H
// All our declarations will go here...
#endif /* HNSW_H */
The unique macro, by convention, is the uppercased filename with the period replaced by an underscore.
Problem 2: C++ Compatibility and Name Mangling
Your library is written in C, but one of the most common languages for performance-critical applications is C++. What happens when a C++ developer tries to use your library? They will #include "hnsw.h" in their .cpp file and try to call hnsw_load(). The result will almost certainly be a linker error: "undefined reference to hnsw_load".
This happens because of a C++ feature called name mangling (or name decoration). To support function overloading (multiple functions with the same name but different parameters), C++ compilers encode the function's parameter types into the symbol name they write to the object file. A C compiler does not do this.
- A C compiler sees
hnsw_load(const char*, size_t, size_t)and generates a symbol like_hnsw_load. - A C++ compiler sees the same function and might generate a symbol like
_Z9hnsw_loadPKcmm(this varies by compiler).
When the C++ linker tries to connect the call from the C++ code to the definition in your C library, it looks for the mangled name and fails to find it.

The Solution: extern "C"
To solve this, C++ provides a linkage specifier, extern "C". When you wrap a function declaration in extern "C" { ... }, you are telling the C++ compiler: "Do not perform name mangling on these functions. Use the standard C linkage and naming conventions."
A C++ developer could wrap their include of your header like this:
// C++ user's code
extern "C" {
#include "hnsw.h"
}
However, a well-designed library doesn't place this burden on its users. It should "just work." We can achieve this by adding the extern "C" block directly into our header file, but in a way that a C compiler will ignore it (since extern "C" is not valid C syntax).
We do this by checking for the __cplusplus macro, which is guaranteed to be defined by any C++ compiler.
The ISO C++ FAQ is the authoritative source for this topic. It provides the exact pattern for creating a header file that is compatible with both C and C++.
First, read the section on linker errors to reinforce your understanding of the problem. Then, read the section titled "How can I modify my own C header files...". This section provides the precise, step-by-step instructions and code for making a header C++-compatible using __cplusplus.
The Final, Professional Header File
By combining include guards and conditional extern "C" blocks, we arrive at the canonical structure for a professional, cross-compatible C header file. This is exactly what a potential employer or a fellow developer would expect to see in a serious systems project.

Our hnsw.h file should now be structured as follows. This is the complete, professional interface for our library.
#ifndef HNSW_H
#define HNSW_H
#include <stddef.h> // For size_t
#include <stdint.h> // For uint32_t
#ifdef __cplusplus
extern "C" {
#endif
/**
* @file hnsw.h
* @brief Public API for the HNSW vector database.
*
* This header defines the C-compatible public interface for creating,
* managing, and querying an HNSW index.
*/
// Opaque pointer; the user never knows the internal layout of HnswIndex.
typedef struct HnswIndex HnswIndex;
/**
* @brief Loads an HNSW index from a file or creates a new one if it doesn't exist.
* @param path Path to the database file.
* @param dim The dimensionality of the vectors.
* @return A pointer to the HnswIndex handle, or NULL on error.
*/
HnswIndex* hnsw_load(const char* path, size_t dim);
/**
* @brief Persists all changes and releases all resources associated with the index.
* @param index The HnswIndex handle to destroy.
*/
void hnsw_destroy(HnswIndex* index);
/**
* @brief Inserts a vector with a specific ID into the index.
* @param index The HnswIndex handle.
* @param vec A pointer to the vector data (must have 'dim' elements).
* @param id A unique 32-bit integer ID for this vector.
* @return 0 on success, -1 on failure.
*/
int hnsw_insert(HnswIndex* index, const float* vec, uint32_t id);
/**
* @brief Searches for the k-nearest neighbors to a query vector.
* @param index The HnswIndex handle.
* @param query_vec The vector to search for.
* @param k The number of nearest neighbors to find.
* @param out_ids A user-provided array to store the IDs of the results.
* @param out_dists A user-provided array to store the distances of the results.
* @return The number of neighbors found (can be less than k).
*/
size_t hnsw_search(HnswIndex* index, const float* query_vec, size_t k, uint32_t* out_ids, float* out_dists);
/**
* @brief Returns a string describing the last error that occurred in the calling thread.
* @return A constant string describing the last error, or NULL if no error occurred.
* The string is valid until the next API call in the same thread.
*/
const char* hnsw_get_last_error();
#ifdef __cplusplus
} // extern "C"
#endif
#endif /* HNSW_H */
Notice the addition of Doxygen-style comments. While not strictly required by the compiler, documenting your public API at the header level is another crucial practice for creating usable and maintainable libraries.
Conclusion
In this lesson, you've elevated your hnsw.h header from a simple set of declarations to a robust and professional API contract. You now understand not just what to write, but why you're writing itโfrom the preprocessor mechanics of include guards to the linker-level details of C++ name mangling. This deep understanding of the build process is a key differentiator in low-level systems engineering.
Key Takeaways:
- Include Guards (
#ifndef/#define/#endif): Prevent re-declaration errors by making header file inclusion an idempotent operation. - Name Mangling: C++ compilers alter function names to support features like overloading, which breaks linking with C code.
extern "C": A C++ linkage specifier that instructs the compiler not to mangle names, ensuring C-compatible linkage.__cplusplusMacro: Allows you to create a single header that works seamlessly for both C and C++ compilers by conditionally including theextern "C"block.
With a complete implementation and a professional header file, your library is nearly ready for distribution. In the next lesson, you will use CMake to compile the project into a dynamically linked shared library (.so on Linux), the final step in packaging your code for use by other programs.