Event Driven Cpp Plug in Framework

alvarofcgm edited this page Oct 23, 2016 · 16 revisions

Event-Driven C++ Plug-in Framework

Index

An Introduction to the Event-Driven C++ Plug-in Framework

What is presented here is an implementation of a server side plug-in framework in C++ using JavaScript as script language. It is built on top of Microsoft C++ REST SDK and uses Mozilla's JavaScript engine Spider Monkey as JavaScript interpreter.

This is an event-driven framework: Plug-ins listen to events. When an event is fired by a remote client or by the server application the plug-ins listening to that event run and return a response. The response is returned to the client or the server application. Plug-ins can communicate. And even if the logic is based on events, a plug-in can be run using its id. Plug-ins can extend other plug-ins.

Requirements

Example code

- C++ Plug-in Server Example

Use CMake to build the project. After doing a “make” two executables will be created in the same folder where the server.conf file is: The "plugin-server" executable will use maps for data storage and the other executable "plugin-server-redis" will use Redis as data storage system.

In the CMakeList.txt of the example (Release/samples/granada/plugin-server/CMakeLists.txt) substitute the include_directories and link_directories paths by the real paths to your mozjs include and lib.

You will have to have something like this:

include_directories(/opt/mozjs-38.0.0/js/src/build_OPT.OBJ/dist/include)
link_directories(/opt/mozjs-38.0.0/js/src/build_OPT.OBJ/dist/lib)

After executing the server use a browser and type the URL: http://localhost to use the client application. You can configure the server modifying the “server.conf” file and restarting the server.

Class Reference

Class Reference

Elements of the framework

Plug-ins

A plug-in is an object with a defined structure that extends an application. Plug-ins can be added from different sources, server side applications, or loaded from files, we call these last plug-ins packaged plug-ins. Server side plug-ins are also portable and can be customized through properties. They can also extend other plug-ins. Not all of them may be executed, some can just have useful functions or wait to be extended by others. They can communicate.”

Plug-in Handler

The Plug-in Handler manages the life-cycle of multiple plug-ins: It Loads plug-ins, runs plug-ins and removes plug-ins. It also manages the communication between plug-ins.

Plug-in Factory

Used to create Plug-ins and Plug-in Handlers.

Plug-in Controller

Used to communicate plug-ins with remote clients via HTTP requests. JSON is used as data format. Client can remotely fire events, run server plug-ins, send messages to server plug-ins and even kill and reset the Plug-in Handler. Session tokens are used to link clients with a Plug-in Handlers.

Plug-ins

Creating and adding plug-ins on the fly

A Plug-in is composed of a header, a configuration and a script.

header: The header is a JSON object containing information about the plug-in such as its id, the events the plug-in will listen to, the plug-ins the plug-in extends from, if the plug-in is active or not and the way the plug-in is going to be loaded, none of these properties is mandatory.

Example of header of math.sum plug-in:

{
  "id": "math.sum",
  "events": [
    "calculate"
  ],
  "extends": [
    "math.calculus"
  ],
  "active": true,
  "loader": {
    "events": [
      "init-ph-after"
    ]
  }
}
  • id: Unique plug-in identifier. If not specified an id will be assigned.
  • events: A list of events the plug-in listens to. When one of the event is fired, the plug-ins listening to that event are run. If no events are provided plug-in will run when added, and in case of being in a repository, it won't run unless it is called by its id.
  • extends: A list with the ids of the plug-ins the plug-in extends. A plug-in can extend more than one plug-in. The plug-in that extends will integrate the public members of the extended plug-in that haven't been overridden. Once a plug-in is extended it remains inactive, other plug-ins can continue to extend it but it will not be executed again, we are supposing its extensions will do the job.
  • active: True if the plug-in is active and can be run, false if it is not. If not provided plug-in is active by default.
  • loader: The way a plug-in will be loaded. If an event list is provided the plug-in will be loaded the first time one of the events in the list is fired. if "load":"eager" is specified, the plug-in will load now and if no loader is provided the plug-in will be loaded the first time one of the events the plug-in is listening to is fired (if the events field is missing it will be loaded on Plug-in Handler initialization).

configuration: Configuration is a JSON object shared by all the plug-ins in the package (client plug-ins also) that contains constants for configuring plug-ins.

Example of configuration:

{"phi":{"value":1.61803,"editor":"number"},"pi":{"value":3.14159,"editor":"number"}}

Configuration does not have to have this structure as long as it is a JSON object, but the provided structure can help if using a dynamic plug-in configuration editor to edit plug-ins configurations.

script: Script is the code interpreted when running a plug-in. Can be a code or a path to a code, it depends, it can even be a path to an executable. If you are using JavaScript plug-ins it will take the value of a JavaScript code in form of string.

Example of JavaScript script:

{
  name : "sum",
  sum : function(a,b){
    return parseInt(a)+parseInt(b);
  },
  run : function(params,eventName){
    var result;
    var message = {};
    sendMessage({"message":"hi"},["math.square","math.multiplication"],function(data){
      message = data;
    });
    result = null;
    if (params["addend1"] && params["addend2"]){
      result = this.sum(params["addend1"],params["addend2"]);
    }
    var oldValue = getValue("sum");
    var response = "{\"configurationtest\":" + JSON.stringify(getConfiguration()) + ",\"result\":" + result + ", \"message\":" + JSON.stringify(message) + ", \"old_value\":\"" + oldValue + "\"}";
    setValue("sum",""+result);
    return response;
  }
}

Notice how the script contains a run function that is going to be executed when running the plug-in. The returned value is a string.

Also notice how in the run function a sendMessage function is called to communicate with other plug-ins as well as other functions such as setValue, getValue or getConfiguration. These functions and others will be explained later.

When running a plug-in in the server side a runner object is used to run the plug-in script. Runners are classes that inherit from the Runner interface, they run scripts or executables and return a response.

code:

#include "granada/plugin/map_spidermonkey_plugin.h"

...

// create a Plug-in Handler to manage the plug-ins.
granada::plugin::MapSpidermonkeyPluginHandler plugin_handler("SspnxI8YJ3Nk6ZPJ1gyol5Ng");


// empty plug-in without id.
granada::plugin::MapSpidermonkeyPlugin plugin();


// header, configuration and script
const web::json::value& header = ...;
const web::json::value& configuration = ...;
const std::string& script = ...;


// instantiate a plug-in and assign its alues after
granada::plugin::MapSpidermonkeyPlugin plugin2(plugin_handler,"math.sum");
plugin2.SetHeader(header);
plugin2.SetScript(script);
plugin2.SetConfiguration(configuration);


// assign values on instatiation, id will be taken from *header*.
granada::plugin::MapSpidermonkeyPlugin plugin3(plugin_handler,header,configuration,script);


// creating a plug-in with a plug-in factory
granada::plugin::MapSpidermonkeyPluginFactory plugin_factory();
const std::shared_map<granada::plugin::Plugin>& plugin4 = plugin_factory.Plugin(plugin_handler,header,configuration,script);


// running a plug-in without adding it. Other plug-ins will not be able to interact with it.
// And we will not be able to retrieve the plug-in later using its id.
web::json::value parameters = web::json::value::parse("{\"adend1\":\"8\",\"addend2\":\"3\"}");
plugin_handler.run(plugin4,parameters,"",[](const web::json::value& data){
     // success callback
     if (data.has_field("result") && data.at("result").is_integer() ){
     	std::cout << "sum result: " << data.at("result").as_integer() << "\n";
     }
},[](const web::json::value& data){
     // failure callback
});


// adding a plug-in, other plug-ins will be able to interact with it.
// We will be able to retrieve the plug-in later using its id.
plugin_handler.Add(plugin4);

// once added we can run the plug-in in any moment using its id.
plugin_handler.Run("math.sum",parameters,[](const web::json::value& data){
     // success callback
     if (data.has_field("result") && data.at("result").is_integer() ){
     	std::cout << "sum result: " << data.at("result").as_integer() << "\n";
     }
},[](const web::json::value& data){
     // failure callback
});

Plug-in repositories

So far we have seen how to create a plug-in and how to add it and run it on the fly using a Plug-in Handler, but what if we want to have a persistent plug-in repository and load the plug-ins of this repository dynamically and automatically without worrying about instantiating them?

Plug-ins can be loaded from repositories. Repositories are just directories containing packages of plug-ins.

Example of plug-in repository with two packages of plug-ins, math and transformation:

plugins
+-- math
|   +-- client
|   +-- server
|   |   +-- calculus.js
|   |   +-- calculus.json
|   |   +-- multiplication.js
|   |   +-- multiplication.json
|   |   +-- square.js
|   |   +-- square.json
|   |   +-- sum.js
|   |   +-- sum.json
|   +-- configuration.json
|   +-- manifest.json
+-- transformation
     +-- client
     +-- server
      |  +-- reflection.js
      |  +-- reflection.json
      |  +-- rotation.js
      |  +-- rotation.json
      |  +-- translation.js
      |  +-- translation.json
      +-- configuration.json
      +-- manifest.json

As said before a plug-in usually contains a header a configuration and a script. In the above tree, the headers of the plug-ins are in the ".json" files, the scripts in the ".js" files and the configurations are in the "configuration.json" files.

Read the next section about the Plug-in Handler to understand how to load plug-ins from repositories.

Plug-in Class Reference: granada::plugin::Plugin

The Plug-in Handler

Init, Stop and Reset

Initializing a Plug-in Handler and pre-loading the plug-ins in the repositories:

// create a vector of strings containing the paths of the repositories.
std::vector<std::string> repositories_paths;
repositories_paths.push_back("/home/my_repo/plugins");
repositories_paths.push_back("/home/my_repo_2/plugins");

// instantiate a Plug-in Handler to manage the plug-ins.
granada::plugin::MapSpidermonkeyPluginHandler plugin_handler("SspnxI8YJ3Nk6ZPJ1gyol5Ng");

// Initialize the Plug-in Handler, the plug-ins in the repositories will be pre-loaded.
plugin_handler.Init(repositories_paths);

Stopping a Plug-in Handler will "unload" the plug-ins and remove all data related to them in memory (or cache) as well as removing all data related to the Plug-in Handler:

plugin_handler.Stop();

Reseting a Plug-in Handler is the same as stopping it and initializing it again:

plugin_handler.Reset();

In order to consider a new added repository plug-in we need to reset the Plug-in Handler.

Load events: Loading and adding plug-ins on events

When a Plug-in Handler initializes, it does not physically loads the plug-ins of the repositories to the memory, thing that will have a cost, to prevent this from happening on Plug-in Handler initialization plug-ins of the repositories are pre-loaded: only their header and the paths to their configuration and to their script is cached in memory. Loading and adding a plug-in means that the content of the configuration and script files is cached in memory (or database or cache support) and that the extensions that involve the plug-in are applied. After being added a plug-in is able to run on events and can communicate with others.

Loading and adding Cpp plug-ins

Adding plug-ins from repositories

Normally the plug-ins in the repositories have a loader in their header so the job of adding load events is automatically done when initializing the Plug-in Handler. Only plug-ins that have been "pre-loaded", repository plug-ins, can be loaded. The other plug-ins should be added directly.

Make a plug-in load and be added when an event is fired:

plugin_handler.AddLoadEvent ("calculate-multiplication","math.multiplication",plugin_loader);
// or:
plugin_handler.AddLoadEvent ("calculate-multiplication","math.multiplication",header);

This means the plug-in "math.multiplication" will be loaded and added and thus able to be run and to communicate when the event "calculate-multiplication" is fired. A plug-in can be loaded on different events, but it will be loaded only once: the first time one of the events is fired.

To remove a load event use:

plugin_handler.RemoveLoadEvent("calculate-multiplication");

Retrieving plug-ins

std::shared_ptr<granada::plugin::Plugin> plugin = plugin_handler.GetPluginById("math.sum");

Events: add event listener, fire event, remove event listeners

Make a plug-in listen to an event:

// the three plug-ins will run on "calculate" event.
plugin_handler.AddEventListener("calculate", "math.sum")
plugin_handler.AddEventListener("calculate", "math.multiplication")
plugin_handler.AddEventListener("calculate", "math.square")

At this point we can fire events that will load and run plug-ins:

web::json::value parameters = web::json::value::parse("{\"number\":\"4\",\"adend1\":\"4\",\"addend2\":\"25\",\"factor1\":\"4\",\"factor2\":\"2\"}");

plugin_handler.Fire("calculate",parameters,[](const web::json::value& data){
     // success callback
     // data will contain the responses of all the plug-ins listening to the
     // "calculate" event in form of a JSON object.
     std::cout << "sum result: " << data.serialize() << "\n";
},[](const web::json::value& data){
     // failure callback
});

To follow the example the "calculate" event will run all the "math" plug-ins of our repository: "math.sum", "math.multiplication" and "math.square" plug-ins. The obtained response will be a JSON object containing the responses of the three plug-ins:

{
  "data": {
    "math.multiplication": {
      "data": {
        "old_value": "2396",
        "result": 8
      }
    },
    "math.square": {
      "data": {
        "old_value": "1435204",
        "result": 16
      }
    },
    "math.sum": {
      "data": {
        "message": {
          "data": {
            "math.multiplication": {
              "data": {
                "response": "Hello I'm the multiplication plug-in!!!"
              }
            },
            "math.square": {
              "data": {
                "response": "Hello I'm the square plug-in!!!"
              }
            }
          }
        },
        "old_value": "1223",
        "result": 29
      }
    }
  }
}

After a Plug-in Handler is initialized, it fires the event: "init-ph-after".

Each time a plug-in runs three events are fired: "plugin.id-before", "plugin.id-in-process" and "plugin.id-after". Other plug-ins could be listening at them.

Other events are fired by the Plug-in Handler or the Plug-ins, these events are called native events.

List of native events:

Event name Description
init-ph-after Fired after PH is initialized.
plugin-run-failure, plugin.id-run-failure Fired when plug-in execution has failed.
plugin-configuration-load-before, plugin-configuration-load-after, plugin.id-configuration-load-before, plugin.id-configuration-load-after Fired after a plug-in configuration has been loaded.
plugin-add-after, plugin.id-add-after Fired after a plug-in is added to the Plug-in Handler.
plugin-remove-before, plugin-remove-after, plugin.id-remove-before, plugin.id-remove-after Fired before/after a plug-in is removed.
plugin.id-before, plugin.id-in-process and plugin.id-after When a plug-in is executed tree events are fired, before the run, during and after id of the plug-in + -before/-in-process/-after.

C++ Plug-in framework basic flows

Firing events

Remove event listeners:

// prevent "math.sum" to listen to "calculate-sum" event.
plugin_handler.RemoveEventListener("calculate-sum", "math.sum");
 
// prevent "math.sum" to listen to any event.
plugin_handler.RemoveEventListeners("math.sum")

// another way to prevent "math.sum" to listen to any event.
const std::shared_ptr<granada::plugin::Plugin>& math_sum_plugin = plugin_handler.GetPluginById("math.sum");
if (math_sum_plugin->GetScript().empty()){
	plugin_handler.RemoveEventListeners(math_sum_plugin);
}

Sending messages to plug-ins

Plug-ins can send and receive messages.

// build a list with the destination ids. If the array is empty,
// message will be sent to ALL added plug-ins.
web::json::value to_ids = web::json::value::array(2);
to_ids[0] = web::json::value::string("math.multiplication");
to_ids[1] = web::json::value::string("math.square");

// message to pass to the plug-ins, a JSON object
web::json::value message = web::json::value::object();
message["readme"] = web::json::value::string("Hi, I'm the sum plug-in, who are you?");

// send message passing the from, to and message
web::json::value responses = plugin_handler.SendMessage("math.sum",to_ids,message);

// send a message to all plug-ins:
to_ids = web::json::value::array();
responses = plugin_handler.SendMessage("math.sum",to_ids,message);

Be careful, If the to_ids array is left empty the message will be sent to ALL added plug-ins!

This is what the responses will look like:

// if "math.multiplication" and "math.square" plug-ins have not yet been added:
{"data":null}

// if they have been loaded and added to the Plug-in Handler:
{
  "data": {
    "math.multiplication": {
      "data": {
        "response": "Hello I'm the multiplication plug-in!!!"
      }
    },
    "math.square": {
      "data": {
        "response": "Hello I'm the square plug-in!!!"
      }
    }
  }
}

Remove Plug-ins

Remove plug-in using its id:

plugin_handler.Remove("math.sum");

Managing plug-in run failure

A plug-in will be completely removed by the Plug-in Handler when its execution fails. After that it won't be able to be executed nor extended again.

Plug-in Handler properties configuration

Plug-in Handler can be configured writing properties in the server.conf file.

Property name Description Default value
plugin_bytes_limit Maximum bytes a Plug-in Hadler can load. 10000000
plugin_runner_use_frequency_limit The minimum time the plug-in handler has to wait between two script runs in milliseconds 20
plugin_handler_use_frequency_limit The minimum time in milliseconds that has to pass between two uses of the same Plug-in Handler 0
plugin_allow_client_to_fire_events True if clients are allowed to fire server plug-in events remotely via HTTP requests. False if not. TRUE
plugin_allow_client_to_run_plugins True if clients are allowed to run serer plug-in remotely via HTTP requests. False if not. TRUE
plugin_allow_client_to_send_messages True if clients are allowed to send messages to server plug-ins remotely via HTTP requests. False if not. TRUE
plugin_allow_client_to_run_commands True if clients are allowed to run commands such as STOP or RESET Plug-in Handler remotely via HTTP requests. False if not. TRUE
plugin_publicfiles_directory Where the public plug-ins repositories are. publicfiles/plugins
plugin_userfiles_directory Where the private repositories are. {{user.directory}} will be replaced by the value of "user.directory" of the session "plugin.select" role {{user.directory}}/plugins

To override a property just write in the server.conf file:

plugin_bytes_limit=5000000
plugin_allow_client_to_fire_events=FALSE

Example of server.conf file here

Plug-in Handler Class Reference: granada::plugin::PluginHandler

Linking a Plug-in Handler with a client session

If we have a Plug-in Handler shared by all users, that may cause a big problem, because some users have their own plug-in repositories they don't want to share, and having the plug-ins data accessible and modifiable by all users is something we don't want. That is why we need to limit the use of a Plug-in Handler to a user or to a client session. In the provided code, the Plug-in Handler is linked to a session, linking it to a user can be also dangerous as the Plug-in Handler will be shared by all the sessions of the user and so will the plug-ins state.

Using the session token as Plug-in Handler id.

As it is an unique not guessable alphanumeric string we can think of using the session token as a Plug-in Handler id.

// retrieve client session
const std::shared_ptr<granada::http::session::Session>& session = session_checkpoint_->check(request,response);
session->Update();

// create a plug-in handler, linked to the session throw the session token.
const std::shared_ptr<granada::plugin::PluginHandler>& plugin_handler = plugin_factory_->PluginHandler(session->GetToken());

Killing the Plug-in Handler when closing a session

We have explained that the plug-ins data is cached, what happens if the session timeouts? It will happen that the Plug-in data cached will remain there as garbage. To prevent this from happening we need to kill the Plug-in Handler on session close.

// kill the plug-in handler on session close.
const std::shared_ptr<granada::http::session::Session>& session = session_checkpoint_->check();
if (!session->close_callbacks()->Has(default_strings::plugin_function_stop_plugin_handler)){
     const std::shared_ptr<granada::plugin::PluginFactory>& plugin_factory = plugin_factory_;
     session->close_callbacks()->Add(default_strings::plugin_function_stop_plugin_handler,[plugin_factory](const web::json::value& data){
          if (data.has_field(entity_keys::session_token)){
               const web::json::value& token = data.at(entity_keys::session_token);
               if (token.is_string()){
                    const std::shared_ptr<granada::plugin::PluginHandler>& plugin_handler = plugin_factory->PluginHandler(token.as_string());
                    plugin_handler->Stop();
               }
          }
          return web::json::value::object();
     });
}

Notice the close_callbacks are shared by ALL sessions, so this callback should be added only once and its code has to be generic.

Plug-in Controller: Remote Plug-in Run

Firing plug-in events, running plug-ins, sending messages to plug-ins can be done remotely via HTTP requests.

For interfacing with remote clients the plug-in framework will use a granada::http::controller::PluginController.

Instantiating the controller:

// session checkpoint used to create sessions.
std::shared_ptr<granada::http::session::Checkpoint> session_checkpoint(new granada::http::session::SimpleSessionCheckpoint());

// factory used to create Plug-in Handlers and Plug-ins
std::shared_ptr<granada::plugin::PluginFactory> plugin_factory(new granada::plugin::MapSpidermonkeyPluginFactory());

uri_builder plugin_uri("http://localhost:80");
plugin_uri.append_path(U("plugin"));
auto addr = plugin_uri.to_uri().to_string();
std::unique_ptr<granada::http::controller::PluginController> plugin_controller(new granada::http::controller::PluginController(addr,session_checkpoint,plugin_factory));
plugin_controller->open().wait();

Notice how we pass a Session Checkpoint and a Plug-in Factory to the controller so we can further instantiate a given type of sessions and a given type of plug-ins in a generic way, benefiting from polymorphism.

You have an example of Plug-in Controller code here.

JSON format is used to communicate with the server.

List of requests that can be done to the server:

Fire an event:

method: POST

url: /plugin

JSON data: {"event" : "calculate", "parameters" : {"number" : "4", "adend1" : "4", "addend2" : "25", "factor1" : "4", "factor2" : "2"}}

Run a plug-in:

method: POST

url: /plugin

JSON data: {"plugin_id" : "math.sum", "parameters" : {"adend1" : "4", "addend2" : "25"}}

Send messages to plug-ins:

method: POST

url: /plugin

JSON data: {"from" : "client", "to_ids" : ["math.sum","math.multiplication"], "parameters" : {"param1":"hi"}}

Another example of JSON data, to send messages to ALL plug-ins: {"from" : "client", "to_ids" : [], "parameters" : {"param1":"hi"}}

Stop the Plug-in Handler:

method: POST

url: /plugin

JSON data:{"command":"stop"}

Reset the Plug-in Handler:

method: POST

url: /plugin

JSON data:{"command":"reset"}

Plug-in Controller Class Reference: granada::http::controller::PluginController

JavaScript Server Plug-ins

run function

The run function is a function all runnable plug-ins should have, as parameters it accepts a JSON object containing parameters and an eventName parameter corresponding to the name of the event that has provoked the plug-in to run (if an event has not provoked the plug-in to run the value will be null). There are plug-ins that don't have a run function, for example plug-ins which purpose is to be extended.

Example of "math.multiplication" plug-in with a run function:

{
  name : "multiplication",
  multiplication : function(a,b){
    return a*b;
  },
  run : function(params, eventName){
    if (params["factor1"] && params["factor2"]){
      result = this.multiplication(parseInt(params["factor1"]),parseInt(params["factor2"]));
    }
    return "{\"result\":" + result + ", \"message\":" + JSON.stringify(message) + ", \"old_value\":\"" + oldValue + "\"}";
  }
}

Example of "math.calculus" plug-in with no run function:

{
  name : "calculus",
  onMessage : function(message,_from){
    return {"response":"Hello I'm the " + this.name + " plug-in!!!"};
  }
}

Guess what: "math.multiplication" extends "math.calculus".

onMessage function

To receive messages a JavaScript plug-in must have an "onMessage" function.

{
  run : function(params, eventName){
    return "{\"result\":\"3.14159\"}";
  },
  onMessage : function(message,_from){
    return {"response":"Hello I'm the Pi plug-in!!! and you are " + _from};
  }
}

Special JavaScript functions to interact with the C++ Plug-in Handler

List of functions that can be called from the plug-in JavaScript:

fire:

fire(eventName,params,success,failure);

Description: Fires an event. All plug-ins listening to that event will run.

Example:

fire("multiply",{"factor1":"3","factor2":"2"},function(data){
     // success callback
},function(data){
     // failure callback
});

run:

runPlugin(id,params,success,failure);

Description: Runs a plug-in.

Example:

runPlugin("math.multiplication",{"factor1":"3","factor2":"2"},function(data){
     // success callback
},function(data){
     // failure callback
});

remove:

remove(pluginId);

Description: Removes a plug-in with given id.

remove("math.multiplication");

removeEvents:

removeEvents();

Description: Removes all the events the plug-in is listening to.

sendMessage:

sendMessage(params, toIds, callback);

Description: Sends a message to other plug-in

Example:

sendMessage({"message":"hi"},["math.square","math.multiplication"],function(data){
      // data contains the responses to the message.
});

setValue, getValue, destroyValue and clearValues:

setValue(key,value);

getValue(key);

destroyValue(key);

clearValues();

Description: This four functions are used to persist plug-in values between its runs.

Example:

// the plug-in runs for the first time and sets a value.
setValue("count","0");

// each time the plug-in runs it increases the value by 1.
var count = getValue("count");
count = "" + (parseInt(count)+1);
setValue("count",count);

// once "count" is greater than 1000 the plug-in erases the value of "average"
var count = getValue("count");
if (parseInt(count)>1000){
    destroyValue("average");
}

// and once "count" is greater than 100000 the plug-in erases all the plug-in stored values:
if (parseInt(count)>100000){
    clearValues();
}

Other JavaScript members

__PLUGIN_ID:

__PLUGIN_ID

Description: Contains the id of the plug-in.

__PLUGIN_HANDLER_ID:

__PLUGIN_HANDLER_ID

Description: Contains the id of the Plug-in Handler of the plug-in.

getConfiguration function:

getConfiguration();

Description: Returns the plug-in configuration JSON.

call function:

call(name,params);

Description: Calls a function of the plug-in with the name "name" and pass it parameters.

Example:

{
	"result" : null,
	"done-with-application-event" : function(params){
		// remove this plug-in on "done-with-application-event" event.
		remove(__PLUGIN_ID);
	},
	"multiply" : function(params){
		if (params["factor1"] && params["factor2"]){
			this.result = parseInt(params["factor1"])*parseInt(params["factor2"]);
		}
  	},
  	"sum" : function(params){
    		if (params["addend1"] && params["addend2"]){
			this.result = parseInt(params["addend1"]) + parseInt(params["addend2"]);
		}
  	},
	"run" : function(params,eventName){
		call(eventName,params);
		return this.result;
	}
}

Extending JavaScript plug-ins

A plug-in can extend more than one plug-in, that means it will integrate the public members of the extended plug-in that it has not overridden.

Once a plug-in is extended he remains inactive, other plug-ins can continue to extend it but it will not be executed again.

Example of a plug-in that extends another:

////
// "math.calculus" plug-in
////

// header of the plug-in "math.calculus"
{"id" : "math.calculus", "extends":["math.base"]}

// script of the plug-in "math.calculus"
{
  name : "calculus",
  onMessage : function(message,_from){
    return {"response":"Hello I'm the " + this.name + " plug-in!!!"};
  }
}

////
// "math.multiplication" plug-in
////

// header of the plug-in "math.multiplication"
{"id" : "math.multiplication","events" : ["calculate"], "extends" : ["math.calculus"]}

// script of the plug-in "math.multiplication"
{
  name : "multiplication",
  multiplication : function(a,b){
    return a*b;
  },
  run : function(params, eventName){
    if (params["factor1"] && params["factor2"]){
      result = this.multiplication(parseInt(params["factor1"]),parseInt(params["factor2"]));
    }
    return "{\"result\":" + result + ", \"message\":" + JSON.stringify(message) + ", \"old_value\":\"" + oldValue + "\"}";
  }
}

The second plug-in extends the first one, so the first one once extended will never be run again, the second will take the members that it does not override.

Once the extension is done, the second plug-in will look like this:

////
// "math.multiplication" plug-in
////

// header of the plug-in "math.multiplication"
{"id" : "math.multiplication","events" : ["calculate"], "extends" : ["math.base","math.calculus"]}

// script of the plug-in "math.multiplication"
{
  name : "multiplication",
  multiplication : function(a,b){
    return a*b;
  },
  run : function(params, eventName){
    if (params["factor1"] && params["factor2"]){
      result = this.multiplication(parseInt(params["factor1"]),parseInt(params["factor2"]));
    }
    return "{\"result\":" + result + ", \"message\":" + JSON.stringify(message) + ", \"old_value\":\"" + oldValue + "\"}";
  },
  onMessage : function(message,_from){
    return {"response":"Hello I'm the " + this.name + " plug-in!!!"};
  }
}

Notice how the "extends" of the first plug-in and second have been merged and how the "onMessage" function is added to the second plug-in. But the "name" member, as it is overridden in the second plug-in is not added.

Extending plug-ins has a cost, it is recommended to use extension the less possible. It has been implemented more in the spirit of overriding functions than in the spirit of inheritance.

Allowing extension to work without the object it extends has its advantages. It weakens plug-in dependency, making the architecture a lot more flexible. Even if extension has not its extended it can still be used. And until its extension is added the extended is fully functional. We can extend up to a point at a certain time.

Conclusions

  • Plug-ins can be very useful for data transformation. For overriding existing processes or implementing specific users scripts.

  • Using JavaScript as the plug-ins script language was the chosen solution as it is well known, access to files or database can be easily controlled, the code runs in a specific context and exceptions are managed. It is portable. The community is huge and the engines more used are developed by Mozilla (Spidermonkey) and Google (V8), thing that ensures maintenance. Plus both are written in C++.

  • Even if JavaScript was used as the plug-ins scripting language the architecture described here allows to integrate other scripting languages, or even compiled code. For doing that a proper runner has to be created and used. The idea of using scripts for C++ or languages like Lisp was rejected because of the multiple variety of languages, not homogenized, not well known or difficult to learn and sometimes with a small community or with an integration with C++ not trusted to be maintained. The possibility of using compiled plug-ins was also on the table but even if it could be good in terms of performance the plug-ins needed to be compiled in each server so they could be executed to ensure portability. There could also be security issues as the plug-ins can be developed by third party developers, errors in the code may cause the server to crash and it could be very difficult to track responsibilities.

  • Performance: The cost of using plug-ins is very high, it multiplies at least by 4 the execution time, without talking about the memory consumption. Certainly an effort has to be done to outperform the current plug-in execution performance, but consider that this framework is not intended to build an application only based on plug-ins. Use plug-ins with care, for punctual things that require them. Communication between plug-ins also has a big cost as well as the abuse of extensions.

  • As said in the text, extensiveness has been implemented more in the spirit of overriding functions than in the spirit of inheritance.

  • Somehow there should be a control of quality of shared plug-ins because of their possibility of propagating quickly. They could damage the application execution, or worse damage or compromise user data. In C++ code we should care about restricting Plug-in Handler and the script runners use.

  • The proposed architecture has the advantages and disadvantages of Loose coupling, it has the advantage of being very flexible, the entities firing events and the plug-ins don't have the obligation of knowing each other. This is also a disadvantage, the implementation of such a structure could soon become a huge mess. Changing the id of a plug-in, its events or its data structure can have big consequences in other dependent plug-ins. Apart of limiting the dependency of plug-ins it is a good practice to standardize the naming of the ids, events and the data structure.

    The proposed naming for plug-in ids is: "package.plugin-id". And for events is : "event-name-1".

    Event naming is more important than id naming. Giving to much specificity to a name event can be a problem for plug-in re-use. In my opinion events should be generic enough, this could be a problem if two different entities fire the same event with different meaning, but it has also the advantage that one plug-in can be used when two different sources fire the same event with the same meaning. For example if an event is named “text-load-after”, it can be fired in more than one situation, the event name does not specify what text or where the text is loaded, this information will be specified in the parameters, with this method one plug-in can be used for many different text loads.

  • Packaged plug-ins (the ones in repositories) have all the same file structure, they have a manifest.json file that describes them and a directory where the code is. We have to think carefully when making a packaged plug-in, as said the purpose is to share it so other users can import them and use them in different applications. Also we have to think that other users may want to extend the plug-in.

  • Plug-ins internationalization: How do we translate texts used in our plug-ins? I suggest using plug-in configuration JSON. This solution is nice if there is little amount of translation data but in exchange we will be loading unused translations. An effort should be made to solve this problem in future releases. I recommend reading chrome.i18n and RequireJS - Define an I18N Bundle.

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.