flyx.org

Personal homepage of Felix Krause

Writing Ada Bindings for C Libraries, Part 3

Previously...

Void Pointers

C has no generics. So whenever a subprogram parameter may take differently typed values, a void pointer is used. Usually, a void pointer value will be used in one of these ways:

  • It will be passed on to another subprogram that will know its type, cast it appropriately and do stuff with it.
  • It will be used to return data to the caller, and he has to know what to do with it.

Here's an example for the second case:

void * clGetExtensionFunctionAddress(const char * func_name);

Here, a void pointer is returned to the caller. The purpose of this function is to return a pointer to a subprogram specified with func_name. So there is a fixed set of accepted values for func_name, and for every value, the function may return a differently typed pointer to a subprogram.

There are several possibilities to wrap C functions taking void pointers in Ada:

Import it multiple times with different signatures

package C renames Interfaces.C;
type Func_Type1 is access function return C.int;
pragma Convention (C, Func_Type1);
type Func_Type2 is access function (Param : C.int) return C.double;
pragma Convention (C, Func_Type2);
function Get_Extension_Function_Address
  (Func_Name : C.Strings.chars_ptr) return Func_Type1;
function Get_Extension_Function_Address
  (Func_Name : C.Strings.chars_ptr) return Func_Type2;
pragma Import (Convention => C, Entity => Get_Extension_Function_Address,
               External_Name => "clGetExtensionFunctionAddress");

The Import pragma will be applied to all functions that match the given name. While this works, it does not give us type safety: If the user calls the wrong function, he gets a function reference back that will not work as expected.

Wrap the C function

function Backend (Func_Name : C.char_array) return System.Address;
pragma Import (Convention => C, Entity => Backend,
               External_Name => "clGetExtensionFunctionAddress");

generic
   type Return_Type is private;
   Function_Name : String;
function Get_Extension_Function_Address return Return_Type is
   function Convert is new Ada.Unchecked_Conversion (System.Address, Return_Type);
begin
   return Convert (Backend (C.To_C (Function_Name)));
end Get_Extension_Function_Address;
function Get_Func1 is new Get_Extension_Function_Address
  (Func_Type1, "func1");
function Get_Func2 is new Get_Extension_Function_Address
  (Func_Type2, "func2");

Obviously, you want to expose just the last two functions to the caller. As you cannot implement a declaration made in a package specification by a generic instantiation, you have to use renames to do that:

function Get_Func1_Public return Func_Type1 renames Get_Func1;

Provide a generic interface

... so the caller can define the type he wants to use. This is useful in cases like this:

void registerCallback(void(*callback)(void* user_data), void* user_data);

Here, the C procedure lets the caller register a callback that, when called, will be passed a pointer to some data the caller provides. This is a pattern that is often used with callbacks in C. You can wrap it like this:

procedure Backend (Callback_Raw, User_Data : System.Address);
pragma Import (Convention => C, Entity => Backend,
               External_Name => "registerCallback");
generic
   type User_Data_Type is private;
   type User_Data_Access is access User_Data_Type;
   type Callback is access procedure (User_Data : User_Data_Type);
procedure Register_Callback (Target : Callback; User_Data : User_Data_Access) is
   function Convert_User_Data is new Ada.Unchecked_Conversion
     (User_Data_Access, System.Address);
   function Convert_Callback is new Ada.Unchecked_Conversion
     (Callback, System.Address);
begin
   Backend (Convert_Callback (Target), Convert_User_Data (User_Data));
end Register_Callback;

You may want to convert this code to a generic package that can define the types User_Data_Access and Callback itself based on the parameter User_Data_Type, particularly if there are multiple similar callback registering functions.

Be aware that this wrapper leaves it to the caller to make sure his callback function has the correct convention (one can also use the pragma Convention on subprograms that are implemented in Ada if they will be called by C code).

If you want to make your wrapper even thicker, you can define your own User_Data_Type and callback function, and embed the reference to the caller's function as well as the caller's data in your User_Data_Type. Your callback function can then extract the subprogram reference and user data from your container and call the callback the caller provided. This way, the caller does not need to apply any pragmas in his code.

Conclusion

If you want to wrap a void pointer, you usually declare it as System.Address and use Ada.Unchecked_Conversion in your wrapper. The lesser the caller needs to take care about Convention pragmas, the easier your wrapper is to use.

Bitfields

Bitfields are usually declared as numeric type like int in C. Then, a number of constants is defined that can be combined with bitwise OR to build a value of the bitfield. Example:

typedef cl_ulong            cl_bitfield;
typedef cl_bitfield         cl_device_type;

/* cl_device_type - bitfield */
#define CL_DEVICE_TYPE_DEFAULT                      (1 << 0)
#define CL_DEVICE_TYPE_CPU                          (1 << 1)
#define CL_DEVICE_TYPE_GPU                          (1 << 2)
#define CL_DEVICE_TYPE_ACCELERATOR                  (1 << 3)

cl_int clGetDeviceIDs(cl_platform_id   /* platform */,
                      cl_device_type   /* device_type */, 
                      cl_uint          /* num_entries */, 
                      cl_device_id *   /* devices */, 
                      cl_uint *        /* num_devices */);

Of course, you could just copy the constants to Ada and provide the same interface. But you can also wrap it with a record:

type Device_Type is record
   Default     : Boolean := False;
   CPU         : Boolean := False;
   GPU         : Boolean := False;
   Accelerator : Boolean := False;
end record;
for Device_Type use record
  Default     at 0 range 0 .. 0;
  CPU         at 0 range 1 .. 1;
  GPU         at 0 range 2 .. 2;
  Accelerator at 0 range 3 .. 3;
end record;
for Device_Type'Size use ULong'Size;
pragma Convention (C_Pass_By_Copy, Device_Type);

This way, the possible values are directly linked to the type. If you just provide constants and a numeric type, there is no explicit link between them.

Tags: ada programming

Writing applications in Ada with ada-bundler

Cross-platform development of desktop applications is a tedious business. You can compile simple Ada code without modifications on any platform that's supported by your compiler, but as soon as you start developing desktop applications (usually with a GUI), you need to implement quite some platform-specific behaviour. In this post, I present ada-bundler, a low-level library and tool for cross-platform file access that aims to make your life easier a bit.

The Problem

If you look at the platforms and languages used commonly for application development today, you'll see that there are lots of build systems that take care of bundling resource files with your application - be it pictures, configuration files or other data your application needs. IDEs just let you add resource files to your project and will bundle them with the compiled binary when you build the application. GPRBuild, the build system commonly used for Ada applications, does not support handling resource files at all - it focuses on compilation of sources.

Bundling files with your application is platform-specific: Native Windows apps may embed them as resources right in the executable file. OSX applications come as so-called "app bundle", which is basically a folder containing all the application files, which the user can double-click to start the application. Linux puts resources in folders like /usr/share. So if you want to properly support these three major operating systems, you have to look at all these places for your files.

Introducing ada-bundler

ada-bundler aims to provide an abstract layer with a univeral API for accessing resource files on these operating systems. It has been written to help developers bundling desktop applications written in Ada. To show you how to use it, let's have a look at some example code.

Writing the ada-bundler configuration file

Consider you have an application named Example. This application needs two data files and a configuration file which should be bundled with it. We start with writing the ada-bundler configuration for this application. The ada-bundler configuration file is a YAML file. Let's see how it looks:

name: Example
version: "0.1"
executable: bin/example
data:
  - data/file1.txt
  - data/file2.txt
configuration:
  - configuration/conf.txt

The contents is quite straight forward: Firstly, you define the name and the version number of your application. Then, you tell ada-bundler where to find the primary executable file. After that comes the payload: You tell ada-bundler which resource files should be bundled with your application. The items are paths relative to the current directory (i.e. the directory where you run ada-bundler from; that should usually be the directory where the ada-bundler configuration file is located).

Instead of file names, you can also specify whole directories. If you do, all the contents of the directory is bundled with your application.

To be able to create a nice application bundle for each target platform, ada-bundler needs some platform-specific configuration - this is the second part of our configuration file:

osx:
  icon: icon.icns
  identifier: com.example.example
windows:
  icon: icon.ico
linux:
  data:
    - data/linux-specific-file.txt

To create an OSX app bundle, you need to provide at least an identifier and an icon file. The identifier is usually something like com.yourcompany.applicationname. You can create the icon file with the iconutil command line tool.

For Windows, you only need to provide an icon file. that file will be embedded in your executable file. It must be an *.ico file.

For Linux, there are no obligatory files that need to be provided. In the example, you see that I add another data file in the linux section. This file will only be bundled when you bundle your application with Linux as target system. You can add additional configuration and data files to every operating system section.

Using ada-bundler in your code

The ada-bundler API is provided with the Ada package Bundle. In the initialization code of your application, you have to tell ada.bundler the name of the folder in which user-specific files of your application should be stored. Usually, this equals the name of your application. After that, you can ask it for the path to one of the resource files you specified in the configuration file:

with Ada.Text_IO;

with Bundle;

procedure Example is
   use Ada.Text_IO;
begin
   -- initialization: 
   Bundle.Set_Application_Folder_Name ("Example");
   -- getting the paths to your files
   declare
      Data_Path_1        : String := Bundle.Data_Path ("file1.txt");
      Data_Path_2        : String := Bundle.Data_Path ("file2.txt");
      Configuration_Path : String := Bundle.Data_Path ("conf.txt");
      
      Data_File_1 : File_Type;
   begin
      Open (Data_File_1, In_File, Data_Path_1);
      -- do something useful here
      Close (Data_File_1);
   end;
end Example;

Note that when you reference the files you bundled with your application, they are located directly in the base directory where your resource files lie - even if you included them from subfolders like data/file1.txt. Of course, if you copy whole directories, the file structure in the directory is preserved.

You can also get paths to user-specific files you want to create or read:

declare
   User_File_Path : String := Bundle.User_Data_Path ("subfolder/user-data.txt");
begin
   -- do something useful here
end;

Note that you can give relative paths here, and by default, the containing folders will be created when you query for the file path. You can change this behaviour by setting the optional second parameter of User_Data_Path to False.

To include ada-bundler in your application, the simplest way is to add its gpr file as dependency in your project's gpr file:

with "ada_bundler.gpr";
project Example is
   for Main use ("example");
   -- ...
end Example;

ada_bundler.gpr has to be found in your ADA_PROJECT_PATH environment variable. You can also just add it to your project's folder.

When you call GPRBuild, you have to specify the target platform by setting the packaging variable:

$ gprbuild example.gpr -Xpackaging=MacOSX

Possible values are MacOSX, Windows and Linux_Universal. The latter one is named like this because I may possibly add support for RPM and / or DEB packages in the future.

Using the ada-bundler tool

You're ready to bundle your application now! The bundling is implemented with a single script file written in Python, named ada-bundler.py. It takes up to two arguments: The first is the name of your ada-bundler configuration file. It defaults to bundle.yaml. The second is the name of your target system; either osx, windows or linux. It defaults to your host system.

You can integrate ada-bundler in your build process with a Makefile. Here is a simple example:

GPRBUILD = gprbuild -p

PACKAGING := Windows
UNAME := $(shell uname)
ifeq ($(UNAME), Darwin)
   PACKAGING := MacOSX
endif
ifeq ($(UNAME), Linux)
   PACKAGING := Linux_Universal
endif


all: bundle

compile:
   mkdir -p bin
   mkdir -p obj
   ${GPRBUILD} -P example.gpr -Xpackaging=${PACKAGING}

bundle: compile
   python ../src/tool/ada-bundler.py

clean:
   rm -rf ./obj ./bin

What ada-bundler cannot do

ada-bundler is not a GUI library. To write a cross-platform desktop application with a graphical user interface, you need a library like GTK or Qt. There are Ada bindings for both libaries. A promising Ada project aiming to provide a user interface library is Lumen. If you want to create an OpenGL-based application, you can use AdaOpenGL.

If your application has dependencies on dynamic libraries, you may need to bundle them with your application. This is currently not possible, it will need some planning and diving into the linux packaging systems, because an RPM / DEB backend probably wants to add dependencies to the packages your application depends on instead of bundling the libraries.

Getting ada-bundler

ada-bundler is available at GitHub.

Tags: ada programming