Chapter 13. Plugins

breve's plugin architecture allows you to incorporate arbitrary code into a breve simulation. By loading external code into breve, you can add customized types of computation, bridges to other languages, connections to other input and output methods and much more.

Effective as of breve release 2.5, all plugins must be compiled with a C++ compiler. Plugins may still be written using C, but must be compiled with a C++ compiler to accomodate changes made to the plugin API.

Programming Experience Required

Building plugins for breve does require some programming experience in C/C++, plus access to and familiarity with the g++ compiler. Building plugins on Windows currently requires a POSIX environment such as MinGW or Cygwin.

The Plugin API: Writing Plugins

The breve Plugin API

In order to build plugins for breve, you'll need the breve plugin API which is included with all command-line distributions of breve. You'll also need a C compiler—the instructions here assume you're using GCC.

In addition to the documentation listed here, you should also look at the sample plugin files included with the breve distribution. These samples show how to build simple plugins for Mac OS X, Linux and Windows.

A number of the names and symbols have been changed from versions of the breve plugin API prior to version 2.0, though the older symbols should be preserved for backward compatibility. Still, you will most likely have to rebuild your plugin using the 2.0 header file to ensure continued compatibility.

In order to write plugins for breve, you'll need to follow a few simple steps.

  1. compose C wrapper functions around your external code (C or C++), the section called “Writing C Wrapper Functions Around Existing Code”

  2. create an "entry point function" in C which will load your functions into the breve engine the section called “Writing an Entry Point Function”

  3. write a class (or classes) to interface with your newly created functions

Writing C Wrapper Functions Around Existing Code

The first step in composing a breve plugin is to write wrapper functions around your existing code. The wrapper functions simply act as a bridge between the internal breve function calling code, and standard C function calls. When a function is called from within steve, the wrapper function is called. The wrapper function, in turn, calls the necessary C code and coordinates input and output between the C code and the breve call.

The wrapper function passes input and output data between breve and C using a structure called brEval. The brEval struct is a C data structure which is used internally to hold the values of expressions in steve. The structure is used to hold any and all types of steve expressions. So ints, lists, objects and the rest of the steve types are all held in brEval structs. The type field of the struct specifies the type of the expression. The values union of the struct contains the actual value of the expression. Information on how to use these fields is listed below.

Wrapper functions have the following prototype:

int function(brEval arguments[], brEval *result, void *instance);

Arguments are passed in as the arguments array of brEval objects. The function output is returned by setting the contents of the brEval object pointed to by result. instance is an internal pointer to the calling instance—this can be ignored.

Getting Values from the brEval Class

To access native C types stored in the brEval class, you'll need to use the following macros, which are defined in the header file distributed with the API.

  • BRINT(&eval), returns the int (C type int) contained in eval

  • BRDOUBLE(&eval), returns the float (C type double) contained in eval

  • BRVECTOR(&eval), returns the vector (C type slVector struct) contained in eval

  • BRMATRIX(&eval), returns the matrix (C type double [3][3]) contained in eval

  • BRBRRING(&eval), returns the string (C type char*) contained in eval

  • BROBJECT(&eval), returns the object (C type brInstance*) contained in eval

  • BRPOINTER(&eval), returns the pointer (C type void*) contained in eval

  • BRDATA(&eval), returns the data (C type brData*) contained in eval

  • BRHASH(&eval), returns the hash (C type brEvalHash*) contained in eval

  • BRLIST(&eval), returns the list (C type brEvalList*) contained in eval

Setting Values in the brEval Class

In order to set the result, use the overloaded method set() in the brEval class. This method can take any of the C types listed above and will set the brEval's type according to the input type. Some examples are shown below.

        // for a function returning an integer
        result->set( 10 );

        // for a function returning a string
        char *myString = "look at me, I'm returning a string!";
        result->set( myString );

In most cases, passing data to set() is as simple as in the examples above. However, care must be taken to ensure that the C-type being passed in corresponds to the desired brEval type. This may become an issue, for example, if a plugin calculation uses floating point math, but desires to return an integer, or when returning NULL pointers. It is therefore necessary to explicitly typecast values that do not match the expected return type. Some examples are shown below. In each example, the wrong type would be returned without the typecast.

        // our calculation creates a double, but we wish to return an AT_INT type to breve:
        result->set( (int)pow( x, y ) );

        // we want to return an AT_STRING type, but the value is NULL:
        result->set( (char*)NULL );

        // now we want to return an AT_LIST type, but the value is NULL:
        result->set( (brEvalListHead*)NULL );

Types in the brEval Class

The following list of constants specifies the types for plugin function input arguments and output types. The "AT" prefix stands for "atomic type".

  • AT_INT

  • AT_DOUBLE

  • AT_STRING

  • AT_VECTOR

  • AT_MATRIX

  • AT_DATA

  • AT_HASH

  • AT_LIST

  • AT_OBJECT

  • AT_POINTER

Your wrapper function should use these macros to extract data from the arguments array, and to store the result. The return value of your wrapper function should be EC_OK in the event of successful execution, or EC_ERROR in the event of a fatal error. Returning EC_ERROR will cause the simulation to stop, so you should generally not return this value. In many cases it is better to indicate the error using a special return value of the internal function (that is to say, by putting a special value in the "result" struct, not actually returning from your C code with a special value). You can then handle the error from within steve.

An Example Wrapper Function

As an example of a breve function wrapper around an existing function, imagine a function with the following prototype:

char *downloadURL(char *url, int timeout);

The wrapper function in breve will need to extract the url and timeout arguments from the arguments array, call the function, and store the resulting string in the structure pointed to by result. Here's how the wrapper function might look.

int breveDownloadURL(brEval *arguments, brEval *result, void *instance) {
        char *url, *urlData;
        int timeout;

        url = BRBRRING(&arguments[0]);
        timeout = BRINT(&arguments[1]);

        urlData = downloadURL(url, timeout);

        result->set( urlData );
                
        return EC_OK;
}

Writing an Entry Point Function

Your entry point function will be called when the plugin is loaded. Its job is to tell the breve engine what new steve functions to add, their names, and the arguments they will take.

The prototype for an entry-point function is:

void entryPointFunctionName(void *data);

The name may be anything you'd like, but it must be a unique symbol.

This entry-point function will be filled with one or more calls to the function brNewBreveCall. The calling convention for this function is:

brNewBreveCall(data, "functionName", cFunctionPointer, returnType, arg1, arg2, ..., 0);
  • The first argument, data, is the "data" pointer which gets passed in to the entry-point function.

  • The second argument, functionName, is the quoted function name as it will appear in steve.

  • The third argument, cFunctionPointer, is the unquoted name of the C function.

  • The fourth argument, returnType, is the return type (as a steve constant, listed in the previous section).

  • Subsequent arguments are the types of input arguments (as steve constants, listed in the previous section) that your steve function will expect, with the value 0 afterwards indicating the end of the parameter list.

  • The final argument, to follow all of the input types, must be 0.

For example, if you have a function which takes two vector inputs and produces an int output, your brNewBreveCall might look like this:

brNewBreveCall(data, "mySteveFunctionName", myCFunctionName, AT_INT, AT_VECTOR, AT_VECTOR, 0);

Interfacing With The New Functions

In order to write plugins for breve, you'll first need to familiarize yourself with a feature of steve which is generally hidden from users—the C-style function call.

C-style function calls in breve work just as they do in C: they take a number of arguments and may return a value. In breve, a C-style function call is used to access code which is built into the breve engine (as opposed to code written in steve). In fact, the built-in class hierarchy provided with breve uses C-style function calls extensively to interface with the breve engine.

From the user's perspective, all computation in breve happens within objects. So when we write a plugin, we'll also give it an object interface. Here's a simple example in which the plugin simply provides some data (like a float or an int) back to the caller.

Object : mySimplePluginObject {
        + to get-input-from-plugin:
                return getPluginInput().
}

By packaging this functionality inside an object, breve users look at it as they do any other object, without needing any information about how the plugin works underneath.

The more important reason to use objects, however, is so that the plugin can be used by more than one agent simultaneously. Imagine, for example, a plugin which simulates neural networks. It's easy to imagine that a breve simulation might want to use several of these neural networks at the same time. Because the neural networking code requires a "persistent state", we would need a way to store many distinct states simultaneously.

Inside our breve object, we'll hold a pointer to C-memory representing these distinct states. Whenever a neural network function is needed, we'll pass that pointer back to the plugin so that it can operate on the correct state. Here's an example:

Object : myNeuralNetwork {
        + variables:
                networkPointer (pointer).

        + to init:
                networkPointer = newNeuralNetwork().

        + to iterate:
                neuralNetworkIterate(networkPointer).

        + to get-output:
                return neuralNetworkOutput(networkPointer).

        + to set-input to value (double):
                neuralNetworkSet(networkPointer, value).
}