Vista Normal

Hay nuevos artículos disponibles. Pincha para refrescar la página.
AnteayerHackaday

Programming Ada: Records and Containers for Organized Code

Por: Maya Posch
4 Junio 2024 at 14:00

Writing code without having some way to easily organize sets of variables or data would be a real bother. Even if in the end you could totally do all of the shuffling of bits and allocating in memory by yourself, it’s much easier when the programming language abstracts all of that housekeeping away. In Ada you generally use a few standard types, ranging from records (equivalent to structs in C) to a series of containers like vectors and maps. As with any language, there are some subtle details about how all of these work, which is where the usage of these types in the Sarge project will act as an illustrative example.

In this project’s Ada code, a record is used for information about command line arguments (flag names, values, etc.) with these argument records stored in a vector. In addition, a map is created that links the names of these arguments, using a string as the key, to the index of the corresponding record in the vector. Finally, a second vector is used to store any text fragments that follow the list of arguments provided on the command line. This then provides a number of ways to access the record information, either sequentially in the arguments vector, or by argument (flag) name via the map.

Introducing Generics

Not unlike the containers provided by the Standard Template Library (STL) of C++, the containers provided by Ada are provided as generics, meaning that they cannot be used directly. Instead we have to create a new package that uses the container generic to formulate a container implementation limited to the types which we intend to use with it. For a start let’s take a look at how to create a vector:

with Ada.Containers.Vectors;
use Ada.Containers;
package arg_vector is new Vectors(Natural, Argument);

The standard containers are part of the Ada.Containers package, which we include here before the instantiating of the desired arguments vector, which is indexed using natural numbers (all positive integers, no zero or negative numbers), and with the Argument type as value. This latter type is the custom record, which is defined as follows:

type Argument is record
    arg_short: aliased Unbounded_String;
    arg_long: aliased Unbounded_String;
    description: aliased Unbounded_String;
    hasValue: aliased boolean := False;
    value: aliased Unbounded_String;
    parsed: aliased boolean := False;
end record;

Here the aliased keyword means that the variable will have a memory address rather than only exist in a register. This is a self-optimizing feature of Ada that is being copied by languages like C and C++ that used to require the inverse action by the programmer in the form of the C & C++ register keyword. For Ada’s aliased keyword, this means that the variable it is associated with can have its access (‘pointer’, in C parlance) taken.

Moving on, we can now create the two vectors and the one map, starting with the arguments vector using the earlier defined arg_vector package:

args : arg_vector.vector;

The text arguments vector is created effectively the same way, just with an unbounded string as its value:

package tArgVector is new Vectors(Natural, Unbounded_String);
textArguments: tArgVector.vector;

Finally, the map container is created in a similar fashion. Note that for this we are using the Ada.Containers.Indefinite_Ordered_Maps package. Ordered maps contrast with hashed maps in that they do not require a hash function, but will use the < operator (existing for the type or custom).  These maps provide a look-up time defined as O(log N), which is faster than the O(N) of a vector and the reason why the map is used as an index for the vector here.

package argNames_map is new Indefinite_Ordered_Maps(Unbounded_String, Natural);
argNames: argNames_map.map;

With these packages and instances defined and instantiated, we are now ready to fill them with data.

Cross Mapping

When we define a new argument to look for when parsing command line arguments, we have to perform three operations: first create a new Argument record instance and assign its members the relevant information, secondly we assign this record to the args vector. The record is provided with data via the setArgument procedure:

procedure setArgument(arg_short: in Unbounded_String; arg_long: in Unbounded_String; 
                            desc: in Unbounded_String; hasVal: in boolean);

This allows us to create the Argument instance as follows in the initialization section (before begin in the procedure block) as follows:

arg: aliased Argument := (arg_short => arg_short, arg_long => arg_long, 
                          description => desc, hasValue => hasVal, 
                          value => +"", parsed => False);

This Argument record can then be added to the args vector:

args.append(arg);

Next we have to set up links between the flag names (short and long version) in the map to the relevant index in the argument vector:

argNames.include(arg_short, args.Last_Index);
argNames.include(arg_long, args.Last_Index);

This sets the key for the map entry to the short or long version of the flag, and takes the last added (highest) index of the arguments vector for the value. We’re now ready to find and update records.

Search And Insert

Using the contraption which we just setup is fairly straightforward. If we want to check for example that an argument flag has been defined or not, we can use the arguments vector and the map as follows:

flag_it: argNames_map.Cursor;
flag_it := argNames.find(arg_flag);
if flag_it = argNames_map.No_Element then
    return False;
elsif args(argNames_map.Element(flag_it)).parsed /= True then
    return False;
end if;

This same method can be used to find a specific record to update the freshly parsed value that we expect to trail certain flags:

flag_it: argNames_map.Cursor;
flag_it := argNames.find(arg_flag);
args.Reference(argNames_map.Element(flag_it)).value := arg;

Using the reference function on the args vector gets us a reference to the element which we can then update, unlike the element function of the package. The requisite index into the arguments vector is obtained by

We can now easily check that a particular flag has been found by looking up its record in the vector and return the found value, as defined in the getFlag function in the sarge.adb file of Sarge:

function getFlag(arg_flag: in Unbounded_String; arg_value: out Unbounded_String) return boolean is
flag_it: argNames_map.Cursor;
use argNames_map;
begin
    if parsed /= True then
        return False;
    end if;

    flag_it := argNames.find(arg_flag);
    if flag_it = argNames_map.No_Element then
         return False;
    elsif args(argNames_map.Element(flag_it)).parsed /= True then
        return False;
    end if;

    if args(argNames_map.Element(flag_it)).hasValue = True then
        arg_value := args(argNames_map.Element(flag_it)).value;
    end if;

    return True;
end getFlag;

Other Containers

There are of course many more containers than just the two types covered here defined in Ada’s Predefined Language Library (PLL). For instance, sets are effectively like vectors, except that they only allow for unique elements to exist within the container. This is only the beginning of the available containers, though, with the Ada 2005 standard defining only the first collection, which got massively extended in the Ada 2012 standard (which we focus on here). These include trees, queues, linked lists and so on. We’ll cover some of these in more detail in upcoming articles.

Together with the packages, functions and procedures covered earlier in this series, records and containers form the basics of organizing code in Ada. Naturally, Ada also supports more advanced types of modularization and reusability, such as object-oriented programming, which will also be covered in upcoming articles.

Programming Ada: First Steps on the Desktop

Por: Maya Posch
23 Abril 2024 at 14:00

Who doesn’t want to use a programming language that is designed to be reliable, straightforward to learn and also happens to be certified for everything from avionics to rockets and ICBMs? Despite Ada’s strong roots and impressive legacy, it has the reputation among the average hobbyist of being ‘complicated’ and ‘obscure’, yet this couldn’t be further from the truth, as previously explained. In fact, anyone who has some or even no programming experience can learn Ada, as the very premise of Ada is that it removes complexity and ambiguity from programming.

In this first part of a series, we will be looking at getting up and running with a basic desktop development environment on Windows and Linux, and run through some Ada code that gets one familiarized with the syntax and basic principles of the Ada syntax. As for the used Ada version, we will be targeting Ada 2012, as the newer Ada 2022 standard was only just approved in 2023 and doesn’t change anything significant for our purposes.

Toolchain Things

The go-to Ada toolchain for those who aren’t into shelling out big amounts of money for proprietary, certified and very expensive Ada toolchains is GNAT, which at one point in time stood for the GNU NYU Ada Translator. This was the result of the United States Air Force awarding the New York University (NYU) a contract in 1992 for a free Ada compiler. The result of this was the GNAT toolchain, which per the stipulations in the contract would be licensed under the GNU GPL and its copyright assigned to the Free Software Foundation. The commercially supported (by AdaCore) version of GNAT is called GNAT Pro.

Obtaining a copy of GNAT is very easy if you’re on a common Linux distro, with the package gnat for Debian-based distros and gcc-ada if you’re Arch-based. For Windows you can either download the AdaCore GNAT Community Edition, or if you use MSYS2, you can use its package manager to install the mingw-w64-ucrt-x86_64-gcc-ada package for e.g. the new ucrt64 environment. My personal preference on Windows is the MSYS2 method, as this also provides a Unix-style shell and tools, making cross-platform development that much easier. This is also the environment that will be assumed throughout the article.

Hello Ada

The most important part of any application is its entry point, as this determines where the execution starts. Most languages have some kind of fixed name for this, such as main, but in Ada you are free to name the entry point whatever you want, e.g.:

with Ada.Text_IO;
procedure Greet is
begin
    -- Print "Hello, World!" to the screen
    Ada.Text_IO.Put_Line ("Hello, World!");
end Greet;

Here the entry point is the Greet procedure, because it’s the only procedure or function in the code. The difference between a procedure and a function is that only the latter returns a value, while the former returns nothing (similar to void in C and C++). Comments start with two dashes, and packages are imported using the with statement. In this case we want the Ada.Text_IO package, as it contains the standard output routines like Put_Line. Note that since Ada is case-insensitive, we can type all of those names in lower-case as well.

Also noticeable might be the avoidance of any symbols where an English word can be used, such as the use of is, begin and end rather than curly brackets. When closing a block with end, this is post-fixed with the name of the function or procedure, or the control structure that’s being closed (e.g. an if/else block or loop). This will be expanded upon later in the series. Finally, much like in C and C++ lines end with a semicolon.

For a reference of the syntax and much more, AdaCore has an online reference as well as a number of freely downloadable books, which include a comparison with Java and C++. The Ada Language Reference Manual (LRM) is also freely available.

Compile And Run

To compile the simple sample code above, we need to get it into a source file, which we’ll call greet.adb. The standard extensions with the GNAT toolchain are .adb for the implementation (body) and .ads for the specification (somewhat like a C++ header file). It’s good practice to use the same file name as the main package or entry point name (unit name) for the file name. It will work if not matched, but you will get a warning depending on the toolchain configuration.

Unlike in C and C++, Ada code isn’t just compiled and linked, but also has an intermediate binding step, because the toolchain fully determines the packages, dependencies, and other elements within the project before assembling the compiled code into a binary.

An important factor here is also that Ada does not work with a preprocessor, and specification files aren’t copied into the file which references them with a with statement, but only takes note of the dependency during compilation. A nice benefit of this is that include guards are not necessary, and headaches with linking such as link order of objects and libraries are virtually eliminated. This does however come at the cost of dealing with the binder.

Although GNAT comes with individual tools for each of these steps, the gnatmake tool allows the developer to handle all of these steps in one go. Although some prefer to use the AdaCore-developed gprbuild, we will not be using this as it adds complexity that is rarely helpful. To use gnatmate to compile the example code, we use a Makefile which produces the following output:

mkdir -p bin
mkdir -p obj
gnatmake -o bin/hello_world greet.adb -D obj/
gcc -c -o obj\greet.o greet.adb
gnatbind -aOobj -x obj\greet.ali
gnatlink obj\greet.ali -o bin/hello_world.exe

Although we just called gnatmake, the compilation, binding and linking steps were all executed subsequently, resulting in our extremely sophisticated Hello World application.

For reference, the Makefile used with the example is the following:

GNATMAKE = gnatmake
MAKEDIR = mkdir -p
RM = rm -f

BIN_OUTPUT := hello_world
ADAFLAGS := -D obj/

SOURCES := greet.adb

all: makedir build

build:
	$(GNATMAKE) -o bin/$(BIN_OUTPUT) $(SOURCES) $(ADAFLAGS)
	
makedir:
	$(MAKEDIR) bin
	$(MAKEDIR) obj

clean:
	rm -rf obj/
	rm -rf bin/
	
.PHONY: test src

Next Steps

Great, so now you have a working development environment for Ada with which you can build and run any code that you write. Naturally, the topic of code editors and IDEs is one can of flamewar that I won’t be cracking open here. As mentioned in my 2019 article, you can use AdaCore’s GNAT Programming Studio (GPS) for an integrated development environment experience, if that is your jam.

My own development environment is a loose constellation of Notepad++ on Windows, and Vim on Windows and elsewhere, with Bash and similar shells the environment for running the Ada toolchain in. If there is enough interest I’d be more than happy to take a look at other development environments as well in upcoming articles, so feel free to sound off in the comments.

For the next article I’ll be taking a more in-depth look at what it takes to write an Ada application that actually does something useful, using the preparatory steps of this article.

❌
❌