3. Scripts: Embedding script languages¶
In a lot of contexts, you want to give the possibility to users to extend your application. This can be done in several ways: define an Ada API from which they can build dynamically loadable modules, provide the whole source code to your application and let users recompile it, interface with a simpler scripting languages,…
Dynamically loadable modules can be loaded on demand, as their name indicate. However, they generally require a relatively complex environment to build, and are somewhat less portable. But when your users are familiar with Ada, they provide a programming environment in which they are comfortable. As usual, changing the module requires recompilation, re-installation,…
Providing the source code to your application is generally even more complex for users. This requires an even more complex setup, your application is generally too big for users to dive into, and modifications done by one users are hard to provide to other users, or will be lost when you distribute a new version of your application.
The third solution is to embed one or more scripting languages in your application, and export some functions to it. This often requires your users to learn a new language, but these languages are generally relatively simple, and since they are interpreted they are easier to learn in an interactive console. The resulting scripts can easily be redistributed to other users or even distributed with future versions of your application.
The module in GNATColl helps you implement the third solution. It was used extensively in the GPS programming environment for its python interface.
Each of the scripting language is optional
This module can be compiled with any of these languages as an optional dependency (except for the shell language, which is always built-in, but is extremely minimal, and doesn’t have to be loaded at run time anyway). If the necessary libraries are found on the system, GNATColl will be build with support for the corresponding language, but your application can chose at run time whether or not to activate the support for a specific language.
Use a scripting language to provide an automatic testing framework for your application.
The GPS environment uses python command for its automatic test suite, including graphical tests such as pressing on a button, selecting a menu,…
3.1. Supported languages¶
The module provides built-in support for several scripting languages, and other languages can “easily” be added. Your application does not change when new languages are added, since the interface to export subprograms and classes to the scripting languages is language-neutral, and will automatically export to all known scripting languages.
The Core component provides support for the following language:
- Shell
- This is a very simple-minded scripting language, which doesn’t provide flow-control instructions (The Shell language).
Optional components add support for other languages, e.g. Python. Please refer to the corresponding component’s documentation.
3.1.1. The Shell language¶
The shell language was initially developed in the context of the GPS programming environment, as a way to embed scripting commands in XML configuration files.
In this language, you can execute any of the commands exported by the application, passing any number of arguments they need. Arguments to function calls can, but need not, be quoted. Quoting is only mandatory when they contain spaces, newline characters, or double-quotes (‘”’). To quote an argument, surround it by double-quotes, and precede each double-quote it contains by a backslash character. Another way of quoting is similar to what python provides, which is to triple-quote the argument, i.e. surround it by ‘”“”’ on each side. In such a case, any special character (in particular other double-quotes or backslashes) lose their special meaning and are just taken as part of the argument. This is in particular useful when you do not know in advance the contents of the argument you are quoting:
Shell> function_name arg1 "arg 2" """arg 3"""
Commands are executed as if on a stack machine: the result of a command is pushed on the stack, and later commands can reference it using % following by a number. By default, the number of previous results that are kept is set to 9, and this can only be changed by modifying the source code for GNATColl. The return values are also modified by commands executed internally by your application, and that might have no visible output from the user’s point of view. As a result, you should never assume you know what %1,… contain unless you just executed a command in the same script:
Shell> function_name arg1
Shell> function2_name %1
In particular, the %1 syntax is used when emulating object-oriented programming in the shell. A method of a class is just a particular function that contains a ‘.’ in its name, and whose first implicit argument is the instance on which it applies. This instance is generally the result of calling a constructor in an earlier call. Assuming, for instance, that we have exported a class “Base” to the shell from our Ada core, we could use the following code:
Shell> Base arg1 arg2
Shell> Base.method %1 arg1 arg2
to create an instance and call one of its methods. Of course, the shell is not the best language for object-oriented programming, and better languages should be used instead.
When an instance has associated properties (which you can export from Ada using Set_Property), you access the properties by prefixing its name with “@”:
Shell> Base arg1 arg2 # Build new instance
Shell> @id %1 # Access its "id" field
Shell> @id %1 5 # Set its "id" field
Some commands are automatically added to the shell when this scripting language is added to the application. These are
- Function load (file)
- Loads the content of file from the disk, and execute each of its lines as a Shell command. This can for instance be used to load scripts when your application is loaded
- Function echo (arg…)
- This function takes any number of argument, and prints them in the console associated with the language. By default, when in an interactive console, the output of commands is automatically printed to the console. But when you execute a script through load above, you need to explicitly call echo to make some output visible.
- Function clear_cache
- This frees the memory used to store the output of previous commands. Calling %1 afterward will not make sense until further commands are executed.
3.1.2. Classes exported to all languages¶
In addition to the functions exported by each specific scripting language, as described above, GNATColl exports the following to all the scripting languages. These are exported when your Ada code calls the Ada procedure GNATCOLL.Scripts.Register_Standard_Classes, which should done after you have loaded all the scripting languages.
- Class Console
Console is a name that you can chose yourself when you call the above Ada procedure. It will be assumed to be Console in the rest of this document.
This class provides an interface to consoles. A console is an input/output area in your application (whether it is a text area in a graphical application, or simply standard text I/O in text mode). In particular, the python standard output streams sys.stdin, sys.stdout and sys.stderr are redirected to an instance of that class. If you want to see python’s error messages or usual output in your application, you must register that class, and define a default console for your scripting language through calls to GNATCOLL.Scripts.Set_Default_Console.
You can later add new methods to this class, which would be specific to your application. Or you can derive this class into a new class to achieve a similar goal.
- Console.write(text)
- This method writes text to the console associated with the class instance. See the examples delivered with GNATColl for examples on how to create a graphical window and make it into a Console.
- Console.clear()
- Clears the contents of the console.
- Console.flush()
- Does nothing currently, but is needed for compatibility with python. Output through Console instances is not buffered anyway.
- Console.isatty(): Boolean
- Whether the console is a pseudo-terminal. This is always wrong in the case of GNATColl.
- Console.read([size]): string
- Reads at most size bytes from the console, and returns the resulting string.
- Console.readline([size]): string
- Reads at most size lines from the console, and returns them as a single string.
3.2. Scripts API¶
This section will give an overview of the API used in the scripts module.
The reference documentation for this API is in the source files themselves. In
particular, each .ads
file fully documents all its public API.
As described above, GNATColl contains several levels of API. In particular, it provides a low-level interface to python, in the packages GNATCOLL.Python. This interface is used by the rest of GNATColl, but is likely too low-level to really be convenient in your applications, since you need to take care of memory management and type conversions by yourself.
Instead, GNATColl provides a language-neutral Ada API. Using this API, it is transparent for your application whether you are talking to the Shell, to python, or to another language integrated in GNATColl. The code remains exactly the same, and new scripting languages can be added in later releases of GNATColl without requiring a change in your application. This flexibility is central to the design of GNATColl.
In exchange for that flexibility, however, there are language-specific features that cannot be performed through the GNATColl API. At present, this includes for instance exporting functions that return hash tables. But GNATColl doesn’t try to export the greatest set of features common to all languages. On the contrary, it tries to fully support all the languages, and provide reasonable fallback for languages that do not support that feature. For instance, named parameters (which are a part of the python language) are fully supported, although the shell language doesn’t support them. But that’s an implementation detail transparent to your own application.
Likewise, your application might decide to always load the python scripting language. If GNATColl wasn’t compiled with python support, the corresponding Ada function still exists (and thus your code still compiles), although of course it does nothing. But since the rest of the code is independent of python, this is totally transparent for your application.
GNATColl comes with some examples, which you can use
as a reference when building your own application.
See the <prefix>/share/examples/gnatcoll
directory.
Interfacing your application with the scripting module is a multistep process:
- You must initialize GNATColl and decide which features to load
- You can create an interactive console for the various languages, so that users can perform experiments interactively. This is optional, and you could decide to keep the scripting language has a hidden implementation detail (or just for automatic testing purposes for instance)
- You can export some classes and methods. This is optional, but it doesn’t really make sense to just embed a scripting language and export nothing to it. In such a case, you might as well spawn a separate executable.
- You can load start up scripts or plug-ins that users have written to extend your application.
3.2.1. Initializing the scripting module¶
GNATColl must be initialized properly in order to provide added value to your application. This cannot be done automatically simply by depending on the library, since this initialization requires multiple-step that must be done at specific moments in the initialization of your whole application.
This initialization does not depend on whether you have build support for python in GNATColl. The same packages and subprograms are available in all cases, and therefore you do not need conditional compilation in your application to support the various cases.
3.2.1.1. Create the scripts repository¶
The type GNATCOLL.Scripts.Scripts_Repository will contain various variables common to all the scripting languages, as well as a list of the languages that were activated. This is the starting point for all other types, since from there you have access to everything. You will have only one variable of this type in your application, but it should generally be available from all the code that interfaces with the scripting language.
Like the rest of GNATColl, this is a tagged type, which you can extend in your own code. For instance, the GPS programming environment is organized as a kernel and several optional modules. The kernel provides the core functionality of GPS, and should be available from most functions that interface with the scripting languages. Since these functions have very specific profiles, we cannot pass additional arguments to them. One way to work around this limitation is to store the additional arguments (in this case a pointer to the kernel) in a class derived from Scripts_Repository_Data.
As a result, the code would look like:
with GNATCOLL.Scripts;
Repo : Scripts_Repository := new Scripts_Repository_Record;
or, in the more complex case of GPS described above:
type Kernel_Scripts_Repository is new
Scripts_Repository_Data with record
Kernel : ...;
end record;
Repo : Scripts_Repository := new Kernel_Scripts_Repository'
(Scripts_Repository_Data with Kernel => ...);
3.2.1.2. Loading the scripting language¶
The next step is to decide which scripting languages should be made available to users. This must be done before any function is exported, since only functions exported after a language has been loaded will be made available in that language.
If for instance python support was build into GNATColl, and
if you decide not to make it available to users, your application will
still be linked with libpython
. It is therefore recommended although
not mandatory to only build those languages that you will use.
This is done through a simple call to one or more subprograms. The following example registers both the shell and python languages:
with GNATCOLL.Scripts.Python;
with GNATCOLL.Scripts.Shell;
Register_Shell_Scripting (Repo);
Register_Python_Scripting (Repo, "MyModule");
- Procedure Register_Shell_Scripting (Repo)
- This adds support for the shell language. Any class or function that is now exported through GNATColl will be made available in the shell
- Procedure Register_Python_Scripting (Repo, Module_Name)
- This adds support for the python language. Any class or function exported from now on will be made available in python, in the module specified by Module_Name
3.2.1.3. Exporting standard classes¶
To be fully functional, GNATColl requires some predefined classes to be exported to all languages (Classes exported to all languages). For instance, the Console class is needed for proper interactive with the consoles associated with each language.
These classes are created with the following code:
Register_Standard_Classes (Repo, "Console");
This must be done only after all the scripting languages were loaded in the previous step, since otherwise the new classes would not be visible in the other languages.
- Procedure Register_Standard_Classes(Repo,Console_Class)
- The second parameter Console_Class is the name of the class that is bound to a console, and thus provides input/output support. You can chose this name so that it matches the classes you intend to export later on from your application.
3.2.2. Creating interactive consoles¶
The goal of the scripting module in GNATColl is to work both in text-only applications and graphical applications. However, in both cases applications will need a way to capture the output of scripting languages and display them to the user (at least for errors, to help debugging scripts), and possibly emulate input when a script is waiting for such input.
GNATColl solved this problem by using an abstract class GNATCOLL.Scripts.Virtual_Console_Record that defines an API for these consoles. This API is used throughout GNATCOLL.Scripts whenever input or output has to be performed.
The examples/
directory in the GNATColl package
shows how to implement a console in text mode and in graphical mode.
If you want to provide feedback or interact with users, you will need to
provide an actual implementation for these Virtual_Console, specific
to your application. This could be a graphical text window, or based on
Ada.Text_IO. The full API is fully documented in
gnatcoll-scripts.ads
, but here is a list of the main subprograms that
need to be overriden.
Virtual_Console.Insert_Text (Txt)
Virtual_Console.Insert_Log (Txt)
- Virtual_Console.Insert_Error (Txt)
- These are the various methods for doing output. Error messages could for instance be printed in a different color. Log messages should in general be directed elsewhere, and not be made visible to users unless in special debugging modes.
- Virtual_Console.Insert_Prompt (Txt)
- This method must display a prompt so that the user knows input is expected. Graphical consoles will in general need to remember where the prompt ended so that they also know where the user input starts
- Virtual_Console.Set_As_Default_Console (Script)
- This method is called when the console becomes the default console for a scripting language. They should in general keep a pointer on that language, so that when the user presses enter they know which language must execute the command
- Virtual_Console.Read (Size, Whole_Line) : String
- Read either several characters or whole lines from the console. This is called when the user scripts read from their stdin.
Virtual_Console.Set_Data_Primitive (Instance)
- Virtual_Console.Get_Instance : Console
These two methods are responsible for storing an instance of Console into a GNATCOLL.Scripts.Class_Instance. Such an instance is what the user manipulates from his scripting language. But when he executes a method, the Ada callback must know how to get the associated Virtual_Console back to perform actual operations on it.
These methods are implemented using one of the GNATCOLL.Scripts.Set_Data and GNATCOLL.Scripts.Get_Data operations when in text mode.
Once you have created one or more of these console, you can set them as the default console for each of the scripting languages. This way, any input/output done by scripts in this language will interact with that console, instead of being discarded. This is done through code similar to:
Console := GtkConsole.Create (...);
Set_Default_Console
(Lookup_Scripting_Language (Repo, "python"),
Virtual_Console (Console));
Creating a new instance of Console, although allowed, will by default create an unusable console. Indeed, depending on your application, you might want to create a new window, reuse an existing one, or do many other things when the user does:
c = Console()
As a result, GNATColl does not try to guess the correct behavior, and thus does not export a constructor for the console. So in the above python code, the default python constructor is used. But this constructor does not associate c with any actual Virtual_Console, and thus any call to a method of c will result in an error.
To make it possible for users to create their own consoles, you need to export a Constructor_Method (see below) for the Console class. In addition to your own processing, this constructor needs also to call:
declare
Inst : constant Class_Instance := Nth_Arg (Data, 1);
begin
C := new My_Console_Record; -- or your own type
GNATCOLL.Scripts.Set_Data (Inst, C);
end
3.2.3. Exporting classes and methods¶
Once all scripting languages have been loaded, you can start exporting new classes and functions to all the scripting languages. It is important to realize that through a single Ada call, they are exported to all loaded scripting languages, without further work required on your part.
3.2.3.1. Classes diagram¶
The following diagram shows the dependencies between the major data types
defined in GNATCOLL.Scripts
. Most of these are abstract classes that
are implemented by the various scripting languages. Here is a brief description
of the role of each type:
- Class Scripts_Repository
- As we have seen before, this is a type of which there is a single instance in your whole application, and whose main role is to give access to each of the scripting languages (Lookup_Scripting_Language function), and to make it possible to register each exported function only once (it then takes care of exporting it to each scripting language).
- Class Scripting_Language
Instances of this type represent a specific language. It provides various operations to export subprograms, execute commands, create the other types described below,… There should exists a single instance of this class per supported language.
This class interacts with the script interpreter (for instance python), and all code executed in python goes through this type, which then executes your Ada callbacks to perform the actual operation.
It is also associated with a default console, as described above, so that all input and output of the scripts can be made visible to the user.
- Class Callback_Data
- This type is an opaque tagged type that provides a language-independent interface to the scripting language. It gives for instance access to the various parameters passed to your subprogram (Nth_Arg functions), allows you to set the return value (Set_Return_Value procedure), or raise exceptions (Set_Error_Msg procedure),…
- Record Class_Type
- This type is not tagged, and cannot be extended. It basically represents a class in any of the scripting languages, and is used to create new instances of that class from Ada.
- Class Class_Instance
A class instance represents a specific instance of a class. In general, such an instance is strongly bound to an instance of an Ada type. For instance, if you have a Foo type in your application that you wish to export, you would create a Class_Type called “Foo”, and then the user can create as many instances as he wants of that class, each of which is associated with different values of Foo in Ada.
Another more specific example is the predefined Console class. As we have seen before, this is a Virtual_Console in Ada. You could for instance have two graphical windows in your application, each of which is a Virtual_Console. In the scripting language, this is exported as a class named Console. The user can create two instances of those, each of which is associated with one of your graphical windows. This way, executing Console.write on these instances would print the string on their respective graphical window.
Some scripting languages, in particular python, allow you to store any data within the class instances. In the example above, the user could for instance store the time stamp of the last output in each of the instances. It is therefore important that, as much as possible, you always return the same Class_Instance for a given Ada object. See the following python example:
myconsole = Console ("title") # Create new console myconsole.mydata = "20060619" # Any data, really myconsole = Console ("title2") # Create another window myconsole = Console ("title") # Must be same as first, print myconsole.mydata # so that this prints "20060619"
- Class Instance_Property
- As we have seen above, a Class_Instance is associated in general with an Ada object. This Instance_Property tagged type should be extended for each Ada type you want to be able to store in a Class_Instance. You can then use the Set_Data and Get_Data methods of the Class_Instance to get and retrieve that associated Ada object.
- Class Subprogram_Record
This class represents a callback in the scripting language, that is some code that can be executed when some conditions are met.
The exact semantic here depends on each of the programming languages. For instance, if you are programming in python, this is the name of a python method to execute. If you are programming in shell, this is any shell code.
The idea here is to blend in as smoothly as possible with the usual constructs of each language. For instance, in python one would prefer to write the second line rather than the third:
def on_exit(): pass set_on_exit_callback(on_exit) # Yes, python style set_on_exit_callback("on_exit") # No
The last line (using a string as a parameter) would be extremely unusual in python, and would for instance force you to qualify the subprogram name with the name of its namespace (there would be no implicit namespace resolution).
To support this special type of parameters, the Subprogram_Record type was created in Ada.
Although the exact way they are all these types are created is largely irrelevant to your specific application in general, it might be useful for you to override part of the types to provide more advanced features. For instance, GPS redefines its own Shell language, that has basically the same behavior as the Shell language described above but whose Subprogram_Record in fact execute internal GPS actions rather than any shell code.
3.2.3.2. Exporting functions¶
All functions that you export to the scripting languages will result in a call to an Ada subprogram from your own application. This subprogram must have the following profile:
procedure Handler
(Data : in out Callback_Data'Class;
Command : String);
The first parameter Data gives you access to the parameters of the subprogram as passed from the scripting language, and the second parameter Command is the name of the command to execute. The idea behind this second parameter is that a single Ada procedure might handle several different script function (for instance because they require common actions to be performed).
- Register_Command (Repo,Command,Min_Args,Max_Args,Handler)
- Each of the shell functions is then exported through a call to Register_Command. In its simplest form, this procedure takes the following arguments. Repo is the scripts repository, so that the command is exported to all the scripting languages. Command is the name of the command. Min_Args and Max_Args are the minimum and maximum number of arguments. Most language allow option parameters, and this is how you specify them. Handler is the Ada procedure to call to execute the command.
Here is a simple example. It implements a function called Add, which takes two integers in parameter, and returns their sum:
Arg1_C : aliased constant String := "arg1";
Arg2_C : aliased constant String := "arg2";
procedure Sum
(Data : in out Callback_Data'Class;
Command : String)
is
Arg1, Arg2 : Integer;
begin
Name_Parameters ((1 => Arg1_C'Access, 2 => Arg2_C'Access));
Arg1 := Nth_Arg (Data, 1);
Arg2 := Nth_Arg (Data, 2);
Set_Return_Value (Data, Arg1 + Arg2);
end Sum;
Register_Command (Repo, "sum", 2, 2, Sum'Access);
This is not the most useful function to export! Still, it illustrates a number of important concepts.
3.2.3.2.1. Automatic parameters types¶
When the command is registered, the number of arguments is specified. This means that GNATColl will check on its own whether the right number of arguments is provided. But the type of these arguments is not specified. Instead, your callback should proceed as if they were correct, and try to retrieve them through one of the numerous Nth_Arg functions. In the example above, we assume they are integer. But if one of them was passed as a string, an exception would be raised and sent back to the scripting language to display a proper error message to the user. You have nothing special to do here.
3.2.3.2.2. Support for named parameters¶
Some languages (especially python) support named parameters, ie parameters can be specified in any order on the command line, as long as they are properly identified (very similar to Ada’s own capabilities). In the example above, the call to Name_Parameters is really optional, but adds this support for your own functions as well. You just have to specify the name of the parameters, and GNATColl will then ensure that when you call Nth_Arg the parameter number 1 is really “arg1”. For scripting languages that do not support named parameters, this has no effect.
Your code can then perform as complex a code as needed, and finally return a value (or not) to the scripting language, through a call to Set_Return_Value.
After the above code has been executed, your users can go to the python console and type for instance:
from MyModule import * # MyModule is the name we declared above
print sum (1,2)
=> 3
print sum ()
=> Error: Wrong number of parameters
print sum ("1", 2)
=> Error: Parameter 1 should be an integer
print sum (arg2=2, arg1=1)
=> 3
3.2.3.3. Exporting classes¶
Whenever you want to make an Ada type accessible through the scripting languages, you should export it as a class. For object-oriented languages, this would map to the appropriate concept. For other languages, this provides a namespace, so that each method of the class now takes an additional first parameter which is the instance of the class, and the name of the method is prefixed by the class name.
Creating a new class is done through a call to New_Class, as shown in the example below:
MyClass : Class_Type;
MyClass := GNATCOLL.Scripts.New_Class (Repo, "MyClass");
At this stage, nothing is visible in the scripting language, but all the required setup has been done internally so that you can now add methods to this class.
You can then register the class methods in the same way that you registered functions. An additional parameter Class exists for Register_Command. A method is really just a standard function that has an implicit first parameter which is a Class_Instance. This extra parameter should not be taken into account in Min_Args and Max_Args. You can also declare the method as a static method, ie one that doesn’t take this extra implicit parameter, and basically just uses the class as a namespace.
Some special method names are available. In particular, Constructor_Method should be used for the constructor of a class. It is a method that receives, as its first argument, a class instance that has just been created. It should associate that instance with the Ada object it represents.
Here is a simple example that exports a class. Each instance of this class is associated with a string, passed in parameter to the constructor. The class has a single method print, which prints its string parameter prefixed by the instance’s string. To start with, here is a python example on what we want to achieve:
c1 = MyClass ("prefix1")
c1.print ("foo")
=> "prefix1 foo"
c2 = MyClass () # Using a default prefix
c2.print ("foo")
=> "default foo"
Here is the corresponding Ada code:
with GNATCOLL.Scripts.Impl;
procedure Handler
(Data : **in out** Callback_Data'Class; Command : String)
is
Inst : Class_Instance := Nth_Arg (Data, 1, MyClass);
begin
if Command = Constructor_Method then
Set_Data (Inst, MyClass, Nth_Arg (Data, 2, "default"));
elsif Command = "print" then
Insert_Text
(Get_Script (Data), null,
String'(Get_Data (Inst)) & " " & Nth_Arg (Data, 2));
end if;
end Handler;
Register_Command
(Repo, Constructor_Method, 0, 1, Handler'Access, MyClass);
Register_Command
(Repo, "print", 1, 1, Handler'Access, MyClass);
This example also demonstrates a few concepts: the constructor is declared as a method that takes one optional argument. The default value is in fact passed in the call to Nth_Arg and is set to “default”. In the handler, we know there is always a first argument which is the instance on which the method applies. The implementation for the constructor stores the prefix in the instance itself, so that several instances can have different prefixes (we can’t use global variables, of course, since we don’t know in advance how many instances will exist). The implementation for print inserts code in the default console for the script (we could of course use Put_Line or any other way to output data), and computes the string to output by concatenating the instance’s prefix and the parameter to print.
Note that Set_Data and Get_Data take the class in parameter, in addition to the class instance. This is needed for proper handling of multiple inheritance: say we have a class C that extends two classes A and B. The Ada code that deals with A associates an integer with the class instance, whereas the code that deals with B associates a string. Now, if you have an instance of C but call a method inherited from A, and if Get_Data didn’t specify the class, there would be a risk that a string would be returned instead of the expected integer. In fact, the proper solution here is that both A and B store their preferred data at the same time in the instances, but only fetch the one they actually need. Therefore instances of C are associated with two datas.
Here is a more advanced example that shows how to export an Ada object. Let’s assume we have the following Ada type that we want to make available to scripts:
type MyType is record
Field : Integer;
end record;
As you can see, this is not a tagged type, but could certainly be. There is
of course no procedure Set_Data in GNATCOLL.Scripts
that enables
us to store MyType in a Class_Instance. This example shows how
to write such a procedure. The rest of the code would be similar to the
first example, with a constructor that calls Set_Data, and methods
that call Get_Data:
type MyPropsR is new Instance_Property_Record with record
Val : MyType;
end record;
type MyProps is access all MyPropsR'Class;
procedure Set_Data
(Inst : Class_Instance; Val : MyType)
is
begin
Set_Data (Inst, Get_Name (MyClass), MyPropsR'(Val => Val));
end Set_Data;
function Get_Data (Inst : Class_Instance) return MyType is
Data : MyProps := MyProps (Instance_Property'
(Get_Data (Inst, Get_Name (MyClass))));
begin
return Data.Val;
end Get_Data;
Several aspects worth noting in this example. Each data is associated with a name, not a class as in the previous example. That’s in fact the same thing, and mostly for historical reasons. We have to create our own instance of Instance_Property_Record to store the data, but the implementation presents no special difficulty. In fact, we don’t absolutely need to create Set_Data and Get_Data and could do everything inline in the method implementation, but it is cleaner this way and easier to reuse.
GNATColl is fully responsible for managing the lifetime of the data associated with the class instances and you can override the procedure Destroy if you need special memory management.
3.2.3.4. Reusing class instances¶
We mentioned above that it is more convenient for users of your exported classes if you always return the same class instance for the same Ada object (for instance a graphical window should always be associated with the same class instance), so that users can associate their own internal data with them.
GNATColl provides a few types to facilitate this. In passing, it is worth noting that in fact the Ada objects will be associated with a single instance per scripting language, but each language has its own instance. Data is not magically transferred from python to shell!
You should store the list of associated instances with your object. The type GNATCOLL.Scripts.Instance_List_Access is meant for that purpose, and provides two Set and Get primitives to retrieve existing instances.
The final aspect to consider here is how to return existing instances. This cannot be done from the constructor method, since when it is called it has already received the created instance (this is forced by python, and was done the same for other languages for compatibility reasons). There are two ways to work around that limitation:
Static get methods
With each of your classes, you can export a static method generally called get that takes in parameter a way to identify an existing instance, and either return it or create a new one. It is also recommended to disable the constructor, ie force it to raise an error. Let’s examine the python code as it would be used:
ed = Editor ("file.adb") # constructor => Error, cannot construct instances ed = Editor.get ("file.adb") => Create a new instance ed2 = Editor.get ("file.adb") => Return existing instance ed == ed2 => True
The corresponding Ada code would be something like:
type MyType is record Val : Integer; Inst : Instance_List_Access; end record; type MyTypeAccess is access all MyType; procedure Handler (Data : in out Callback_Data'Class; Cmd : String) is Inst : Class_Instance; Tmp : MyTypeAccess; begin if Cmd = Constructor_Method then Set_Error_Msg (Data, "cannot construct instances"); elsif Cmd = "get" then Tmp := check_if_exists (Nth_Arg (Data, 1)); if Tmp = null then Tmp := create_new_mytype (Nth_Arg (Data, 1)); Tmp.Inst := new Instance_List; end if; Inst := Get (Tmp.Inst.all, Get_Script (Data)); if Inst = No_Class_Instance then Inst := New_Instance (Get_Script (Data), MyClass); Set (Tmp.Inst.all, Get_Script (Data), Inst); Set_Data (Inst, Tmp); end if; Set_Return_Value (Data, Inst); end if; end Handler;
Factory classes
The standard way to do this in python, which applies to other languages as well, is to use the Factory design pattern. For this, we need to create one class (MyClassImpl) and one factory function (MyClass).
The python code now looks like:
ed = MyClass ("file.adb") # Create new instance => ed is of type MyClassImpl ed = MyClass ("file.adb") # return same instance ed.do_something()
It is important to realize that in the call above, we are not calling the constructor of a class, but a function. At the Ada level, the function has basically the same implementation as the one we gave for get above. But the python code looks nicer because we do not have these additional .get() calls. The name of the class MyClassImpl doesn’t appear anywhere in the python code, so this is mostly transparent.
However, if you have more than one scripting language, in particular for the shell, the code looks less nice in this case:
MyClass "file.adb" => <MyClassImpl_Instance_0x12345> MyClassImpl.do_something %1
and the new name of the class is visible in the method call.
3.2.4. Executing startup scripts¶
The final step in starting up your application is to load extensions or plug-ins written in one of the scripting languages.
There is not much to be said here, except that you should use the GNATCOLL.Scripts.Execute_File procedure to do so.
3.2.5. Multithreading applications and scripts¶
Python itself is not thread-safe. So a single thread can call the python C API at a time. To enforce this, the python interpreter provides a global interpreter lock, which you must acquire before calling the C API, and release when you are done. To simulate multitasking, the python interpreter will in fact release and reacquire the lock every 100 micro-instructions (opcodes in the python virtual machine), to give a chance to run to other tasks. So this is preemptive multitasking.
The threads that are created in Ada that do not need access to python do not need any special handling. However, those that need access to python must make a special function call before they first call the python C API, so that python can create a thread-specific data for them.
GNATCOLL.Scripts.Python contains a number of subprograms to interact with the global interpreter lock of the python engine. The initialization of your application needs to do two extra calls:
Register_Python_Scripting (...);
Initialize_Threads_Support; -- Also acquires the lock
Begin_Allow_Threads; -- Releases the lock
Whenever a task needs to execute python commands (or basically use any subprogram from GNATCOLL.Scripts, it needs to do the following:
Ensure_Thread_State; -- Block all python threads
... access to python C API as usual
Begin_Allow_Threads; -- Let other python threads run
In some cases, the simplest is to get the lock at the beginning of the task, and release it when done. This assumes the task executes fast enough. In other cases, you will need finer grain control over the lock.
3.2.6. Debugging scripts¶
GNATColl provides a convenient hook to debug your script. By default, a script (python for instance) will call your Ada callback, which might raise errors. Most of the time, the error should indeed be reported to the user, and you can thus raise a standard exception, or call Set_Error_Msg.
But if you wish to know which script was executing the command, it is generally not doable. You can however activate a trace (Traces: Logging information) called “PYTHON.TB” (for “traceback”), which will output the name of the command that is being executed, as well as the full traceback within the python scripts. This will help you locate which script is raising an exception.