When linking normal C libraries, your code declares the signature of the functions and variables you expect to dynamically link (usually by #including a header file), and the operating system's dynamic linker links those objects into your program at runtime. OpenGL is rather different. In the year 2000, SGI created the OpenGL ABI for Linux, which included OpenGL 1.2, GLU 1.3, and GLX 1.3 symbols. Therefore, you can #include the relevant headers, dynamically link to the relevant libraries, and expect to get linked to those symbols. For any symbols exported by later versions of GL and GLX, you must query and retrieve them manually at runtime. Note that you can also use this type of mechanism to manually link symbols from other libraries, rather than creating a dynamic executable.
We already had an example of this in the "Specifying your OpenGL context version" tutorial with glXCreateContextAttribsARB.
Build with:
gcc main.c -lX11 -lGL
const char *gl_extensions = (const char*)glGetString (GL_EXTENSIONS);
const char *glx_extensions = glXQueryExtensionsString (display,0);
OpenGL provides us a list of all available extensions in the form of a space-separated string. GLX does the same, and these two function calls are how we get them. Feel free to read through some of the GL/GLX extensions your context reports. This works in all OpenGL context versions, but if your context is 3.0 or later, you can also retrieve the number of extensions with glGetIntegerv (GL_NUM_EXTENSIONS, &extension_count) then iterate each extension as an individual string with glGetStringi (GL_EXTENSIONS, i).
bool GLHasExtension (const char *const haystack, const char *const needle) {
const char *e = haystack;
const auto len = strlen (needle);
for (;;) {
e = strstr (e, needle);
if (e == NULL) return false;
char next = *(e + len);
if (next == ' ' || next == '\0') return true;
e += len;
}
}
This function searches for the given extension in an extension string. It's important to check that the found extension is followed by a space or NULL, because some extensions have the same name as another extension, with additional text added on, eg. GLX_EXT_swap_control and GLX_EXT_swap_control_tear. On all of my systems, every extension in the string, including the last, is followed by a space, so I could just check for the extension followed by a space to eliminate the need to check the next character. But according to this nVidia document, page 6, the last extension may not have a space and may just be NULL-terminated.
while (glversion.full >= (glversion_t){.major = 2, .minor = 0}.full && glversion_compatibility) {
typedef GLuint (*glCreateShader_t) (GLenum shaderType);
const auto glCreateShader = (glCreateShader_t)glXGetProcAddress ((const GLubyte*)"glCreateShader");
if (glCreateShader == NULL) {
puts ("Failed to retrieve glCreateShader function pointer");
break;
}
const auto vertex_shader = glCreateShader (GL_VERTEX_SHADER);
printf ("glCreateShader found and called: %u\n", vertex_shader);
break;}
glCreateShader was introduced in OpenGL 2.0, so we check the context version against that. We create a type for the function pointer. The function signature can be copied from the OpenGL specification. Next, we use glXGetProcAddress to request the symbol by name, casting the pointer to the correct type. If the symbol exists, we'll have its address. If not, glxGetProcAddress returns NULL. After retrieving the function pointer and verifying we got it, we call it just to demonstrate that it works.
When retrieving a function that is part of a specific OpenGL version specification, you can just check that your context is a compatible version and then retrieve the function pointer. Given this, if you know all of your GL functions are from version 2.1 and earlier, for example, you can do a single check for the version, then retrieve all the functions pointers.
if (GLHasExtension (gl_extensions, "GL_ARB_vertex_buffer_object")) {
do {
typedef void (*glGenBuffersARB_t) (GLsizei n, GLuint *buffers);
const auto glGenBuffersARB = (glGenBuffersARB_t)glXGetProcAddress ((const GLubyte*)"glGenBuffersARB");
if (glGenBuffersARB == NULL) {
puts ("Failed to retrieve glGenBuffersARB function pointer");
break;
}
GLuint buffer = 0;
glGenBuffersARB (1, &buffer);
printf ("glGenBuffersARB found and called: %d\n", buffer);
} while (false);
}
Here we check for the extension, GL_ARB_vertex_buffer_object, then get the function pointer for glGenBuffersARB. Pretty much the same as for glCreateShader, except it's an extension function, not an OpenGL core specification function.
if (GLHasExtension (glx_extensions, "GLX_EXT_swap_control_tear")) {
do {
typedef void (*glXSwapIntervalEXT_t)(Display *dpy, GLXDrawable drawable, int interval);
const auto glXSwapIntervalEXT = (glXSwapIntervalEXT_t)glXGetProcAddress((const GLubyte*)"glXSwapIntervalEXT");
if (glXSwapIntervalEXT == NULL) {
puts ("Failed to retrieve glXSwapIntervalEXT function pointer");
break;
}
glXSwapIntervalEXT (display, xwindow, -1);
puts ("glXSwapIntervalEXT found and called");
} while (false);
}
Note that since we're checking for a GLX extension, we pass the glx_extensions string to GLHasExtension. Other than that, this is practically the same as the prior code.
You may wonder: why check for the version/extension at all? Why not just attempt to retrieve the function pointer without checking those things? In many cases that will work, but glXSwapIntervalEXT is a good example of why it is sometimes necessary to check for the relevant extension first. You see, glXSwapIntervalEXT is included in two different extensions: GLX_EXT_swap_control, and GLX_EXT_swap_control_tear. If the _tear extension is present, you can call glXSwapIntervalEXT with a -1 interval value in order to allow tearing when the framerate dips below the vertical sync rate. But when only the non-tear extension is present, the function fails when called with a negative value.
The above code works for retrieving any GL and GLX functions not included in the ABI. In my game engine, I use fewer than 20 such functions, so I just created a simple macro and called it once for each function. If you're using more functions than that, or want to retrieve everything up to a particular version of OpenGL, you might use a library such as GLEW or Glad, or you could download the OpenGL API registry XML and GLX API registry XML, and write a program to parse them and generate your own code for loading extensions. This is what I plan to do whenever I use more OpenGL in my next engine.