Plug-In Architecture

From CockosWiki

Jump to: navigation, search

Contents

Introduction

This is getting better with time, and some thoughtful contributions.

Working Environment

You'll need REAPER, the SDK kit, and you'll need to download and try out the example C++ Plug-in from the site.

Please add details of configurations that you know work along with any details that might be useful to someone setting

O/SDevelopment environmentContributed by
Windows XP Home SP3Visual C++ 2008, Express Edition, .NET Framwork version 3.5 sp1MikeLacey
Windows XP Pro SP2Visual C++ 2008, Express Edition, Microsoft Platform SDK for Windows Server 2003 SP1 (required to get sample plugins working in SDK)Fingers

Building/Installation

Your plug-in must be built as a shared library (DLL). For REAPER to automatically load it its name must start with "reaper_" and it must be copied into REAPER's "Plugins" directory.

Required Code

reaper_plugin.h header which is found in the SDK contains all the required declarations for accessing the API. Another header file reaper_plugin_functions.h which is exported from reaper itself (filter by "c++" in the action list in reaper) contains all the reaper functions which you can get addresses to. The only header you need at this stage is reaper_plugin.h. The reaper_plugin_functions.h header is useful as a a reference or if your extension contains multiple source files. So all you need at the top of your source file is:

#include "reaper_plugin.h"

Next declare pointers to the functions you will be using. The easiest way to do this is to copy and paste from reaper_plugin_functions.h (without REAPERAPI_DECL)

MediaTrack *(*CSurf_TrackFromID)(int idx, bool mcpView);
bool (*GetSetTrackState)(MediaTrack* track, char* str, int maxlen);  
void (*FreeHeapPtr)(void* ptr); 

When Reaper loads your library from the plugins folder it calls the REAPER_PLUGIN_ENTRYPOINT function with a handle to yourself and a pointer to a reaper_plugin_info_t struct which is your interface for accessing Reaper. This function needs to be declared extern "C"; to stop your compiler from name mangling it.

A handy macro for registering function is below. You pass the name of the function you want to register as x and the name of the reaper_plugin_info_t struct pointer as y. It will set up the function pointer for the API function hiding all the gory details:

#define REG_FUNC(x,y) (*(void **)&x) = y->GetFunc(#x)

An example entrypoint function:

extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(REAPER_PLUGIN_HINSTANCE hInstance, reaper_plugin_info_t *rec)
{
        // This function is visible to, and will be called by, REAPER
        // So let's do some checking...
	if (rec)
        {
                // check the version numbers
		if (rec->caller_version != REAPER_PLUGIN_VERSION || !rec->GetFunc)
			return 0; // return an error if version numbers are different

		// Get the address of each API function that will be used
                /* Setting up your function pointers the ugly way */
		*((void **)&CSurf_TrackFromID) = rec->GetFunc("CSurf_TrackFromID");   
		*((void **)&FreeHeapPtr) = rec->GetFunc("FreeHeapPtr");

                /* Macro way, which is much easier and neater! */
                REG_FUNC(GetSetTrackState, rec);
		    
		// Check that the address of each API function has been set, not every version
                // of the API running out there will have the functions you need.
		if (
			!CSurf_TrackFromID ||
			!GetSetTrackState ||
			!FreeHeapPtr
		) return 0; // return an error if any address isn't set

                /* Initialization code goes here */

		return 1; // return success
	}
        else
        {
		// The plug-in is being unloaded. Do any cleanup necessary...
		return 0;
	}
} // REAPER_PLUGIN_ENTRYPOINT

User Interface

Usually a plug-in has some kind of user interface. In simple cases this will be one or more actions that can (optionally) be invoked via accelerator (aka shortcut) keys. Additionally those actions may have corresponding items in REAPER's menus. Regardless of whether an action shall be accessible via a menu item or not, the first step is to register it with REAPER.

Registering Actions

The first thing needed to register an action is a unique ID for it. This is an integer value by which one can later identify the action. Theoretically one could just pick an arbitrary number, but even when avoiding the range reserved for Cockos use (values < 0x10000000) there's no guarantee that another plug-in author doesn't choose the same number for one of her actions. So the best way is to request a unique ID from REAPER:

int fooCommandId = plugin_register("command_id", (void*)"SomeUniqueNameForTheAction");

The string given as the second argument should be a short but unique name consisting only of letters and digits (e.g. a concatenation of plug-in and action name). One can later ask REAPER to translate the ID back to that string, but just storing the ID for comparison is probably simpler.

Now, having a unique ID, the action can be registered with REAPER using the reaper_plugin_info_t's Register() function:

gaccel_register_t accelerator;
accelerator.accel.fVirt = FCONTROL | FALT | FVIRTKEY;
accelerator.accel.key = 'D';
accelerator.accel.cmd = fooCommandId;
accelerator.desc = "Perform foo on the selected tracks";
if (!rec->Register("gaccel", &accelerator)) {
	// registering the action failed ...
}

Note that the accel member of gaccel_register_t structure is Windows specific. Its fVirt and key fields specify the default accelerator key to be associated with the action (in this case Control-Alt-D). Set both to 0, if you don't wish to set a default (the user will still be able to set an accelerator key). The cmd field needs to be set to the ID of the action. The gaccel_register_t's desc should be assigned a short user-readable description of the action; it's the text shown in REAPER's action list.

Adding Menu Items

An item can be added to one of REAPER's menus only in a special menu hook function which can be registered in the plug-in initialization function:

if (!rec->Register("hookcustommenu", &customMenuHook)) {
	// registering the hook function failed ...
}

The implementation of the hook function could look like this:

static void customMenuHook(const char* menuString, HMENU menu, int flag)
{
	if (strcmp(menuString, "Track control panel context") == 0) {
		// the track context menu
		if (flag == 0) {
			// add our menu item
			int itemCount = GetMenuItemCount(menu);
			MENUITEMINFO itemInfo = {};
			itemInfo.cbSize = sizeof(itemInfo);
			itemInfo.fMask = MIIM_ID | MIIM_FTYPE | MIIM_STRING;
			itemInfo.wID = fooCommandId;
			itemInfo.fType = MFT_STRING;
			itemInfo.dwTypeData = "Perform Foo On Tracks";
			InsertMenuItem(menu, itemCount++, true, &itemInfo);
		}
	}
}

The menuString parameter of the function specifies which of the menus the hook is invoked for. In this case we're interested in the track context menu. The menu parameter is a handle for the menu object. flag specifies at which time the hook is invoked. A value of 0 means that this is right after the menu has been created (happens only once). A value of 1 means that the menu is about to be shown (happens each time the menu is opened by the user). In the sample implementation the item is added directly after the menu creation and it is not touched afterwards, which is just the thing to do unless you want to dynamically add/remove the item depending on the program/plug-in state.

The code actually adding the item is utterly Windows specific. We get the number of items already in that menu via GetMenuItemCount() and use it as the index at which to add our item via InsertMenuItem(). Note that we set the wID field to the ID of our action.

Executing Actions

Regardless of how an action is invoked (explicitly in REAPER's action list, via accelerator key, or via menu item) we can register a hook function which will be called when that happens:

if (!rec->Register("hookcommand", &commandFilter)) {
	// registering the hook function failed ...
}
static bool commandFilter(int commandId, int flag)
{
	if (commandId == fooCommandId) {
		// perform the action
		// ...

		// return true to indicate that this was our command
		return true;
	}

	// not our command
	return false;
}

The commandId parameter is the ID of the command that was invoked. To identify the action to be performed it can be compared to the ID registered earlier. If recognized, true shall be returned to indicate that the command has been processed and false otherwise.

There's another action-related hook function that is only of interest, when you have checkable (i.e. on/off) actions:

if (!rec->Register("toggleaction", &toggleActionHook)) {
	// registering the hook function failed ...
}
static int toggleActionHook(int commandId)
{
	if (commandId == fooCommandId) {
		// return 0, if not checked and 1, if checked
		return 0;
	}

	// not our command
	return -1;
}

Return 1 from the hook function for the respective action to be checked (the menu item will have a check mark), 0 for it to be unchecked, or -1, if the command is not one of yours.

Personal tools