Chapter 11. 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.

NoteProgramming Experience Required
 

Building plugins for breve does require some programming experience in C, plus access to and familiarity with the GCC compiler.

11.1. The Plugin API: Writing Plugins

NoteThe breve Plugin API
 

In order to build plugins for breve, you'll need to download the plugin API from the breve website. 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.

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++), Section 11.1.1

  2. create an "entry point function" in C which will load your functions into the breve engine Section 11.1.2

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

11.1.1. 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 stEval. The stEval 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 stEval 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(stEval arguments[], stEval *result, void *instance);
Arguments are passed in as the arguments array of stEval structs. The function output is returned by setting the contents of the stEval structure 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 stEval struct, you'll need to use the following macros, which are defined in the header file distributed with the API.

  • STINT(&eval), returns the int (int) contained in eval

  • STDOUBLE(&eval), returns the float (double) contained in eval

  • STVECTOR(&eval), returns the vector (slVector) contained in eval

  • STMATRIX(&eval), returns the matrix (double [3][3]) contained in eval

  • STSTRING(&eval), returns the string (char*) contained in eval

  • STOBJECT(&eval), returns the object (stInstance*) contained in eval

  • STPOINTER(&eval), returns the pointer (void*) contained in eval

  • STDATA(&eval), returns the data (stData*) contained in eval

  • STHASH(&eval), returns the hash (stEvalHash*) contained in eval

  • STLIST(&eval), returns the list (stEvalList*) contained in eval

These macros correspond to steve constants representing types. You'll need these constants in the next section when defining the inputs and outputs your functions will take.

  • ST_INT

  • ST_DOUBLE

  • ST_STRING

  • ST_VECTOR

  • ST_MATRIX

  • ST_DATA

  • ST_HASH

  • ST_LIST

  • ST_OBJECT

  • ST_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 sucessful 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, buy 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(stEval *arguments, stEval *result, void *instance) {
	char *url;
	int timeout;

	url = STSTRING(&arguments[0]);
	timeout = STINT(&arguments[1]);

	STSTRING(result) = downloadURL(url, timeout);
		
	return EC_OK;
}

11.1.2. 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 stNewSteveCall. The calling convention for this function is:
stNewSteveCall(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 stNewSteveCall might look like this:
stNewSteveCall(data, "mySteveFunctionName", myCFunctionName, AT_INT, AT_VECTOR, AT_VECTOR, 0);

11.1.3. 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 in to 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 is 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).
}