The Python scripting language in CHARM++ allows the user to dynamically execute pieces of code inside a running application, without the need to recompile. This is performed through the CCS (Converse Client Server) framework (see ``Converse Manual'' for more information about this). The user specifies which elements of the system will be accessible through the interface, as we will see later, and then run a client which connects to the server.
In order to exploit this functionality, Python interpreter needs to be installed
into the system, and CHARM++ LIBS need to be built with:
./build LIBS arch options
The interface provides three different types of requests:
There are three modes to run code on the server, ordered here by increase of functionality, and decrease of dynamic flexibility:
The description will follow the client implementation first, and continuing then on the server implementation.
In order to facilitate the interface between the client and the server, some classes are available to the user to include into the client. Currently C++ and java interfaces are provided.
C++ programs need to include PythonCCS-client.h into their code. This file is among the CHARM++ include files. For java, the package charm.ccs needs to be imported. This is located under the java directory on the CHARM++ distribution, and it provides both the Python and CCS interface classes.
There are three main classes provided: PythonExecute, PythonPrint, and PythonFinished which are used for the three different types of request.
All of them have two common methods to enable communication across different platforms:
A tipical invocation to send a request from the client to the server has the following format:
To execute a Python script on a running server, the client has to create an instance of PythonExecute, the two constructors have the following signature (java has a correspondent functionality):
The second one is used for iterative requests (see 3.22.4). The only required argument is the code, a null terminated string, which will not be modified by the system. All the other parameters are optional. They refer to the possible variants that an execution request can be. In particular, this is a list of all the options present:
These flags can be set and checked with the following routines (CmiUInt4 represent a 4 byte unsigned integer):
From a PythonExecute request, the server will answer with a 4 byte integer value, which is a handle for the interpreter that is running. It can be used to request for prints, check if the script has finished, and for reusing the same interpreter (if it was persistent).
A value of 0 means that there was an error and the script didn't run. This is typically due to a request to reuse of an existing interpreter which is not available, either because it was not persistent or because another script is still running on that interpreter.
When a Python script is run inside a CHARM++ application, two Python modules are made available by the system. One is ck, the other is charm. The first one is always present and it represent basic functions, the second is related to high level scripting and it is present only when this is enabled (see 3.22.2 for how to enable it, and 3.22.11 for a description on how to implement charm functions).
The methods present in the ck module are the following:
Sometimes some operations need to be iterated over all the elements in the system. This ``iterative'' functionality provides a shortcut for the client user to do this. As an example, suppose we have a system which contains particles, with their position, velocity and mass. If we implement read and write routines which allow us to access single particle attributes, we may upload a script which doubles the mass of the particles with velocity greater than 1:
Instead of all these read and writes, it will be better to be able to write:
This is what the ``iterative'' functionality provides. In order for this to work, the server has to implement two additional functions (see 3.22.9), and the client has to pass some more information together with the code. This information is the name of the function that has to be called (which can be defined in the ``code'' or have already been uploaded to a persistent interpreter), and a user defined structure which specifies over what data the function should be invoked. These values can be specified either while constructing the PythonExecute variable (see the second constructor in section 3.22.2), or with the following methods:
As for the PythonIterator object, it has to be a class defined by the user, and the user has to insure that the same definition is present inside both the client and the server. The CHARM++ system will simply pass this structure as a void pointer. This structure needs to inherit from PythonIterator. It is recommended that no pointers are used inside this class, and no dynamic memory allocation. If this is the case, nothing else needs to be done.
If instead pointers and dynamic memory allocation is used, the following methods have to be reimplemented:
The first returns the size of the class/structure after being packed. The second returns a pointer to a newly allocated memory containing all the packed data, the returned memory must be compatible with the class itself, since later on this same memory a call to unpack will be performed. Finally, the third will do the work opposite to pack and fix all the pointers. This method will not return anything and is supposed to fix the pointers ``inline''.
In order to receive the output printed by the Python script, the client needs to send a PythonPrint request to the server. The constructor is:
PythonPrint(CmiUInt4 interpreter, bool Wait=true, bool Kill=false);
The interpreter for which the request is made is mandatory. The other parameters are optional. The wait parameter represents whether a reply will be sent back immediately to the client even if there is no output (false), or if the answer will be delayed until there is an output (true). The kill option set to true means that this is not a normal request, but a signal to unblock the latest print request which was blocking.
The returned data will be a non null-terminated string if some data is present (or if the request is blocking), or a 4 byte zero data if nothing is present. This zero reply can happen in different situations:
As for a print kill request, no data is expected to come back, so it is safe to call CcsNoResponse(server).
The two options can also be dynamically set with the following methods:
In order to know when a Python code has finished executing, especially when using persistent interpreters, and a serialization of the scripts is needed, a PythonFinished request is available. The constructor is the following:
PythonFinished(CmiUInt4 interpreter, bool Wait=true);
The interpreter corresponds to the handle for which the request was sent, while the wait option refers to a blocking call (true), or immediate return (false).
The wait option can be dynamically modified with the two methods:
This request will return a 4 byte integer containing the same interpreter value if the Python script has already finished, or zero if the script is still running.
In order for a CHARM++ object (chare, array, node, or nodegroup) to receive python requests, it is necessary to define it as python-compliant. This is done through the keyword python placed in square brackets before the object name in the .ci file. Some examples follow:
In order to register a newly created object to receive Python scripts, the method registerPython of the proxy should be called. As an example, the following code creates a 10 element array myArray, and then registers it to receive scripts directed to ``pycode''. The argument of registerPython is the string that CCS will use to address the Python scripting capability of the object.
As explained previously in subsection 3.22.3, some functions are automatically made available to the scripting code through the ck module. Two of these, read and write are only available if redefined by the object. The signatures of the two methods to redefine are:
The read function receives as a parameter an object specifying from where the data will be read, and returns an object with the information required. The write function will receive two parameters: where the data will be written and what data, and will perform the update. All these PyObjects are generic, and need to be coherent with the protocol specified by the application. In order to parse the parameters, and create the value of the read, please refer to the manual ``Extending and Embedding the Python Interpreter'', and in particular to the functions PyArg_ParseTuple and Py_BuildValue.
In order to use the iterative mode as explained in subsection 3.22.4, it is necessary to implement two functions which will be called by the system. These two functions have the following signatures:
The first one is called once before the first execution of the Python code, and receives two parameters. The first is a pointer to an empty PyObject to be filled with the data needed by the Python code. In order to manage this object, some utility functions are provided. They are explained in subsection 3.22.10.
The second is a void pointer containing information of what the iteration should run over. This parameter may contain any data structure, and an agreement between the client and the user object is necessary. The system treats it as a void pointer since it has no information of what user defined data it contains.
The second function (nextIteratorUpdate) has three parameters. The first contains the object to be filled like in buildIterator, but this time the object contains the PyObject which was provided for the last iteration, potentially modified by the Python function. Its content can be read with the provided routines, used to retrieve the next logical element in the iterator (with which to update the parameter itself), and possibly update the content of the data inside the CHARM++ object. The second parameter is the object returned by the last call to the Python function, and the third parameter is the same data structure passed to buildIterator.
Both functions return an integer which will be interpreted by the system as follows:
They are inherited when declaring an object as Python-compliant, and therefore they are available inside the object code. All of them accept a PyObject pointer where to read/write the data, a string with the name of a field, and one or two values containing the data to be read/written (note that to read the data from the PyObject, a pointer needs to be passed). The strings used to identify the fields will be the same strings that the Python script will use to access the data inside the object.
The name of the function identifies the type of Python object stored inside the PyObject container (i.e String, Int, Long, Float, Complex), while the parameter of the functions identifies the C++object type.
To handle more complicated structures like Dictionaries, Lists or Tuples, please refer to ``Python/C API Reference Manual''.
When other than defining the CHARM++ object as python, an entry method
is also defined python, it can be accessed directly by a Python script through the
charm module. For example, the following definition will be accessible
with the python call:
result = charm.highMethod(var1, var2, var3)
It can accept any number of parameters (even complex like tuples or
dictionaries), and it can return an object as complex as needed.
The method must have the following signature:
The parameter is a handle that is passed by the system and has to be used in subsequent calls to return the control to the Python code. Thus, if the method does not return immediately but it sends out messages to other CHARM++ objects, the handle must be saved somewhere. Note: if another Python script is sent to the server, this second one could also call the same function. If this is possible, the handle should be saved in a non-scalar variable.
The arguments passed by the Python caller can be retrieved using the function:
PyObject *pythonGetArg(int handle);
which will return a PyObject. This object is a Tuple containing a vector of all parameters. It can be parsed using PyArg_ParseTuple to extract the single parameters.
At the end of the computation, when the control flow must be retuned to the Python script, a special returning function needs to be called:
void pythonReturn(int handle, PyObject* result);
where the second parameter is optional, and needs to be present only if a result is returned to Python. The function Py_BuildValue can be used to create this value.
A characteristic of Python is that in a multithreaded environment (like the one provided in CHARM++), the running thread needs to keep a lock to prevent other threads to access any variable. When using high level scripting, and the Python script is suspended for long times waiting for the CHARM++ application to perform the required task, it may be useful to release the lock to allow other threads to run. This can be done using the two functions:
Important to remember is that before any Python value is accessed, the Python interpreter must be awake. This include the functions Py_BuildValue and PyArg_ParseTuple. Note: it is an error to call these functions more than once before the other one is called.
September 22, 2008
Charm Homepage