Overview

When working with large graphics APIs like OpenGL ES and EGL, especially in complex systems like Android, developers face the challenge of managing hundreds of function declarations, pointers, and related metadata. A common and powerful solution is to use macro-based code generation with .in files. This approach, while adding some complexity, brings significant benefits in maintainability, consistency, and flexibility.

This post explains what .in files are, how they’re used with macros, and why this pattern is so valuable in graphics stacks like OpenGL ES and EGL. We’ll also look at concrete examples and discuss the trade-offs involved.


What is a .in File?

A .in file is a simple text file that lists API functions in a structured, macro-invocation format. For example, an OpenGL ES .in file might look like this:

GL_ENTRY(void, glClear, GLbitfield mask)
GL_ENTRY(void, glDrawArrays, GLenum mode, GLint first, GLsizei count)

And for EGL:

EGL_ENTRY(EGLBoolean, eglInitialize, EGLDisplay dpy, EGLint *major, EGLint *minor)
EGL_ENTRY(EGLBoolean, eglTerminate, EGLDisplay dpy)

Each line represents a function, its return type, name, and parameters.


How Are Macros Used With .in Files?

The real magic happens when you include the .in file in your C/C++ code with different macro definitions. For example:

#define GL_ENTRY(_r, _api, ...) #_api,
char const * const gl_names[] = {
    #include "../entries.in"
    nullptr
};

This expands to:

const char* gl_names[] = {
    "glClear",
    "glDrawArrays",
    nullptr
};

By redefining the macro, you can generate function declarations, function pointer tables, wrappers, or string tables, all from the same .in file.

Code Reference: <AOSP>/frameworks/native/opengl/libs/EGL/egl.cpp#200


Example: Generating Function Pointer Tables

Suppose you want to create a table of function pointers for dynamic loading:

#define GL_ENTRY(_r, _api, ...) _r (*_api)(__VA_ARGS__);
struct GLFunctionTable {
#include "gl_entries.in"
};

This expands to:

struct GLFunctionTable {
    void (*glClear)(GLbitfield mask);
    void (*glDrawArrays)(GLenum mode, GLint first, GLsizei count);
};

You can then populate this table at runtime using dlsym or a similar mechanism.


Why Use This Pattern?

Single Source of Truth

All API functions are listed in one place. If you add, remove, or change a function, you only need to update the .in file.

Consistency and Maintainability

The preprocessor ensures that all generated code (declarations, tables, wrappers) is always in sync with the .in file, reducing the risk of errors.

Flexible Code Generation

You can easily generate different code structures (declarations, tables, wrappers, string arrays) by redefining the macro before including the .in file.

Reduces Human Error

Less manual copy-pasting means fewer typos and mismatches.


Trade-offs and Drawbacks

ProsCons
Consistency across codebaseHarder to read for beginners
Easier to update/extend APIsMore complex build/preprocessor logic
Reduces duplication and errorsCan be confusing if misused
Enables advanced features (tracing, etc.)Debugging macro expansions is harder
  • For small projects, this pattern may be overkill.
  • For large, evolving APIs (like OpenGL ES/EGL), it’s a best practice.

Real-World Usage

This pattern is widely used in Android’s graphics stack and other large projects. For example, Android’s EGL loader uses .in files and macros to generate function pointer tables and wrappers for tracing and debugging. (Reference: egl_example on GitHub)


Conclusion

Macro-based code generation with .in files is a powerful technique for managing large, complex APIs like OpenGL ES and EGL. It centralizes API definitions, ensures consistency, and enables flexible code generation, at the cost of some added complexity. For large-scale or evolving projects, the benefits far outweigh the drawbacks.


Further Reading