Gem #106: Lady Ada Kisses Python — Part 2

The first part of this Gem described why having a Python interface might provide an effective way to customize and extend your application. It also highlighted how the GNAT Components Collection takes care of a good part of the work. This Gem will now go into the technical details of how you can use GNATCOLL.Scripts in practice. The GNATCOLL documentation provides additional details, so please refer to it if you need more information (see GNATColl: GNAT Reusable Components).

Your application needs to indicate which scripting languages it wants to support. Handles to those languages are stored in a global variable called a Scripts_Repository (which your application provides, so it could be stored in your own record type, or wrapped in a protected object, etc.).

with GNATCOLL.Scripts.Python;
use GNATCOLL.Scripts, GNATCOLL.Scripts.Python;
declare
   Repo : Scripts_Repository := new Scripts_Repository_Record;
begin
   Register_Python_Scripting (Repo, "GPS");
end;

The second parameter on the last line is the name of the Python module that your application exports. This example is extracted from GPS, where all functions and classes are available as GPS.Console, GPS.Logger, etc. This parameter is the namespace in which your exports will go.

The next step is to register some of the standard classes that are needed by GNATCOLL itself. The most important of these is the Console class, which provides input/output between your application and Python. In many cases you do not want to allow Python to output to stdout; for instance, if you are writing a GUI application you would have an interactive Python console for this. As a result, the standard Python input/output will be redirected to instances of the Console class.

Register_Standard_Classes (Repo, "Console");

We now need to define how a console behaves at the Ada level (either to the usual stdin and stdout, or to a GUI window for instance). We will not go into the details of creating custom consoles, but GNATCOLL comes with examples for the two usual cases in its examples/textconsole.ads and examples/gtkconsole.ads files.

Console := GtkConsole.Create (...);  --  See the examples directory
Set_Default_Console
  (Lookup_Scripting_Language (Repo, "python"), Virtual_Console (Console));

At this point, your application can show a GUI window to interact with Python, in which users can write usual Python commands. But your application still hasn't exported anything useful.

Let's take a simple example. We want to export from Ada a function that performs the addition of two integers (of course, this is already provided by Python, but this is just an example). We first need to declare what we are exporting:

Register_Command
   (Repo,
    Command => "add",
    Params  => (1 => Param("p1"), 2 => Param("p2"),
                3 => Param("p3", Optional => True)),
    Handler => Handler'Access);

This indicates that from Python we will be able to make calls such as:

  n = add(p1=23, p2=45)
  n = add(p2=45, p1=23)   # order of parameters is irrelevant
  n = add(23, 45, 67)     # three parameters

The first version shows the use of named parameters in Python (one of the nice features it inherited from Ada). These parameters can be specified in any order in Python, and GNATCOLL will automatically reorder them so that your application always accesses "p1" as the first parameter and "p2" as the second parameter.

As you can see in the Ada code, there is no reference to Python. In fact, "add" will be available in all registered languages (Python here, but also potentially the GPS shell, and in the future other languages). If new languages are added in GNATCOLL, your code will not need any change to benefit from these.

We now need to provide the implementation:

procedure Handler (Data : in out Callback_Data'Class; Command : String) is
   P1, P2 : Integer;
begin
   if Command = "add" then
      P1 := Nth_Arg (Data, 1);
      P2 := Nth_Arg (Data, 2);
      P3 := Nth_Arg (Data, 3, 0);  --  Default value is 0
      Set_Return_Value (Data, P1 + P2 + P3);
   end if;
end Handler;

Again, this code is independent of Python. We always access "P1" as the first parameter (through the call to Nth_Arg), and GNATCOLL will automatically take care of reordering the parameters if the user has specified them in a different order.

GNATCOLL will automatically raise a Python exception if the user calls "add" with an incorrect number of parameters. Note that it's also possible to export functions with optional parameters (P3 in our example). Nth_Arg can then be used to specify the default value for parameter.

Also worth noting is that Nth_Arg will raise an exception if the corresponding parameter has an incorrect type. There exist several variants of Nth_Arg (for strings, integers, booleans, and a few other types). In our case, if the user calls "add" with a string, Nth_Arg will raise an Ada exception that your application can handle. If your application does not catch the exception, it will be propagated to Python.

The example in this Gem has a limited scope, but highlights some of the fundamental features and services that GNATCOLL offers on top of Python. Once GNATCOLL has been properly initialized, exporting new commands requires little additional glue code. Exporting classes and functions is very similar to what the example described. For more information, see the online GNATCOLL documentation at GNATCOLL: Embedding Script Languages.

At this stage, the most difficult task is to define a clear Python API to your application, one that users can understand relatively easily, and yet that can be extended in the future by exporting even more capabilities from Ada, without breaking the whole design of the API.