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.
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.
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.
compose C wrapper functions around your external code (C or C++), the section called “Writing C Wrapper Functions Around Existing Code”
create an "entry point function" in C which will load your functions into the breve engine the section called “Writing an Entry Point Function”
write a class (or classes) to interface with your newly created functions
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.
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(&
, returns the int (C
type int) contained in eval
)eval
BRDOUBLE(&
, returns the float
(C type double) contained in eval
)eval
BRVECTOR(&
, returns the vector
(C type slVector struct) contained in eval
)eval
BRMATRIX(&
, returns the matrix
(C type double [3][3]) contained in eval
)eval
BRBRRING(&
, returns the string
(C type char*) contained in eval
)eval
BROBJECT(&
, returns the object
(C type brInstance*) contained in eval
)eval
BRPOINTER(&
, returns the pointer
(C type void*) contained in eval
)eval
BRDATA(&
, returns the data (C
type brData*) contained in eval
)eval
BRHASH(&
, returns the hash (C
type brEvalHash*) contained in eval
)eval
BRLIST(&
, returns the list (C
type brEvalList*) contained in eval
)eval
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 );
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.
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; }
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);
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). }