How-Do-I Guide
Working with Object and PyObject
The CPython API is only to be invoked directly from within src/nrnpython
.
The way to support using Python from outside this folder without introducing a dependency on Python is to declare a function
pointer outside the folder, then have that function pointer be set by something inside e.g. src/nrnpython/nrnpy_hoc.cpp
.
All uses outside the folder must first check to see if the funciton pointer is not NULL; if it is NULL then Python is not
available. This is done for a number of functions in the nrnpy_hoc()
function in src/nrnpython/nrnpy_hoc.cpp
.
To check if a NEURON object is wrapping a PyObject
To see if an Object* obj is wrapping a PyObject, check if
obj->ctemplate->sym == nrnpy_pyobj_sym_
The variable nrnpy_pyobj_sym_
here is a Symbol*
and it is shared and available at least within nrnpython
.
(See object types below for more about detecting the type of an Object*
.)
To check if a PyObject is wrapping a NEURON object
Use PyObject_TypeCheck(po, hocobject_type)
where po
is the PyObject
.
To encapsulate a NEURON Object in a PyObject
Use nrnpy_ho2po
. If the NEURON object is wrapping a PyObject
it returns the original PyObject
with the reference count increased by one.
Otherwise it returns a PyHocObject
. Note: this function does not convert NEURON strings or floats to PyObject
instances as those are
not internally stored with Object
instances. Those can be converted directly using the CPython API e.g. PyFloat_FromDouble
, PyLong_FromLong
.
To encapsule a PyObject in a NEURON Object
Use nrnpy_po2ho
. If the object is wrapping a NEURON Object, it increments the NEURON object’s reference count by 1 and returns the original
NEURON object. Otherwise it returns a new NEURON Object* wrapping the PyObject
and increments the PyObject
reference count (remember, this
can be checked using the macro Py_REFCNT
). In particular, this function always returns a NEURON object, even for
strings and floats that would be more naturally represented in NEURON as their respective datatypes.
To check if a PyObject is a number
Use nrnpy_numbercheck(po)
where po
is the PyObject*
to check.
This is built-on but has modified semantics from the CPython API PyNumber_Check
to detect things that NEURON cannot treat directly as numbers,
like complex numbers.
To reference a NEURON Object
Use hoc_obj_ref(obj)
where obj
is an Object*
. This is defined in src/oc/hoc_oop.c
. Include src/oc/oc_ansi.h
to define the function prototype.
To dereference a NEURON Object
Use hoc_obj_unref(obj)
where obj
is an Object*
. This is defined in src/oc/hoc_oop.c
. Include src/oc/oc_ansi.h
to define the function prototype.
Arguments
Only positional arguments (and sec=) are allowed in NEURON except for functions with no HOC equivalent that are handled separately.
Note: NEURON argument indices are always numbered from 1.
Check for the existence of an argument
Use ifarg(n)
to check to see if there is an ``n``th argument (the arguments are numbered from 1).
Many existing functions and methods currently do not check for too many arguments, however doing so is recommended as it indicates a probable user error.
Check the type of an argument
Relevant functions include:
- hoc_is_object_arg(n)
- hoc_is_pdouble_arg(n)
- hoc_is_str_arg(n)
- hoc_is_double_arg(n)
Get the value of an argument
Relevant functions include:
- hoc_obj_getarg(n)
May want to combine this with
nrnpy_ho2po
if you know the argument is aPyObject
; e.g.PyObject* obj = nrnpy_ho2po(*hoc_objgetarg(n))
vector_arg(n)
– returns aVect*
hoc_pgetarg(n)
– returns adouble**
gargstr(n)
getarg(n)
– returns adouble*
. Python bools, ints, and floats are all valid inputs.
Note: attempting to get the wrong type of an argument displays a “bad stack access” message and
a Python RuntimeError
exception gets raised. If multiple types of arguments are possible,
you must check the type of the argument first.
Classes
Declaring classes
Classes are declared using the class2oc
function, e.g.
class2oc("ClassName", cons, destruct, members, NULL, retobj_members, NULL)
Here cons
is the constructor, which must take an Object*
and return a void*
.
destruct
is the destructor, which takes a void*
and has no return.
members
is a null-terminated array of Member_func
of methods that in Python could return float,
integer, or bool. In HOC, these all return doubles.
- To specify the return type as seen by Python, set hoc_return_type_code
. A value of 0 indicates
the funciton is returning a float; 1 indicates an integer; a value of 2 indicates a bool.
- Each of these methods must take a
void*
and return a double.
retobj_methods
is a null-terminated array of Member_ret_obj_func
of methods that return objects.
(The actual functions implementing them take a void*
and return an Object**
.)
Object types
The type of every NEURON Object* obj
is determined by it’s ctemplate->sym
. This is a Symbol*
.
The pointers can be directly compared to see if two objects are of the same type. In particular,
a Symbol
has a char*
field name
. That is, to print the name of the type
that obj
is an instance of, one can use:
printf("The type of obj is: %s\n", obj->ctemplate->sym->name);
The hoc_lookup
function takes a NEURON class name and returns the associated Symbol*
.
For example:
Symbol* vector_sym = hoc_lookup("Vector");
NEURON provides internal convenience functions is_obj_type(Object* obj, const char* type_name)
and check_obj_type(Object* obj, const char* type_name)
that check to see if obj
is of the
type specified by type_name
. The former returns a 1 (true) or 0 (false); the latter has no
return and raises an error if the type is wrong. These work by doing a strcmp
. If the
Symbol*
is known, it is more efficient to directly compare the Symbol*
.
For example, to see if obj
is an instance of Vector
and the Symbol*
is not already
known, use is_obj_type(obj, "Vector")
.
These convenience functions are defined in src/oc/hoc_oop.cpp
.
HocCommand objects
The HocCommand
class, defined in src/ivoc/objcmd.cpp
, provides a consistent interface
for calling Python or HOC code. The constructor accepts a const char*
for HOC, or a
const char*
and an Object*
also for HOC (in this case the HOC string is executed in
the context of the object), or an Object*
that wraps a Python callable.
Each HocCommand
object has a pyobject()
method that returns the underlying Python
object if any, else NULL. This can be used to distinguish between HOC and Python calls.
The execute()
method runs the underlying HOC or Python code. No value is returned in
this case.
The func_call(int narg, int* perr)
method returns a double from invoking the HOC or Python.
The value pointed to by perr
is set to 1 if the HocCommand
is to run Python but running
Python failed. Otherwise perr
is unchanged. In particular, note that if perr
originally
points to a 1, then it will still point to a 1 even upon success. The number of arguments is
indicated with narg
. The arguments themselves must have already been pushed onto NEURON’s
stack, e.g. with pushx
for doubles, hoc_push_object
for Object*
, hoc_push_str
for char**
, or hoc_pushpx
for pointers to doubles (stack manipulation functions are
defined in src/oc/code.cpp
).
Miscellaneous tips
Raising a NEURON error
Use hoc_exec_error
which takes two char*
arguments (which can be NULL). e.g.
hoc_execerror("Message part 1", "Message part 2");
Note: all NEURON errors currently are received by Python as a RuntimeError
exception, and all errors
print their error messages before returning to Python, meaning that they will always print out, even
inside a try/except block.
Checking if the name of an internal symbol
hoc_table_lookup(name, hoc_built_in_symlist)
returns NULL if name
not in the symlist; otherwise
it returns the Symbol*