Houdini 20.0 Python scripting

Extending the hou Module Using C++

On this page

Overview

Houdini provides a module named inlinecpp that lets you write functions in C++ that are accessible from Python. Your C++ source code appears inline in your Python source code, and is automatically compiled into a library for you. This library is automatically loaded, and has the benefits that the code won’t be compiled when the library already exists, and the library won’t be reloaded when it’s already loaded.

The inlinecpp module provides easy access to the C++ HDK (Houdini Development Kit) from Python with only a minimal amount of code. Consider this example that allows you to call UT_String's multiMatch method from Python:

>>> import inlinecpp
>>> mymodule = inlinecpp.createLibrary(
...     name="cpp_string_library",
...     includes="#include <UT/UT_String.h>",
...     function_sources=[
... """
... bool matchesPattern(const char *str, const char *pattern)
... {
...     return UT_String(str).multiMatch(pattern);
... }
... """])
...
>>> string = "one"
>>> for pattern in "o*", "x*", "^o*":
...     print repr(string), "matches", repr(pattern), ":",
...     print mymodule.matchesPattern(string, pattern)
...
'one' matches 'o*' : True
'one' matches 'x*' : False
'one' matches '^o*' : False

This module also allows you to convert between Python objects in the hou module and the corresponding C++ HDK objects, giving you access to methods available in the HDK but not in hou. In this example, a hou.Geometry object is automatically converted to const GU_Detail *:

>>> geomodule = inlinecpp.createLibrary("cpp_geo_methods",
...     includes="#include <GU/GU_Detail.h>",
...     function_sources=["""
... int numPoints(const GU_Detail *gdp)
... { return gdp->getNumPoints(); }"""])
...
>>> geo = hou.node("/obj").createNode("geo").displayNode().geometry()
>>> geomodule.numPoints(geo)
80

This module also lets you extend hou classes using C++:

>>> inlinecpp.extendClass(hou.Geometry, "cpp_geo_methods", function_sources=["""
... int numPoints(const GU_Detail *gdp)
... { return gdp->getNumPoints(); }"""])
...
>>> geo = hou.node("/obj").createNode("geo").displayNode().geometry()
>>> geo.numPoints()
80

Tip

Be careful extending the hou classes, since users of your extensions may not differentiate between methods provided by Houdini and methods provided by your extensions, and may be confused when using Houdini without your extensions. You may want to name the methods you add with a common prefix to make it clear they are extensions.

Usage

The following functions are available in the inlinecpp module:

createLibrary(name, includes="", function_sources=(), debug=False,
              catch_crashes=None, acquire_hom_lock=False, structs=(),
              include_dirs=(), link_dirs=(), link_libs=())

This returns a module-like object.

Create a library of C++ functions from C++ source, returning it if it’s already loaded, compiling it only if it hasn’t already been compiled.

name

A unique name used to build the shared object file’s name. Be careful not to reuse the same name, or inlinecpp will delete the library when it encounters Python code that creates another library with the same name, leading to wasted time because of unnecessary recompilations.

includes

A string containing #include lines to go before your functions. You can also put helper functions in this string that can be called from your functions but are not exposed to Python.

function_sources

A sequence of strings, with each string containing the source code to one of your functions. The string must begin with the signature for your function so it can be parsed out.

debug

If True, the code will be compiled with debug information. If True and you do not specify a value for catch_crashes, Houdini will also attempt to convert crashes in your C++ code into Python RuntimeError exceptions.

catch_crashes

If True, Houdini will attempt to catch crashes in your C++ code and convert them into Python RuntimeError exceptions containing a C++ stack trace. There is no guarantee that Houdini can always recover from crashes in your C++ code, so Houdini may still crash even if this parameter is set to True. Setting this parameter to None (the default) will make it use the same setting as the debug parameter.

acquire_hom_lock

If True, the code will be automatically modified to use a HOM_AutoLock, to ensure threadsafe access to the C++ object when the Python code is being run in a separate thread. If your code modifies Houdini’s internal state, set this parameter to True.

structs

A sequence of descriptions of C structures that can be used as return values. Each structure description is a pair, where the first element is the name of the structure and the second is a sequence of member descriptions. Each member description is a pair of strings containing the member name and type.

Note that, instead of a sequence of member descriptions, the second element of a struct description may be a string. In this case, the type will be a typedef. These typedefs are useful to create type names for arrays of values.

The details of this parameter are discussed below.

include_dirs

A sequence of extra directory paths to be used to search for include files. These paths are passed as -I options to the hcustom command when compiling the C++ code.

extendClass(cls, library_name, includes="", function_sources=(), debug=False, catch_crashes=None, structs=(), include_dirs=(), link_dirs=(), link_libs=())

Extend a hou class by adding methods implemented in C++.

cls is the hou module class you're extending. The rest of the arguments are the same as for createLibrary, except acquire_hom_lock is always True. Note that this function automatically adds a #include line for the underlying C++ class.

The first parameter to your C++ functions must be a pointer to a C++ object corresponding to the hou object.

Compiler Errors and Warnings

inlinecpp uses the hcustom HDK tool to compile and link your C++ code. If your C++ code fails to compile, it raises an inlinecpp.CompilerError exception containing the full output from hcustom, including the compiler errors.

Since inlinecpp uses hcustom, you must have set up your environment to use the HDK. On Windows, you need to have Microsoft Visual Studio C++ (either Express or Professional Edition) installed. You can optionally have the MSVCDir environment variable set to control which compiler inlinecpp uses, but if it is not set inlinecpp will try to automatically set it for you. On Linux and Mac, you should not need to do anything to set up the compiler environment.

If your code compiled, you can check to see if there were any compiler warnings by calling the _compiler_output method on the object returned by createLibrary. For example:

>>> mymodule = inlinecpp.createLibrary("test", function_sources=["""
... int doubleIt(int value)
... {
...     int x;
...     return value * 2;
... }
... """])
...
>>> print mymodule._compiler_output()
Making /home/user/houdiniX.Y/inlinecpp/test_Linux_x86_64_10.5.313_PM3a6t3irKe+jdb113yHpw.o and
/home/user/houdiniX.Y/inlinecpp/test_Linux_x86_64_10.5.313_PM3a6t3irKe+jdb113yHpw.so from
/home/user/houdiniX.Y/inlinecpp/test_Linux_x86_64_10.5.313_PM3a6t3irKe+jdb113yHpw.C
/home/user/houdiniX.Y/inlinecpp/test_Linux_x86_64_10.5.313_PM3a6t3irKe+jdb113yHpw.C: In function int doubleIt(int):
/home/user/houdiniX.Y/inlinecpp/test_Linux_x86_64_10.5.313_PM3a6t3irKe+jdb113yHpw.C:8: warning: unused variable x

Note that if createLibrary did not need to compile the library because it was already compiled, calling _compiler_output will force the library to compile.

Allowed Parameter Types

Your C++ functions may only use certain parameter types. Valid parameter types are:

int

pass something that can be converted to a Python int

float

pass something that can be converted to a Python float

double

pass something that can be converted to a Python float

const char *

pass a Python str object

bool

pass something that can be converted to a Python bool

GU_Detail *

pass a hou.Geometry object from inside a Python SOP

const GU_Detail *

pass a hou.Geometry object

OP_Node *

pass a hou.OpNode object

CHOP_Node *

pass a hou.ChopNode object

COP2_Node *

pass a hou.Cop2Node object

DOP_Node *

pass a hou.DopNode object

LOP_Node *

pass a hou.LopNode object

OBJ_Node *

pass a hou.ObjNode object

ROP_Node *

pass a hou.RopNode object

SHOP_Node *

pass a hou.ShopNode object

SOP_Node *

pass a hou.SopNode object

VOP_Node *

pass a hou.VopNode object

VOPNET_Node *

pass a hou.VopNetNode object

OP_Operator *

pass a hou.NodeType object

OP_OperatorTable *

pass a hou.NodeTypeCategory object

PRM_Tuple *

pass a hou.ParmTuple object

CL_Track *

pass a hou.Track object

SIM_Data *

pass a hou.DopData object

UT_Vector2D *

pass a hou.Vector2 object

UT_Vector3D *

pass a hou.Vector3 object

UT_Vector4D *

pass a hou.Vector4 object

UT_DMatrix3 *

pass a hou.Matrix3 object

UT_DMatrix4 *

pass a hou.Matrix4 object

UT_BoundingBox *

pass a hou.BoundingBox object

UT_Color *

pass a hou.Color object

UT_QuaternionD *

pass a hou.Quaternion object

UT_Ramp *

pass a hou.Ramp object

The following details are important to note:

  • If your function receives a GU_Detail *, you must pass in a hou.Geometry object that is not read-only, otherwise inlinecpp will raise a hou.GeometryPermissionError exception when you call your function. In other words, only use a GU_Detail * for functions being called from a Python SOP where you can modify the geometry. For functions that do not modify the geometry, use a const GU_Detail * parameter.

  • If you use a parameter type other than the ones listed above, inlinecpp will convert the Python object passed in to a void *. For example, if you have a parameter type of int * you may pass in a Python integer containing an address of an integer array.

  • Do not modify the contents of a Python string from C++ code by receiving a string as a char * and changing the data. Strings in Python are immutable, and changing a string’s contents risks invalidating Python’s internal state and crashing Houdini.

Allowed Return Types

The following return types are provided by default:

void

converted to None

int

converted to a Python int

float

converted to a Python float

double

converted to a Python float

const char *

converted to a Python str (return a null-terminated string that will not be freed

char *

converted to a Python str (return a null-terminated string that will be freed with free())

bool

converted to True or False

inlinecpp::String

converted to a Python 3 str, or a Python 2 unicode (construct with a std::string)

inlinecpp::BinaryString

converted to a Python 3 bytes, or a Python 2 str (construct with a std::string)

The following details are important to note:

  • The best way for your functions to return a string is to return an inlinecpp::BinaryString (see below for details). However, your functions can also return strings by returning pointers to a null-terminated character array. The return type of your function determines if the array is freed after converting it to a Python string. If the return type is const char *, the array is not freed. If it is char *, the array is freed using free(). There is no way to free the character array with delete [], so if the array was allocated with new, return the strdup'd result and delete the array from your function.

You may also easily create your own return types. See below for details.

Returning Strings

The easiest way to return a string is for your C++ function to return an inlinecpp::String and then construct that String from a std::string. The following example returns a string containing a node’s deletion script:

node_utilities = inlinecpp.createLibrary("node_utilities",
    acquire_hom_lock=True,
    includes="#include <OP/OP_Node.h>",
    function_sources=["""
inlinecpp::String deletion_script(OP_Node *node)
{
    return node->getDelScript().toStdString();
}
"""])

Sometimes your C++ code will need to return a null-terminated C-style string. If this C-style string is owned by local variables inside the function and will be freed when the function returns, you need to return a copy of the string. There are a two ways to return such a copy. The first is simply to construct a std::string from the C-style string, and return an inlinecpp::String. The second is to return an inlinecpp::String and construct it using the inlinecpp::as_string function, avoiding the construction of the std::string, as illustrated below:

node_utilities = inlinecpp.createLibrary("node_utilities",
    acquire_hom_lock=True,
    includes="#include <OP/OP_Node.h>",
    function_sources=["""
inlinecpp::String deletion_script(OP_Node *node)
{
    return inlinecpp::as_string(node->getDelScript().nonNullBuffer());
}
"""])

If your C++ code needs to return a C-style string that will not be freed when the function returns and does not need to be returned by the caller, you can simply use a return type of const char * (not char *). When returning large strings, this approach is more efficient than returning inlinecpp::String because it avoids an unnecesary copying of the data. Note that Python will create a copy of the string, so it’s ok if the C-style string’s contents change after your function returns. Here is an example:

example_lib = inlinecpp.createLibrary("example_lib",
    includes="#include <unistd.h>",
    function_sources=["""
const char *user_name()
{
    return getlogin();
}
"""])

Finally, if your C++ code needs to return a C-style string that does need to be freed, use a return type of char * (not const char *). Note that inlinecpp will call free() on the data, not delete []; if you need to delete the array, create a copy of it into a std::string, delete the array, and return the std::string as an inlinecpp::String. The following (artificial) example illustrates how to return a string that will be freed:

example_lib = inlinecpp.createLibrary("example_lib",
    function_sources=["""
char *simple_string()
{
    return strdup("hello world");
}
"""])

Returning Binary Data

If your C++ code needs to return arbitrary binary data, you can return an inlinecpp::BinaryString that is converted into a Python bytes object. For example, if you need to return a string that may contain null characters, you can call the set method of an inlinecpp::BinaryString object, passing in a const char * pointer to the data and the size in bytes. Here is an example:

example_lib = inlinecpp.createLibrary("example_lib",
    function_sources=["""
inlinecpp::BinaryString build_binary_data()
{
    char binary_data[] = "embedded\0null\0characters";
    inlinecpp::BinaryString result;
    result.set(binary_data, 24);
    return result;
}
"""])

You can also use inlinecpp::BinaryString to return the binary representation of an array of ints, floats, or doubles. Simply store the values in a std::vector of the appropriate type and construct the inlinecpp::BinaryString from the std::vector by calling inlinecpp::as_binary_string, as in the following example. Using Python’s array module you can easily convert the string back into an array of the appropriate type. (Note that it is possible to return an array without having to use inlinecpp::BinaryString by using array return types.)

import array

example_lib = inlinecpp.createLibrary("example_lib",
    function_sources=["""
inlinecpp::BinaryString build_int_array()
{
    std::vector<int> values(10);
    for (int i=0; i<10; ++i)
        values[i] = i;
    return inlinecpp::as_binary_string(values);
}
"""])

>>> data = example_lib.build_int_array()
>>> int_array = array.array("i", data)
>>> for value in int_array:
...     print value

Finally, if you need to return a dynamically-allocated array of ints, floats, or doubles, simply call inlinecpp::BinaryString’s set method using a pointer to the first element in the array and the size of the array, in bytes. (Note that a simpler approach is possible is possible with array return types.)

example_lib = inlinecpp.createLibrary("example_lib",
    function_sources=["""
inlinecpp::BinaryString build_int_array()
{
    int *values = new int[10];
    for (int i=0; i<10; ++i)
        values[i] = i;

    inlinecpp::BinaryString result;
    result.set((const char *)values, 10 * sizeof(int));
    delete [] values;
    return result;
}
"""])

Custom Return Types

In addition to the default return types and string return types listed above, it is possible to return structures and arrays that are nicely converted into objects and sequences in Python.

To use such return types, pass the structs parameter into createLibrary with a description of the C++ structures you would like to return from the functions in your library. Set this parameter to a sequence of descriptions, where each element in the sequence is a pair of values. The first element in the pair is the name of the structure, and the second is a sequence of member descriptions. Each member description is a pair of strings containing the member name and type. Member types may be one of the format characters used by Python’s struct module (e.g. i for integer, d for double, f for float, c for character, etc.).

The following example creates a structure named Position2D with two double members named x and y, and returns the position of a node.

>>> node_utilities = inlinecpp.createLibrary(
...     acquire_hom_lock=True,
...     name="node_utilities",
...     includes="#include <OP/OP_Node.h>",
...     structs=[("Position2D", (
...         ("x", "d"),
...         ("y", "d"),
...     ))],
...     function_sources=["""
... Position2D node_position(OP_Node *node)
... {
...     Position2D result;
...     result.x = node->getX();
...     result.y = node->getY();
...     return result;
... }
... """])
...
>>> geo_node = hou.node("/obj").createNode("geo")
>>> geo_node.setPosition((3.5, 4.5))
>>> print geo_node.position()
[3.5, 4.5]
>>> position = node_utilities.node_position(geo_node)
>>> print position
<inlinecpp.Position2D object at 0x7f10e7cf0d90>
>>> print position.x
3.5
>>> print position.y
4.5

From the previous example, note that structs are initialized in C++ by creating a struct instance and assigning to the individual members.

Returning Structs With Array Members

Member type strings may also be preceded by a *, to indicate an array of those elements. For example, *i is an array of integers. The following example shows how to return two arrays of numbers:

example_lib = inlinecpp.createLibrary(
    "example_lib",
    structs=[("ArrayPair", (
        ("ints", "*i"),
        ("doubles", "*d"),
    ))],
    function_sources=["""
ArrayPair build_array_pair()
{
    std::vector<int> ints;
    ints.push_back(2);
    ints.push_back(4);

    std::vector<double> doubles;
    doubles.push_back(1.5);
    doubles.push_back(3.5);

    ArrayPair result;
    result.ints.set(ints);
    result.doubles.set(doubles);
    return result;
}
"""])

>>> array_pair = example_lib.build_array_pair()
>>> print array_pair.ints[0]
2
>>> print array_pair.doubles[1]
3.5
>>> print zip(array_pair.ints, array_pair.doubles)
[(2, 1.5), (4, 3.5)]

The above example called the set method of the array members, passing in a std::vector. You can pass the following values into the set method:

  • a std::vector

  • a pointer and a number of elements

  • a std::string (only if the array is an array of characters)

  • a std::vector of std::vectors (only if the array is an array of arrays)

  • a std::vector<std::string> (only if the array is an array of arrays of characters)

Returning Arrays

Instead of a sequence of member descriptions, the second element of a structure description pair may also be a string. In this case, inlinecpp creates a typedef, and such typedefs are useful when returning arrays of values. The following example shows how to return an array of integers corresponding to the node ids of the global node selection:

node_utilities = inlinecpp.createLibrary(
    "node_utilities",
    acquire_hom_lock=True,
    includes="""
        #include <OP/OP_Director.h>
        #include <UT/UT_StdUtil.h>
    """,
    structs=[("IntArray", "*i")],
    function_sources=["""
IntArray ids_of_selected_nodes()
{
    std::vector<int> ids;
    UTarrayToStdVector(OPgetDirector()->getPickedItemIds(), ids);
    return ids;
}
"""])

def selected_nodes():
    return [hou.nodeBySessionId(node_id)
        for node_id in node_utilities.ids_of_selected_nodes()]

Note that the above example did not create an IntArray object, call the set method on it, and return it. Instead, you can simply construct the IntArray object from a std::vector. Any set of parameters that can be passed into the set method of an array may also be passed into the constructor. (Structs that are not arrays, however, do not have constructors, so you must assign to each element of the struct.)

Note that you can also call the set method on an array object, passing in a pointer to the first element and the number of elements:

example_lib = inlinecpp.createLibrary("example_lib",
    structs=[("IntArray", "*i")],
    function_sources=["""
IntArray build_int_array()
{
    int *values = new int[10];
    for (int i=0; i<10; ++i)
        values[i] = i;

    IntArray result;
    result.set(values, 10);
    delete [] values;
    return result;
}
"""])

The following example illustrates how to return an array of strings by returning an array of arrays of characters:

example_lib = inlinecpp.createLibrary("example_lib",
    structs=[("StringArray", "**c")],
    function_sources=["""
StringArray build_string_array()
{
    std::vector<std::string> result;
    result.push_back("one");
    result.push_back("two");
    return result;
}
"""])

>>> example_lib.build_string_array()
('one', 'two')

Nesting Structures and Arrays

In addition to format characters, member types may use the name of an earlier type in the sequence of structs, or an array of such types. With this approach, you can nest structs or arrays of structs inside other structs.

For example, the following sequence creates a Point struct with two integer members x and y, and a Data struct containing members named tolerance (a double), single_point (a Point), and points (an array of Points. A C++ function returns a Data value that is automatically converted into a Python object.

example_lib = inlinecpp.createLibrary(
    "example_lib",
    structs=(
        ("Point", (
            ("x", "i"),
            ("y", "i"),
        )),
        ("Data", (
            ("tolerance", "d"),
            ("single_point", "Point"),
            ("points", "*Point"),
        )),
    ),
    function_sources=["""
Data build_nested_struct()
{
    std::vector<Point> points;
    for (int i=0; i<5; ++i)
    {
        Point point;
        point.x = i;
        point.y = i + 1;
        points.push_back(point);
    }

    Data result;
    result.tolerance = 0.01;
    result.single_point.x = 4;
    result.single_point.y = 6;
    result.points.set(points);
    return result;
}
"""])

>>> result = example_lib.build_nested_struct()
>>> print result.tolerance
0.01
>>> print result.single_point.x
4
>>> print result.points[2].y
3

You can also create typedefs that are arrays of structs, or even arrays of arrays of structs. The following example shows how to return an array of array of structures, where each structure contains two integers named prim_id and vertex_id. It creates a function that receives a hou.Geometry object and, for each point in it, returns the vertices (as primitive and vertex ids) that reference that point.

point_ref_utils = inlinecpp.createLibrary(
    "point_ref_utils",
    acquire_hom_lock=True,
    structs=(
        ("ReferencingVertex", (
            ("prim_id", "i"),
            ("vertex_id", "i"),
        )),
        ("ReferencesToPoint", "*ReferencingVertex"),
        ("ReferencesToAllPoints", "*ReferencesToPoint"),
    ),  
    includes="""
#include <GU/GU_Detail.h>
#include <GB/GB_PointRef.h>

int vertex_index(GB_Vertex &vertex, GB_Primitive &prim)
{   
    GEO_Vertex &geo_vertex = dynamic_cast<GEO_Vertex &>(vertex);
    GEO_Primitive &geo_prim = dynamic_cast<GEO_Primitive &>(prim);
    int num_vertices = geo_prim.getVertexCount();
    for (int i=0; i<num_vertices; ++i)
    {
        if (geo_prim.getDetail().getVertexMap() == geo_vertex.getIndexMap()
            && geo_prim.getVertexOffset(i) == geo_vertex.getMapOffset())
        {
            return i; 
        }
    }
    return -1;
}   
""",
    function_sources=["""
ReferencesToAllPoints vertices_referencing_points(const GU_Detail *gdp)
{   
    GB_PointRefArray point_ref_array(gdp, /*group=*/NULL);

    std::vector<ReferencesToPoint> references_to_points;
    for (int i=0; i<point_ref_array.entries(); ++i)
    {       
        std::vector<ReferencingVertex> referencing_vertices;
        for (const GB_PointRef *ref = point_ref_array(i); ref; ref=ref->next)
        {   
            ReferencingVertex referencing_vertex;
            referencing_vertex.prim_id = ref->prim->getNum();
            referencing_vertex.vertex_id = vertex_index(*ref->vtx, *ref->prim);
            referencing_vertices.push_back(referencing_vertex);
        }
        references_to_points.push_back(referencing_vertices);
    }
    return references_to_points;
}
"""])

def vertices_referencing_points(geo):
    """Return a list of values, with one entry per point in the geometry.
    Each entry in the list is a list of vertices, each of which references
    the corresponding point.
    """
    vertices_for_each_point = []
    for references_to_point in point_ref_utils.vertices_referencing_points(geo):
        vertices = []
        for reference_to_point in references_to_point:
            prim = geo.iterPrims()[reference_to_point.prim_id]
            vertices.append(prim.vertex(reference_to_point.vertex_id))
        vertices_for_each_point.append(vertices)
    return vertices_for_each_point

>>> geo = hou.node("/obj").createNode("geo").createNode("box").geometry()
>>> for point, vertices in zip(geo.points(), vertices_referencing_points(geo)):
...     print point
...     for vertex in vertices:
...         print "    ", vertex

Array Parameters

Filling Arrays

When using inlinecpp, you will often you will use C++ code to fill the contents of an array for later use in Python. For example, a generator COP using Python might use C++ to compute the contents of all pixels and use Python to evaluate node parameters and store the pixels into the COP.

cpp_lib = inlinecpp.createLibrary("example", function_sources=["""
void fill_array(float *array, int length)
{
    for (int i=0; i<length; ++i)
        array[i] = i;
}
"""])

length = 10

# The following code illustrates how to use the array module that ships with
# Python to create an array and then pass it into the C++ function.
import array
a = array.array("f", [0] * length)
cpp_lib.fill_array(a.buffer_info()[0], len(a))
print a

# The following code shows how to use Python's numpy module to do the
# same thing:
import numpy
a = numpy.zeros(length, dtype=numpy.float32)
cpp_lib.fill_array(a.ctypes.data, len(a))
print a

# Since the C++ code does not require the array contents to be initialized,
# it is faster with very large arrays to use numpy.empty instead of numpy.zero:
a = numpy.empty(length, dtype=numpy.float32)
cpp_lib.fill_array(a.ctypes.data, len(a))
print a

Note the following:

  • The numpy module has many more features than the array module. However, the array module ships with all Python distributions while numpy does not. You may need to install numpy to use it.

  • For many operations, you can use numpy to modify the contents of the array, and you may not need to use inlinecpp at all.

  • numpy.zeros will initialize all entries in the array to zero, while numpy.empty will leave the array contents uninitialized.

  • "f" tells the array module to create an array of 32-bit floats. Similarly, numpy.float32 (or "f4") tells numpy to create an array of 32-bit floats. "d" tells the array module to use 64-bit doubles, and numpy.float64 (or "f8") tells numpy to do the same.

  • The buffer_info method on an array.array returns a tuple of two integers: the address of the raw array and its size in bytes, so you can pass the first value of the tuple into a C++ function expecting a pointer to the array. Similarly, the ctypes.data attribute on a numpy.array object contains a pointer to the raw data in a numpy array.

  • The C++ function cannot tell the length of the array from just the pointer to the first element, so you must pass in the length as a parameter.

  • For large arrays, the numpy module is more efficient than the array module because the latter does not provide an efficient way to create an array of zeros or an array with uninitialized values. Instead, the array module needs you to first construct a temporary list by writing [0] * length and then initialize the array with the contents of the list. While Python is optimized to contruct lists of small integers (like zero), it is still time consuming to do so and then construct array objects from them when working with very large arrays.

Tip

On Linux and Mac OS, Houdini tries to use the Python distribution available on your computer. So, to install numpy you should only need to install it on your system’s Python distribution and Houdini will pick it up. However, on Windows Houdini uses a Python distribution that ships with Houdini. To use numpy from Houdini you must install it into the appropriate Python directory in $HFS (for example, $HFS/python26).

Both array.arrays and numpy.arrays behave like Python sequences: you can iterate over them and index into them from Python. If necessary, you can convert them into binary strings, too. With array.arrays, write

a.tostring()

and with numpy.arrays, write

str(a.data)

You can pass these strings into methods such as hou.Cop2Node.setPixelsOfCookingPlaneFromString, hou.Geometry.setPointFloatAttribValuesFromString, and hou.Volume.setAllVoxelsFromString. However, note that you can also pass in array.array or numpy.array objects into these methods directly, without first converting them to strings. Doing so is more efficient because it avoids unnecessarily allocating memory and copying the data.

Constructing Arrays From Binary Data

You can easily construct arrays from binary string data returned by Houdini. For example, using the array.array function you might write:

a = array.array("f", geo.pointFloatAttribValuesAsString("P"))

and using the numpy.frombuffer function you might write:

a = numpy.frombuffer(geo.pointFloatAttribValuesAsString("P"), dtype=numpy.float32)
# Note that a is read-only.

or

a = numpy.frombuffer(geo.pointFloatAttribValuesAsString("P"), dtype=numpy.float32).copy()
# The contents of a may be modified.

Note the following:

  • arrays created with the array module are always readable and writable. However, arrays created with numpy are sometimes both readable and writable and at other times only readable. When creating an array with numpy.zeros or numpy.empty, or by calling the copy method on an existing array, the returned array is always writable. However, when creating a numpy array using frombuffer, numpy does not create a copy of the data and the array is read-only.

Modifying Arrays

The following example Python SOP shows how to create an array from binary data returned by Houdini, modify the contents of the array in C++, and then send the data back to Houdini:

import inlinecpp
cpp_module = inlinecpp.createLibrary("example", function_sources=["""
void modify_point_positions(float *point_positions, int num_points)
{
    // Add 0.5 to the y-component of each point.
    for (int point_num=0; point_num < num_points; ++point_num)
    {
        point_positions[1] += 0.5;
        point_positions += 3;
    }
}
"""])

geo = hou.pwd().geometry()
num_points = len(geo.iterPoints())

# The following code uses the array module to modify the positions:
import array
positions = array.array("f", geo.pointFloatAttribValuesAsString("P"))
cpp_module.modify_point_positions(positions.buffer_info()[0], num_points)
geo.setPointFloatAttribValuesFromString("P", positions)

# The following code uses numpy to modify the positions again:
import numpy
positions = numpy.frombuffer(geo.pointFloatAttribValuesAsString("P"), dtype=numpy.float32).copy()
cpp_module.modify_point_positions(positions.ctypes.data, num_points)
geo.setPointFloatAttribValuesFromString("P", positions)

Arrays of Structures

Numpy’s structured arrays provide a mechanism to pass an array of structures to a C++ function that can be received as an array of structs. The following example evaluates the P (position) and Cd (diffuse color) attributes of all points into two numpy arrays, combines the arrays into one, and passes the raw array data into a C++ function. The C++ function receives it as an array of structures where each array element contains x, y, z, red, green, and blue float values.

import numpy
import inlinecpp

geo = hou.pwd().geometry()
num_points = len(geo.iterPoints())

positions_array = numpy.frombuffer(
    geo.pointFloatAttribValuesAsString("P"), dtype="f4,f4,f4")
positions_array.dtype.names = ("x", "y", "z")

colors_array = numpy.frombuffer(
    geo.pointFloatAttribValuesAsString("Cd"), dtype="f4,f4,f4")
colors_array.dtype.names = ("red", "green", "blue")

attribs_array = numpy.empty(num_points, dtype="f4,f4,f4,f4,f4,f4")
attribs_array.dtype.names = ("x", "y", "z", "red", "green", "blue")
for component in "x", "y", "z":
    attribs_array[component] = positions_array[component]
for component in "red", "green", "blue":
    attribs_array[component] = colors_array[component]

cpp_lib = inlinecpp.createLibrary("example",
    includes="""
#pragma pack(push, 1)
struct AttribValues {
    float x;
    float y;
    float z;
    float red;
    float green;
    float blue;
};
#pragma pack(pop)
""",
    function_sources=["""
void process_attribs(AttribValues *attrib_values_array, int length)
{
    for (int i=0; i<length; ++i)
    {
        AttribValues &values = attrib_values_array[i];

        // Do something here to analyze the attribute values.
        cout << values.x << ", " << values.y << ", " << values.z << " "
             << values.red << ", " << values.green << ", " << values.blue
             << endl;
    }
}
"""])

cpp_lib.process_attribs(attribs_array.ctypes.data, len(attribs_array))

Note the following:

  • If the dtype parameter to numpy.frombuffer is a comma-separated string, numpy will create a structured array. "f4" is equivalent to numpy.float32.

  • numpy.empty allocates an array of the correct size and with the correct fields, but will not initialize the data.

  • You can create a view on one field of the array by using the field name as the index into the array. Using these views, you can assign from an array containing all values for one field into one field in the destination array.

  • If you create an inlinecpp C++ function with a parameter type it does not know, it will treat the type as a pointer. So, By passing in the address of numpy array data containing 24 bytes per element, you can receive it as a C++ array of those elements by creating a C++ struct that is 24 bytes in size and whose field line up with the numpy fields.

  • The C++ compiler may add padding between structure elements to make them line up to particular byte boundaries. Use the #pragma pack macros above to ensure that no padding is added to the structures.

  • Numpy’s structured arrays provide nice sorting mechanisms that let you order elements according by the different field values.

Python SOPs

inlinecpp is very useful when writing a Python SOP, since it gives you access to high level operations available in the C++ GU_Detail class. As well, it can be used to accelerate slow loops by re-implementing them in C++. This approach combines the benefits of using the Type Properties dialog to create your parameters (so you do not have to use C++ PRM_Templates) with the performance of a C++ implementation.

Here is an example of the code inside a Python SOP that clips geometry along a plane. It assumes a parameter for the normal (a vector of 3 floats called normal) and a parameter for the distance along that normal (a float called distance) and it creates a plane with that normal, offset from the origin by that distance, and clips the geometry with the plane.

import inlinecpp

geofuncs = inlinecpp.createLibrary(
    "example_clip_sop_library",
    acquire_hom_lock=True,
    includes="""
#include <GU/GU_Detail.h>
#include <GQ/GQ_Detail.h>
""",
    function_sources=["""
void clip(GU_Detail *gdp, float nx, float ny, float nz, float distance)
{
    GQ_Detail *gqd = new GQ_Detail(gdp);
    UT_Vector3 normal(nx, ny, nz);
    gqd->clip(normal, distance, 0);
    delete gqd;
}
"""])

nx, ny, nz = hou.parmTuple("normal").eval()
geofuncs.clip(hou.pwd().geometry(), nx, ny, nz, hou.ch("distance"))

Also see the HDK for examples comparing the same SOP written using pure Python code, Python with inlinecpp, and pure C++ using the HDK. In those examples, the inlinecpp version is just as fast as the HDK version but requires much less code and is much easier to use because of automatic compilation.

Here is another example that adds a destroyMe method to hou.Attrib (note that hou.Attrib.destroy already exists):

import types

geomodule = inlinecpp.createLibrary("geomodule",
    acquire_hom_lock=True,
    includes="""
        #include <GU/GU_Detail.h>

        template <typename T>
        bool delete_attribute(GU_Detail *gdp, T &attrib_dict, const char *name)
        {
            GB_Attribute *attrib = attrib_dict.find(name);
            if (!attrib)
                return false;
            gdp.destroyAttribute(attrib->getOwner(), attrib->getName());
            return true;
        }
    """, function_sources=[ """
        int delete_point_attribute(GU_Detail *gdp, const char *name)
        { return delete_attribute(gdp, gdp->pointAttribs(), name); }
    """, """
        int delete_prim_attribute(GU_Detail *gdp, const char *name)
        { return delete_attribute(gdp, gdp->primitiveAttribs(), name); }
    """, """
        int delete_vertex_attribute(GU_Detail *gdp, const char *name)
        { return delete_attribute(gdp, gdp->vertexAttribs(), name); }
    """, """
        int delete_global_attribute(GU_Detail *gdp, const char *name)
        { return delete_attribute(gdp, gdp->attribs(), name); }
    """])

attrib_type_to_delete_function = {
    hou.attribType.Point: geomodule.delete_point_attribute,
    hou.attribType.Prim: geomodule.delete_prim_attribute,
    hou.attribType.Vertex: geomodule.delete_vertex_attribute,
    hou.attribType.Global: geomodule.delete_global_attribute,
}

def destroyAttrib(attrib):
    return attrib_type_to_delete_function[attrib.type()](attrib.geometry(), attrib.name())

hou.Attrib.destroyMe = types.MethodType(destroyAttrib, None, hou.Attrib)

Extending hou.OpNode Classes

The following examples illustrate how to extend the hou.OpNode class or its subclasses:

This first example adds a method called expandGroupPattern that receives a pattern string and returns a comma-separated list of names of children nodes in groups matching that pattern.

>>> inlinecpp.extendClass(hou.OpNode, "cpp_node_methods", function_sources=["""
... inlinecpp::BinaryString expandGroupPattern(OP_Node *node, const char *pattern)
... {
...     UT_String result;
...     node->expandGroupPattern(pattern, result);
...     return result.toStdString();
... }"""])
...
>>> hou.node("/obj").expandGroupPattern("@group1")
'geo1,geo2,geo3'

This second example adds a setSelectable method to object node objects. (Note that this method is already available as hou.ObjNode.setSelectableInViewport).

inlinecpp.extendClass(
    hou.ObjNode,
    "node_methods",
    includes="#include <UT/UT_UndoManager.h>",
    function_sources=["""
void setSelectable(OBJ_Node *obj_node, bool selectable)
{
    if (!obj_node->canAccess(PRM_WRITE_OK))
        return;

    UT_AutoUndoBlock undo_block("Setting selectable flag", ANYLEVEL);
    obj_node->setPickable(selectable);
}
"""])

Python COPs

For examples of how to use inlinecpp in Python COPs, see Writing Part of the COP in C++ in the Python COPs|/hom/pythoncop2] section.

Raising Exceptions

Your C++ source code cannot raise exceptions to Python. However, one technique to raise exceptions is to have your C++ functions return a code, and then create a wrapper Python function that checks the code and raises an exception if necessary. Here is the same example from above that raises a hou.PermissionError exception.

import types

cpp_node_methods = inlinecpp.createLibrary(
    "node_methods",
    acquire_hom_lock=True,
    includes="""
#include <OBJ/OBJ_Node.h>
#include <UT/UT_UndoManager.h>
""",
    function_sources=["""
bool setObjectSelectable(OBJ_Node *obj_node, bool selectable)
{
    if (!obj_node->canAccess(PRM_WRITE_OK))
        return false;

    UT_AutoUndoBlock undo_block("Setting selectable flag", ANYLEVEL);
    obj_node->setPickable(selectable);
    return true;
}
"""])

def setObjectSelectable(obj_node, selectable):
    if not cpp_node_methods.setObjectSelectable(obj_node, selectable):
        raise hou.PermissionError()

hou.ObjNode.setSelectable = types.MethodType(setObjectSelectable, None, hou.ObjNode)

Returning hou Objects

You cannot return something from your C++ function that gets automatically convert to a corresponding hou object. However, you can use a Python wrapper function to do the conversion for you.

For example, OP_Node::getParmsThatReference takes a parameter name and returns a UT_PtrArray of PRM_Parm pointers. You can create a Python function that receives a hou.Parm object and returns a sequence of hou.ParmTuple objects as follows:

parm_reference_lib = inlinecpp.createLibrary(
    "parm_reference_lib",
    acquire_hom_lock=True,
    structs=[("StringArray", "**c")],
    includes="""
#include <OP/OP_Node.h>
#include <PRM/PRM_Parm.h>

// This helper function is not called from Python directly:
static std::string get_parm_tuple_path(PRM_Parm &parm_tuple)
{
    UT_String result;
    parm_tuple.getParmOwner()->getFullPath(result);
    result += "/";
    result += parm_tuple.getToken();
    return result.toStdString();
}
""",
    function_sources=["""
StringArray get_parm_tuples_referencing(OP_Node *node, const char *parm_name)
{
    std::vector<std::string> result;
    UT_PtrArray<PRM_Parm *> parm_tuples;
    UT_IntArray component_indices;
    node->getParmsThatReference(parm_name, parm_tuples, component_indices);

    // Even though we're returned an array of component indices, they'll often
    // be -1, so we can't use them.
    for (int i=0; i<parm_tuples.entries(); ++i)
        result.push_back(get_parm_tuple_path(*parm_tuples(i)));

    return result;
}
"""])

def parm_tuples_referencing(parm):
    return [hou.parmTuple(parm_tuple_path)
        for parm_tuple_path in parm_reference_lib.get_parm_tuples_referencing(
            parm.node(), parm.name())]

The C++ function in this example returns a space-separated string that is split into a list of strings, and then converted into a list of hou.ParmTuple objects.

Note that this example creates a helper function that is not exposed to Python by putting its source code in the includes parameter.

Distributing Your Compiled Library

If you want to distribute your compiled library without worrying whether the user has a proper compiler environment set up, simply distribute the shared object (.so/.dll/.dylib) file from your $HOME/houdiniX.Y/inlinecpp directory that inlinecpp created. As long as the user doesn’t modify your C++ source code, the checksum will be the same and the file name of the shared object file file will be the same.

Houdini will look in all inlinecpp subdirectories of $HOUDINI_PATH when looking for the shared object, so you can put the shared library at any point in your $HOUDINI_PATH.

To determine the path to the shared file, you can call the _shared_object_path method of the module-like object. For example:

>>> mymodule = inlinecpp.createLibrary("test", function_sources=["""
... int doubleIt(int value) { return value * 2; }"""])
...
>>> mymodule._shared_object_path()
'/home/user/houdiniX.Y/inlinecpp/test_Linux_x86_64_11.0.313_3ypEJvodbs1QT4FjWEVmmg.so'

The library will have different names for different platforms and architectures, so you can distribute multiple shared objects to support multiple platforms without having to worry about conflicts between shared object names.

Storing your Code Outside a Digital Asset

See the Python SOP documentation for an example of how to store your Python source code outside of a digital asset, to make it easier to edit and manage under a version control system.

Fixing Crashes

Your C++ code runs inside the Houdini process, so a bug in your code can easily crash Houdini. By passing debug=True when creating your library, Houdini will compile your code without optimization and with debug symbols, making it easier for you to debug your code by attaching a standard debugger to Houdini.

By default, if you pass debug=True and do not specify a value for the catch_crashes parameter, Houdini will attempt to recover from a crash in your C++ source code and convert it into a Python RuntimeError exception containing a C++ stack trace. For example, the following code crashes because it dereferences a null pointer:

>>> mymodule = inlinecpp.createLibrary("crasher", debug=True, function_sources=["""
... void crash()
... {
...     int *p = 0;
...     *p = 3;
... }
... """])
>>> mymodule.crash()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/opt/hfs/houdini/python3.9libs/inlinecpp.py", line 620, in __call__
    return self._call_c_function(*args)
  File "/opt/hfs/houdini/python3.9libs/inlinecpp.py", line 589, in call_c_function_and_catch_crashes
    return hou.runCallbackAndCatchCrashes(lambda: c_function(*args))
RuntimeError: Traceback from crash:
pyHandleCrash
UT_Signal::UT_ComboSignalHandler::operator()(int, siginfo*, void*) const
UT_Signal::processSignal(int, siginfo*, void*)
_L_unlock_15 (??:0)
crash (crasher_Linux_x86_64_10.5.313_fpk9yUhcRwBJ8R5BAVtGwQ.C:9)
ffi_call_unix64+0x4c
ffi_call+0x214
_CallProc+0x352
<unknown>

Note that the Python portion of the traceback has the most recent call last followed by the exception, and the exception contains the C++ stack trace with the most recent call first.

The relevant line of the trace is crash (crasher_Linux_x86_64_10.5.313_fpk9yUhcRwBJ8R5BAVtGwQ.C:9), telling you to look at line 9 of $HOME/houdiniX.Y/inlinecpp/crasher_Linux_x86_64_10.5.313_fpk9yUhcRwBJ8R5BAVtGwQ.C. If you look at that file you’ll see something like:

#include <UT/UT_DSOVersion.h>
#include <HOM/HOM_Module.h>

extern "C" {

void crash()
{
    int *p = 0;
    *p = 3;
}

}

The statement on line 9 is a null pointer dereference.

By by default, crash handling is enabled if and only if debug=True. You can control whether crash handling is enabled independent of the debug setting by passing a value for the catch_crashes parameter.

Python scripting

Getting started

Next steps

Reference

  • hou

    Module containing all the sub-modules, classes, and functions to access Houdini.

Guru level

Python viewer states

Python viewer handles

Plugin types