This blog post is part two of a tutorial based on the OpenGLAda project and will cover implementation details such as a type system for interfacing with C, error handling, memory management, and loading functions.
If you haven't read part one I encourage you to do so. It can be found here.
As part of the binding process we noted in the previous blog post that we will need to translate typedefs within the OpenGL C headers into Ada types so that our description of C functions that take arguments or return a value are accurate. Let’s begin with the basic numeric types:
with Interfaces.C; package GL.Types is type Int is new Interfaces.C.int; -- GLint type UInt is new Interfaces.C.unsigned; -- GLuint subtype Size is Int range 0 .. Int'Last; -- GLsizei type Single is new Interfaces.C.C_float; -- GLfloat type Double is new Interfaces.C.double; -- GLdouble end GL.Types;
We use Single as a name for the single-precision floating point type to avoid confusion with Ada's Standard.Float. Moreover, we can apply Ada’s powerful numerical typing system in our definition of GLsize by defining it with a non-negative range. This affords us some extra compile-time and run-time checks without having to add any conditionals – something not possible in C.
The type list above is, of course, shortened for this post, however, two important types are explicitly declared elsewhere:
- GLenum, which is used for parameters that take a well-defined set of values specified within the #define directive in the OpenGL header. Since we want to make the Ada interface safe we will use real enumeration types for that.
- GLboolean, which is an unsigned char representing a boolean value. We do not want to have a custom boolean type in the Ada API because it will not add any value compared to using Ada's Boolean type (unlike e.g. the Int type, which may have a different range than Ada's Integer type).
For these types, we define another package called GL.Low_Level:
with Interfaces.C; package GL.Low_Level is type Bool is new Boolean; subtype Enum is Interfaces.C.unsigned; private for Bool use (False => 0, True => 1); for Bool'Size use Interfaces.C.unsigned_char'Size; end GL.Low_Level;
We now have a Bool type that we can use for API imports and an Enum type that we will solely use to define the size of our enumeration types. Note that Bool also is an enumeration type, but uses the size of unsigned_char because that is what OpenGL defines for GLboolean.
To show how we can wrap GLenum into actual Ada enumeration types, lets examine glGetError which is defined like this in the C header:
The return value is one of several error codes defined as preprocessor macros in the header. We translate these into an Ada enumeration then wrap the subprogram resulting in the following:
package GL.Errors is type Error_Code is (No_Error, Invalid_Enum, Invalid_Value, Invalid_Operation, Stack_Overflow, Stack_Underflow, Out_Of_Memory, Invalid_Framebuffer_Operation); function Error_Flag return Error_Code; private for Error_Code use (No_Error => 0, Invalid_Enum => 16#0500#, Invalid_Value => 16#0501#, Invalid_Operation => 16#0502#, Stack_Overflow => 16#0503#, Stack_Underflow => 16#0504#, Out_Of_Memory => 16#0505#, Invalid_Framebuffer_Operation => 16#0506#); for Error_Code'Size use Low_Level.Enum'Size; end GL.Errors;
With the above code we encode the errors defined in the C header as representations for our enumeration values - this way, our safe enumeration type has the exact same memory layout as the defined error codes and maintains compatibility.
We then add the backend for Error_Flag as import to GL.API:
function Get_Error return Errors.Error_Code; pragma Import (StdCall, Get_Error, "glGetError");
The OpenGL specification states that whenever an error arises while calling a function of the API, an internal error flag gets set. This flag can then be retrieved with the function glGetError we wrapped above.
It would certainly be nicer, though, if these API calls would raise Ada exceptions instead, but this would mean that in every wrapper to an OpenGL function that may set the error flag we'd need to call Get_Error, and, when the returned flag is something other than No_Error, we'd subsequently need to raise the appropriate exception. Depending on what the user does with the API, this may lead to significant overhead (let us not forget that OpenGL is much more performance-critical than it is safety-critical). In fact, more recent graphics API’s like Vulkan have debugging extensions which require manual tuning to receive error messages - in other words, due to overhead, Vulkan turns off all error checking by default.
So, what we will provide is a feature that auto-raises exceptions whenever the error flag is set, but make it optional. To achieve this, Ada exceptions derived from OpenGL’s error flags need to be defined.
Let’s add the following exception definitions to GL.Errors:
Invalid_Operation_Error : exception; Out_Of_Memory_Error : exception; Invalid_Value_Error : exception; Stack_Overflow_Error : exception; Stack_Underflow_Error : exception; Invalid_Framebuffer_Operation_Error : exception; Internal_Error : exception;
Notice that the exceptions carry the same names as the corresponding enumeration values in the same package. This is not a problem because Ada is intelligent enough to know which one of the two we want depending on context. Also notice the exception Internal_Error which does not correspond to any OpenGL error – we'll see later what we need it for.
Next, we need a procedure that queries the error flag and possibly raises the appropriate exception. Since we will be using such a procedure almost everywhere in our wrapper let’s declare it in the private part of the GL package so that all of GL's child packages have access:
And in the body:
procedure Raise_Exception_On_OpenGL_Error is separate;
Here, we tell Ada that this procedure is defined in a separate compilation unit enabling us to provide different implementations depending on whether the user wants automatic exception raising to be enabled or not. Before we continue though let’s set up our project with this in mind:
library project OpenGL is -- Windowing_System config omitted type Toggle_Type is ("enabled", "disabled"); Auto_Exceptions : Toggle_Type := external ("Auto_Exceptions", "enabled"); OpenGL_Sources := ("src"); case Auto_Exceptions is when "enabled" => OpenGL_Sources := OpenGL_Sources & "src/auto_exceptions"; when "disabled" => OpenGL_Sources := OpenGL_Sources & "src/no_auto_exceptions"; end case; for Source_Dirs use OpenGL_Sources; -- packages and other things omitted end OpenGL;
To conform with the modifications made to the project file we must now create two new directories inside the src folder and place the implementations of our procedure accordingly. GNAT expects the source files to both be named gl-raise_exception_on_openl_error.adb. The implementation of no_auto_exceptions is trivial:
separate (GL) procedure Raise_Exception_On_OpenGL_Error is begin null; end Raise_Exception_On_OpenGL_Error;
And the one in auto_exceptions looks like this:
with GL.Errors; separate (GL) procedure Raise_Exception_On_OpenGL_Error is begin case Errors.Error_Flag is when Errors.Invalid_Operation => raise Errors.Invalid_Operation_Error; when Errors.Invalid_Value => raise Errors.Invalid_Value_Error; when Errors.Invalid_Framebuffer_Operation => raise Errors.Invalid_Framebuffer_Operation_Error; when Errors.Out_Of_Memory => raise Errors.Out_Of_Memory_Error; when Errors.Stack_Overflow => raise Errors.Stack_Overflow_Error; when Errors.Stack_Underflow => raise Errors.Stack_Underflow_Error; when Errors.Invalid_Enum => raise Errors.Internal_Error; when Errors.No_Error => null; end case; exception when Constraint_Error => raise Errors.Internal_Error; end Raise_Exception_On_OpenGL_Error;
The exception section at the end is used to detect cases where glGetError returns a value we did not know of at the time of implementing this wrapper. Ada would then try to map this value to the Error_Code enumeration, and since the value does not correspond to any value specified in the type definition, the program will raise a Constraint_Error. Of course, OpenGL is very conservative about adding error flags, so this is unlikely to happen, but it is still nice to plan for the future.
Types Fetching Function Pointers at Runtime
Part 1: Implementing the "Fetching" Function
As previously noted, many functions from the OpenGL API must be retrieved as a function pointer at run-time instead of linking to them at compile-time. The reason for this once again comes down to the concept of graceful degradation -- if some functionality exists as an extension (especially functions not part of the OpenGL core) but is unimplemented by a target graphics card driver then the programmer will be able to identify or recognize this case when setting the relevant function pointers during execution. Unfortunately though, this creates an extra step which prevents us from simply importing the whole of the API, and, worse still, on Windows no functions being defined on OpenGL 2.0 or later are available for compile time linking, making programmatic queries required.
So then, the question arises: how will these function pointers are to be retrieved? Sadly, this functionality is not available from within the OpenGL API or driver, but instead is provided by platform-specific extensions, or more specifically, the windowing system supporting OpenGL. So, as with exception handling, we will use a procedure with multiple implementations and switch to the appropriate implementation via GPRBuild:
case Windowing_System is when "windows" => OpenGL_Sources := OpenGL_Sources & "src/windows"; when "x11" => OpenGL_Sources := OpenGL_Sources & "src/x11"; when "quartz" => OpenGL_Sources := OpenGL_Sources & "src/mac"; end case;
...and we declare this function in the main source:
function GL.API.Subprogram_Reference (Function_Name : String) return System.Address;
Then finally, in the windowing-system specific folders, we place the implementation and necessary imports from the windowing system's API. Those imports and the subsequent implementations are not very interesting, so I will not discuss them at length here, but I will show you the implementation for Apple's Mac operating system to give you an idea:
with GL.API.Mac_OS_X; function GL.API.Subprogram_Reference (Function_Name : String) return System.Address is -- OSX-specific implementation uses CoreFoundation functions use GL.API.Mac_OS_X; package IFC renames Interfaces.C.Strings; GL_Function_Name_C : IFC.chars_ptr := IFC.New_String (Function_Name); Symbol_Name : constant CFStringRef := CFStringCreateWithCString (alloc => System.Null_Address, cStr => GL_Function_Name_C, encoding => kCFStringEncodingASCII); Result : constant System.Address := CFBundleGetFunctionPointerForName (bundle => OpenGLFramework, functionName => Symbol_Name); begin CFRelease (Symbol_Name); IFC.Free (GL_Function_Name_C); return Result; end GL.API.Subprogram_Reference;
With the above code in effect, we are now able to retrieve the function pointers, however, we still need to implement the querying machinery to which there are three possible approaches:
- Lazy: When a feature is first needed, its corresponding function pointer is loaded and stored for future use. This approach to loading may produce the least amount of work needed to be done by the resulting application, although, theoretically, it makes performance of a call unpredictable. Since fetching function pointers is fairly trivial operation, however, this is not really a necessarily practical reason against this.
- Eager: At some defined point in time, a call gets issued to a loading function for every function pointer that is supported by OpenGLAda. The Eager approach produces the largest amount of work for the resulting application, but again, since loading is trivial it does not noticeably slow down the application (and, even if it did, it would so during initialization where it is most tolerable).
- Explicit: The user is required to specify which features they want to use and we only load the function pointers related to such features. Explicit loading places the heaviest burden on the user, since they must state which features they will be using.
Overall, the consequences of choosing one of these three possibilities are mild, so we will go with the one easiest to implement, which is the eager approach and is the same one used by many other popular OpenGL libraries.
Part 2: Autogenerating the Fetching Implementation
For each OpenGL function we import that must be loaded at runtime we need to create three things:
- The definition of an access type describing the function's parameters and return types.
- A global variable having this type to hold the function pointer as soon as it gets loaded.
- A call to a platform-specific function which will return the appropriate function pointer from a DLL or library for storage into our global function pointer.
Implementing these segments for each subprogram is a very repetitive task, which hints to the possibility of automating it. To check whether this is feasible, let’s go over the actual information we need to write in each of these code segments for an imported OpenGL function:
- The Ada subprogram signature
- The name of the C function we import
As you can see, this is almost exactly the same information we would need to write an imported subprogram loaded at compile time! To keep all information about imported OpenGL function centralized, let’s craft a simple specification format where we may list all this information for each subprogram.
Since we need to define Ada subprogram signatures, it seems a good idea to use Ada-like syntax (like GPRBuild does for its project files). After writing a small parser (I will not show details here since that is outside the scope of this post), we can now process a specification file looking like the following. We will discuss the package GL.Objects.Shaders and more about what it does in a bit.
with GL.Errors; with GL.Types; with GL.Objects.Shaders; spec GL.API is use GL.Types; function Get_Error return Errors.Error_Code with Implicit => "glGetError"; procedure Flush with Implicit => "glFlush"; function Create_Shader (Shader_Type : Objects.Shaders.Shader_Type) return UInt with Explicit => "glCreateShader"; end GL.API;
This specification contains two imports we have already created manually and one new import – in this case we use Create_Shader as an example for a subprogram that needs to be loaded via function pointer. We use Ada 2012-like syntax for specifying the target link name with aspects and the import mode. There are two import modes:
- Implicit - meaning that the subprogram will be imported via pragmas. This will give us a subprogram declaration that will be bound to its implementation by the dynamic library loader. So it happens implicitly and we do not actually need to write any code for it. This is what we previously did in our import of glFlush in part one.
- Explicit - meaning that the subprogram will be provided as a function pointer variable. We will need to generate code that assigns a proper value to that variable at runtime in this case.
Processing this specification will generate us the following Ada subunits:
with GL.Errors; with GL.Types; private package GL.API is use GL.Types; type T1 is access function (P1 : Objects.Shaders.Shader_Type) return UInt; pragma Convention (StdCall, T1); function Get_Error return Errors.Error_Code; pragma Import (StdCall, Get_Error, "glGetError"); procedure Flush; pragma Import (StdCall, Flush, "glFlush"); Create_Shader : T1; end package GL.API; -- --------------- with System; with Ada.Unchecked_Conversion; private with GL.API.Subprogram_Reference; procedure GL.Load_Function_Pointers is use GL.API; generic type Function_Reference is private; function Load (Function_Name : String) return Function_Reference; function Load (Function_Name : String) return Function_Reference is function As_Function_Reference is new Ada.Unchecked_Conversion (Source => System.Address, Target => Function_Reference); Raw : System.Address := Subprogram_Reference (Function_Name); begin return As_Function_Reference (Raw); end Load; function Load_T1 is new Load (T1); begin GL.API.Create_Shader := Load_T1 ("glCreateShader"); end GL.Load_Function_Pointers;
Notice how our implicit subprograms get imported like before, but for the explicit subprogram, a type T1 got created as an access type to the subprogram, and a global variable Create_Shader is defined to be of this type - satisfying all of our needs.
The procedure GL.Load_Function_Pointers contains the code to fill this variable with the right value by obtaining a function pointer using the platform-specific implementation discussed above. The generic load function exists so that additional function pointers can be loaded using this same code.
The only thing left to do is to expose this functionality in the public interface like the example below:
package GL is -- ... other code procedure Init; -- ... other code end GL; -- ------ with GL.Load_Function_Pointers; package body GL is -- ... other code procedure Init renames GL.Load_Function_Pointers; -- ... other code end GL;
Of course, we now require the user to explicitly call Init somewhere in their code... You might think that we could automatically execute the loading code at package initialization, but this would not work, because some OpenGL implementations (most prominently the one on Windows) will refuse to load any OpenGL function pointers unless there is a current OpenGL context. This context will only exist after we created an OpenGL surface to render on, which will be done programmatically by the user.
In practice, OpenGLAda includes a binding to the GLFW library as a platform-independent way of creating windows with an OpenGL surface on them, and this binding automatically calls Init whenever a window is made current (i.e. placed in foreground), so that the user does not actually need to worry about it. However, there may be other use-cases that do not employ GLFW, like, for example, creating an OpenGL surface widget with GtkAda. In that case, calling Init manually is still required given our design.
The OpenGL API enables us to create various objects that reside in GPU memory for things like textures or vertex buffers. Creating such objects gives us an ID (kind of like a memory address) which we can then use to refer to the object instead of a memory address. To avoid memory leaks, we will want to manage these IDs automatically in our Ada wrapper so they are automatically destroyed once the last reference vanishes. Ada’s Controlled types are an ideal candidate for the job. Let's start writing a package GL.Objects to encapsulate the functionality:
package GL.Objects is use GL.Types; type GL_Object is abstract tagged private; procedure Initialize_Id (Object : in out GL_Object); procedure Clear (Object : in out GL_Object); function Initialized (Object : GL_Object) return Boolean; procedure Internal_Create_Id (Object : GL_Object; Id : out UInt) is abstract; procedure Internal_Release_Id (Object : GL_Object; Id : UInt) is abstract; private type GL_Object_Reference; type GL_Object_Reference_Access is access all GL_Object_Reference; type GL_Object_Reference is record GL_Id : UInt; Reference_Count : Natural; Is_Owner : Boolean; end record; type GL_Object is abstract new Ada.Finalization.Controlled with record Reference : GL_Object_Reference_Access := null; end record; -- Increases reference count. overriding procedure Adjust (Object : in out GL_Object); -- Decreases reference count. Destroys texture when it reaches zero. overriding procedure Finalize (Object : in out GL_Object); end GL.Objects;
GL_Object is our smart pointer here, and GL_Object_Reference is the holder of the object's ID as well as the reference count. We will derive the actual object types (which there are quite a few) from GL_Object so that the base type can be abstract and we can define some subprograms that must be overridden by the child types to enforce the rule. Note that since the class hierarchy is based on GL_Object, all derived types have an identically-typed handle to a GL_Object_Reference object, and thus, our reference-counting is independent of the actual derived type.
The only thing the derived type must declare in order for our automatic memory management to work is how to create and delete the OpenGL object in GPU memory – this is what Internal_Create_Id and Internal_Release_Id in the above segment are for. Because they are abstract, they must be put into the public part of the package even though they should never be called by the user directly.
The core of our smart pointer machinery will be implemented in the Adjust and Finalize procedures provided by Ada.Finalization.Controlled. Since this topic has already been extensively covered in this Ada Gem I am going to skip over the gory implementation details.
So, to create a new OpenGL object the user must call Initialize_Id on a smart pointer which assigns the ID of the newly created object to the smart pointer's backing object. Clear can then later be used to make the smart pointer uninitialized again (but only delete the object if the reference count reaches zero).
To test our system, let's implement a Shader object. Shader objects will hold source code and compiled binaries of GLSL (GL Shading Language) shaders. We will call this package GL.Objects.Shaders in keeping with the rest of the project's structure:
package GL.Objects.Shaders is pragma Preelaborate; type Shader_Type is (Fragment_Shader, Vertex_Shader, Geometry_Shader, Tess_Evaluation_Shader, Tess_Control_Shader); type Shader (Kind : Shader_Type) is new GL_Object with private; procedure Set_Source (Subject : Shader; Source : String); procedure Compile (Subject : Shader); procedure Release_Shader_Compiler; function Compile_Status (Subject : Shader) return Boolean; function Info_Log (Subject : Shader) return String; private type Shader (Kind : Shader_Type) is new GL_Object with null record; overriding procedure Internal_Create_Id (Object : Shader; Id : out UInt); overriding procedure Internal_Release_Id (Object : Shader; Id : UInt); for Shader_Type use (Fragment_Shader => 16#8B30#, Vertex_Shader => 16#8B31#, Geometry_Shader => 16#8DD9#, Tess_Evaluation_Shader => 16#8E87#, Tess_Control_Shader => 16#8E88#); for Shader_Type'Size use Low_Level.Enum'Size; end GL.Objects.Shaders;
The two overriding procedures are implemented like this:
overriding procedure Internal_Create_Id (Object : Shader; Id : out UInt) is begin Id := API.Create_Shader (Object.Kind); Raise_Exception_On_OpenGL_Error; end Internal_Create_Id; overriding procedure Internal_Release_Id (Object : Shader; Id : UInt) is pragma Unreferenced (Object); begin API.Delete_Shader (Id); Raise_Exception_On_OpenGL_Error; end Internal_Release_Id;
Of course, we need to add the subprogram Delete_Shader to our import specification so it will be available in the generated GL.API package. A nice thing is that, in Ada, pointer dereference is often done implicitly so we need not worry whether Create_Shader and Delete_Shader are loaded via function pointers or with the dynamic library loader – the code would look exactly the same in both cases!
One problem we did not yet address is documentation. After all, because we are adding structure and complexity to the OpenGL API, which does not exist in its specification, how is a user supposed to find the wrapper of a certain OpenGL function they want to use?
What we need to do, then, is generate a list where the name of each OpenGL function we wrap is listed and linked to its respective wrapper function in OpenGLAda's API. Of course, we do not want to generate that list manually. Instead, let’s use our import specification again and enrich it with additional information:
function Get_Error return Errors.Error_Code with Implicit => "glGetError", Wrapper => "GL.Errors.Error_Flag"; procedure Flush with Implicit => "glFlush", Wrapper => "GL.Flush";
With the new "aspect-like" declarations in our template we can enhance our generator with code that writes a Markdown file listing all imported OpenGL functions and linking that to their wrappers. In theory, we could even avoid adding the wrapper information explicitly by analyzing OpenGLAda's code to detect which subprogram wraps the OpenGL function. Tools like ASIS and LibAdaLang would help us with that, but that implementation would be far more work than adding our wrapper references explicitly.
The generated list can be seen on OpenGLAda's website showing all the functions that are actually supported. It is intended to be navigated via search (a.k.a. Ctrl+F).
By breaking down the complexities of a large C API like OpenGL, we have gone through quite a few improvements that can be done when creating an Ada binding. Some of them were not so obvious and probably not necessary for classifying a binding as thick - for example, auto-loading our function pointers at run-time was simply an artifact of supporting OpenGL and not covered inside the scope of the OpenGL API itself.
We also discovered that when wrapping a C API in Ada we must lift the interface to a higher level since Ada is indeed designed to be a higher-level language than C, and, in this vein, it was natural to add features that are not part of the original API to make it fit more at home in an Ada context.
It might be tempting to write a thin wrapper for your Ada project to avoid overhead, but beware - you will probably still end up writing a thick wrapper. After all, the code around calls that facilitates thinly wrapped functions and the need for data conversions does not simply vanish!
Of course, all this is a lot of work! To give you some numbers: The OpenGLAda repository contains 15,874 lines of Ada code (excluding blanks and comments, tests, and examples) while, for comparison, the C header gl.h (while missing many key features) is only around 3,000 lines.