Subsections


26 . Dynamic Code Injection

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:
Execute
requests to execute a code, it will contain the code to be executed on the server, together with the instructions on how to handle the environment;
Print
asks the server to send back all the strings which have been printed by the script until now;
Finished
asks the server if the current script has finished or it is still running.
There are three modes to run code on the server, ordered here by increase of functionality, and decrease of dynamic flexibility: This documentation will describe the client API first, and then the server API.


26 . 1 Client API

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:
int size();
Returns the size of the class, as number of bytes that will be transmitted through the network (this includes the code and other dynamic variables in the case of PythonExecute ).
char *pack();
Returns a new memory location containing the data to be sent to the server, this is the data which has to be passed to the CcsSendRequest function. The original class will be unmodified and can be reused in subsequent calls.

A typical invocation to send a request from the client to the server has the following format:


 CcsSendRequest (&server, "pyCode", 0, request.size(), request.pack());


26 . 2 PythonExecute

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 corresponding functionality):


 PythonExecute(char *code, bool persistent=false, bool highlevel=false, CmiUInt4 interpreter=0);

PythonExecute(char *code, char *method, PythonIterator *info, bool persistent=false,
              bool highlevel=false, CmiUInt4 interpreter=0);

The second one is used for iterative requests (see  26.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 for an execution request. In particular, this is a list of all the options:

iterative
If the request is a single code (false) or if it represents a function over which to iterate (true) (see  26.4 for more details).

persistent
It is possible to store information on the server which will be retained across different client calls (from simple data all the way up to complete libraries). True means that the information will be retained on the server, false means that the information will be deleted when the script terminates. In order to properly release the memory, when the last call is made (and the data is no longer required), this flag should be set to false. To reuse persistent data, the interpreter field of the request should be set to handle returned by a previous persistent call (see later in this subsection).

high level
In order to have the ability to call high level Charm++ functions (available through the keyword python ) this flag must be set to true. If it is false, the entire module ``charm'' will not be present, but the startup of the script will be faster.

print retain
When the requested action triggers printed output for the client, this data can be retrieved with a PythonPrint request. If the output is not desired, this flag can be set to false, and the output will be discarded. If it is set to true the output will be buffered pending retrieval by the client. The data will survive also after the termination of the Python script, and if not retrieved will bloat memory usage on the server.

busy waiting
Instead of returning a handle immediately to the client, that can be used to retrieve prints and check if the script has finished, the server will answer to the client only when the script has terminated to run (and it will effectively work as a PythonFinished request).

These flags can be set and checked with the following routines (CmiUInt4 represent a 4 byte unsigned integer):


 void setCode(char *set);

void setPersistent(bool set);

void setIterate(bool set);

void setHighLevel(bool set);

void setKeepPrint(bool set);

void setWait(bool set);

void setInterpreter(CmiUInt4 i);


bool isPersistent();

bool isIterate();

bool isHighLevel();

bool isKeepPrint();

bool isWait();

CmiUInt4 getInterpreter();

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 an existing interpreter which is not available, either because it was not persistent or because another script is still running on that interpreter.


26 . 3 Auto-imported modules

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 26.2 for how to enable it, and 26.11 for a description on how to implement charm functions).

The methods present in the ck module are the following:

printstr
It accepts a string as parameter. It will write into the server stdout that string using the CkPrintf function call.

printclient
It accepts a string as parameter. It will forward the string back to the client when it issues a PythonPrint request. It will buffer the strings until requested by PythonPrint if the KeepPrint option is true, otherwise it will discard them.

mype
Requires no parameters, and will return an integer representing the current processor where the code is executing. It is equivalent to the Charm++ function CkMyPe() .

numpes
Requires no parameters, and will return an integer representing the total number of processors that the application is using. It is equivalent to the Charm++ function CkNumPes() .

myindex
Requires no parameters, and will return the index of the current element inside the array, if the object under which Python is running is an array, or None if it is running under a Chare, a Group or a Nodegroup. The index will be a tuple containing as many numbers as the dimension of the array.

read
It accepts one object parameter, and it will perform a read request to the Charm++ object connected to the Python script, and return an object containing the data read (see 26.8 for a description of this functionality). An example of a call can be: value = ck.read((number, param, var2, var3))
where the double parenthesis are needed to create a single tuple object containing four values passed as a single paramter, instead of four different parameters.

write
It accepts two object parameters, and it will perform a write request to the Charm++ object connected to the Python script. For a description of this method, see 26.8 . Again, only two objects need to be passed, so extra parenthesis may be needed to create tuples from individual values.


26 . 4 Iterate mode

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:


 size = ck.read((``numparticles'', 0));

for i in range(0, size):
    vel = ck.read((``velocity'', i));
    mass = ck.read((``mass'', i));
    mass = mass * 2;
    if (vel > 1): ck.write((``mass'', i), mass);

Instead of all these read and writes, it will be better to be able to write:


 def increase(p):
    if (p.velocity > 1): p.mass = p.mass * 2;

This is what the ``iterative'' functionality provides. In order for this to work, the server has to implement two additional functions (see  26.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 was previously 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  26.2 ), or with the following methods:


 void setMethodName(char *name);

void setIterator(PythonIterator *iter);

The PythonIterator object must be defined by the user, and the user must 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 must inherit from PythonIterator . In the simple case (highly recommended), wherein no pointers or dynamic allocation are used inside this class, nothing else needs to be done because it is trivial to serialize such objects.

If instead pointers or dynamic memory allocation are used, the following methods have to be reimplemented to support correct serialization:


 int size();

char * pack();

void unpack();

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''.


26 . 5 PythonPrint

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:


 void setWait(bool set);

bool isWait();


void setKill(bool set);

bool isKill();


26 . 6 PythonFinished

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:


 void setWait(bool set);

bool isWait();

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.


26 . 7 Server API

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:


 mainchare [python] main {...}

array [1D] [python] myArray {...}

group [python] myGroup {...}

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.


 Cproxy_myArray localVar = CProxy_myArray::ckNew(10);

localVar.registerPython(``pycode'');


26 . 8 Server read and write functions

As explained previously in subsection  26.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:


 PyObject* read(PyObject* where);

void write(PyObject* where, PyObject* what);

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 PyObject s 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 .


26 . 9 Server iterator functions

In order to use the iterative mode as explained in subsection  26.4 , it is necessary to implement two functions which will be called by the system. These two functions have the following signatures:


 int buildIterator(PyObject*, void*);

int nextIteratorUpdate(PyObject*, PyObject*, void*);

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  26.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 about what user defined data it contains.

The second function ( nextIteratorUpdate ) has three parameters. The first parameter contains the object to be filled (similar to buildIterator ), but the second 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:

1
- a new iterator in the first parameter has been provided, and the Python function should be called with it;
0
- there are no more elements to iterate.


26 . 10 Server utility functions

These 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.


 void pythonSetString(PyObject*, char*, char*);

void pythonSetString(PyObject*, char*, char*, int);

void pythonSetInt(PyObject*, char*, long);

void pythonSetLong(PyObject*, char*, long);

void pythonSetLong(PyObject*, char*, unsigned long);

void pythonSetLong(PyObject*, char*, double);

void pythonSetFloat(PyObject*, char*, double);

void pythonSetComplex(PyObject*, char*, double, double);


void pythonGetString(PyObject*, char*, char**);

void pythonGetInt(PyObject*, char*, long*);

void pythonGetLong(PyObject*, char*, long*);

void pythonGetLong(PyObject*, char*, unsigned long*);

void pythonGetLong(PyObject*, char*, double*);

void pythonGetFloat(PyObject*, char*, double*);

void pythonGetComplex(PyObject*, char*, double*, double*);

To handle more complicated structures like Dictionaries, Lists or Tuples, please refer to ``Python/C API Reference Manual'' .


26 . 11 High level scripting

When in addition to the definition of the Charm++ object as python , an entry method is also defined as python , this entry method 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:


 entry [python] void highMethod(int handle);

The parameter is a handle that is passed by the system, and can be used in subsequent calls to return values to the Python code.

The arguments passed by the Python caller can be retrieved using the function:

PyObject *pythonGetArg(int handle);

which returns 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.

When the Charm++'s entry method terminates (by means of return or termination of the function), control is returned to the waiting Python script. Since the python entry methods execute within an user-level thread, it is possible to suspend the entry method while some computation is carried on in Charm++. To start parallel computation, the entry method can send regular messages, as every other threaded entry method (see  11.2 for more information on how this can be done using CkCallbackResumeThread callbacks). The only difference with other threaded entry methods is that here the callback CkCallbackPython must be used instead of CkCallbackResumeThread. The more specialized CkCallbackPython callback works exactly like the other one, except that it correctly handles Python internal locks.

At the end of the computation, the following special function will return a value to the Python script:

void pythonReturn(int handle, PyObject* result);

where the second parameter is the Python object representing the returned value. The function Py_BuildValue can be used to create this value. This function in itself does not terminate the entry method, but only sets the returning value for Python to read when the entry method terminates.

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 periods of time while waiting for the Charm++ application to perform the required task, the Python internal locks are automatically released and re-acquired by the CkCallbackPython class when it suspends.