Crate gc_plugin_abi
Expand description
This crate provides a safe ABI with minimal overhead to interact with the core gateway. Every plugin must import this crate to interact with the core gateway.
To create a Rust plugin, the user is just responsible for defining a struct that implements the GCPluginInstance trait and also includes the gc_plugin macro. The following is the simplest example of a working minimal plugin.
use gc_plugin_abi::{GCDatapointValue, GCBorrowedDatapointValue, GCPluginInfo, GCPluginInterface, GCPluginInstance, gc_plugin};
use serde_json::Value;
#[derive(schemars::JsonSchema)]
pub struct DummyConfig {}
#[gc_plugin]
pub struct ExamplePlugin<'a> {
plugin_interface: &'a GCPluginInterface,
}
impl<'a> ExamplePlugin<'a> {
pub fn new(plugin_interface: &'a GCPluginInterface) -> Self {
Self {
plugin_interface: plugin_interface,
}
}
}
impl<'a> GCPluginInstance<'a> for ExamplePlugin<'a> {
// Entrypoint which contains the interface to communicate with the core gateway
fn init(plugin_interface: &'a GCPluginInterface) -> Box<Self> {
let state = Box::new(ExamplePlugin::new(plugin_interface));
state
}
fn get_plugin_info() -> GCPluginInfo {
GCPluginInfo::new("ExamplePlugin", "0.1.0", "0", 0)
}
// Receive datapoint by subscription
// Whenever a datapoint is received, publish it multiplied by 10 (if it is a u64 type)
// to the datapoint with id 0.
// If wanted, store the datapoint value in the timeseries database.
fn receive_datapoint(&self, data_value: GCBorrowedDatapointValue) -> bool {
let value = data_value.get_value_u64().unwrap_or(10) * 10;
let dp = GCDatapointValue::new_u64(
0,
1748880760000000, // Nanoseconds timestamps
value,
data_value.get_quality(),
);
// Publish to realtime database
self.plugin_interface.publish_datapoint(&dp);
//Explicitly store in timeseries database
self.plugin_interface.store_datapoint(&dp);
true
}
// Optional function to get the plugin configuration schema into the runtime
fn get_config_schema() -> Option<Value> {
let schema = schemars::schema_for!(DummyConfig);
let mut schema_val = serde_json::to_value(schema).unwrap();
Some(schema_val)
}
}
The gc_plugin macro will be responsible for generating the necessary unsafe C ABI functions and callbacks. The plugin just needs to provide an implementation of GCPluginInstance, this instance will exist for the entire lifetime of the plugin instance (until the core calls raw::gc_plugin_shutdown). This is will be the object used by the core gateway to interact with the plugin.
It’s important to remember that the lifetime of a plugin instance is different then the lifetime of the plugin (or dynamic library). A plugin may contain multiple plugin instances that run inside the same process, it’s important to keep thin in mind
as it can affect the overall functionality (eg: handling global and static variables).
If the plugin needs to preform any additional cleanup, it can implement the Drop trait on the GCPluginInstance object which will be called when the plugin instance is shutdown.
It is also important to note that the callbacks functions might be invoked by different threads at the same time, thus the Send + Sync restriction in the GCPluginInstance trait.
§Fetching serial and ethernet interfaces
The plugin can fetch serial and ethernet interfaces information from the core gateway using the GCPluginInterface object. This is useful to avoid hardcoding interface names and configurations in the plugin configuration stage.
use gc_plugin_abi::{GCEthernetInterface, GCSerialInterface, GCPluginInterface};
let plugin_interface = get_plugin();
let ethernet_interface = plugin_interface.get_ethernet_interface("LAN1");
let ethernet_interface = plugin_interface.get_ethernet_interface("LAN1");§Using additional threads and async runtimes
Spawning an additional thread at startup or using an asynchronous runtime is common practice, although due to the possibility of the core unloading or shuting down the plugin, this introduces some additional complexity which the ABI abstractions cannot handle on it’s own.
Unfortunately, is not possible to safely pass the GCPluginInterface to a new thread due to it requiring a ’static lifetime. This is by design to avoid leaking the interface which is invalid after the plugin is dropped.
When working with async runtime, there is no mechanism to scope the lifetime, thus it is recommended to transform the GCPluginInterface to a ’static lifetime. This requires an unsafe operation, but as long as
the Runtime object is added to the GCPluginInstance object as a field, it will be safely joined when the plugin is unloaded/shutdown.
The following is an example of how to achieve this using Tokio:
use gc_plugin_abi::{GCBorrowedDatapointValue, GCPluginInfo, GCPluginInterface, GCPluginInstance, gc_plugin};
use tokio::runtime::{Runtime, Builder};
#[gc_plugin]
pub struct ExamplePlugin<'a> {
plugin_interface: &'a GCPluginInterface,
runtime: Runtime,
}
impl<'a> ExamplePlugin<'a> {
pub fn new(plugin_interface: &'a GCPluginInterface) -> Self {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_io()
.worker_threads(1)
.build()
.expect("Failed to create tokio runtime");
let plugin_interface = unsafe { std::mem::transmute::<&'a GCPluginInterface, &'static GCPluginInterface>(plugin_interface) };
runtime.spawn(async move {
// Do async work with the plugin interface
});
Self {
plugin_interface,
runtime
}
}
}
impl<'a> GCPluginInstance<'a> for ExamplePlugin<'a> {
// Entrypoint which contains the interface to communicate with the core gateway
fn init(plugin_interface: &'a GCPluginInterface) -> Box<Self> {
let state = Box::new(ExamplePlugin::new(plugin_interface));
state
}
fn get_plugin_info() -> GCPluginInfo {
GCPluginInfo::new("ExamplePlugin", "0.1.0", "0", 0)
}
// Receive datapoint by subscription
fn receive_datapoint(&self, _data_value: GCBorrowedDatapointValue) -> bool {
// Handle incoming datapoint
true
}
}The plugin is then responsible for ensuring GCPluginInterface is not used after raw::gc_plugin_shutdown is called.
§Plugin template
To create a new plugin it is recommended to use the script that will create a new plugin with the necessary boilerplate code.
./create_new_plugin.sh my_plugin_name§Important notes
§Examples
Examples of plugins can be found in our repository directory.
§Testing Plugins
Since testing plugins independently from the core gateway can be difficult, we provide a submodule to simulate the Gateway and help with integration tests. Head over to the [testing] module for more information.
Re-exports§
pub use logger::LogBuffer;
Modules§
- logger
- A logging system to provide log information to the core gateway without dynamic allocation.
- raw
- Raw bindings access.
Macros§
- audit
- Logs an audit message
- critical
- Logs a message with the
CRITICALlevel - critical_
limit - Rate-limited critical logging
- debug
- Logs a message with the
DEBUGlevel - debug_
limit - Rate-limited debug logging
- error
- Logs a message with the
ERRORlevel - error_
limit - Rate-limited error logging
- info
- Logs a message with the
INFOlevel - info_
limit - Rate-limited info logging
- log
- Logs a message with a given level. This macro should not be used directly, instead use the provided macros:
- warn
- Logs a message with the
WARNlevel - warn_
limit - Rate-limited warning logging
Structs§
- GCBorrowed
Datapoint Value - A wrapper around a GCDatapointValue that takes ownership of a datapoint value across the C ABI boundary and ensures the memory is deallocated when the object is dropped This wraps the raw C struct returned by the raw::gc_plugin_receive_datapoint function and ensures that the appropriate deallocation function is called when the object is dropped.
- GCDatapoint
- Safe zero cost abstraction over the gc_abi_sys::GCDatapoint This struct is meant to be initialized only by the gateway or another internal module.
- GCDatapoint
Publication Name - GCDatapoint
Subscription Name - GCDatapoint
Value - Represents a datapoint value that can be published to the core
- GCDatapoint
Value Quality - The quality of a datapoint value Represents a u16 bit field similar to the IEC 61850 protocol with the following structure:
- GCEthernet
Interface - Zero-cost abstraction over Ethernet interface configuration from C ABI.
- GCEthernet
Interface Type - GCPlugin
Info - A struct that represents the information of a plugin. This struct is meant to be initialized by the plugin and send to the gateway. It also the owner of the data it represents.
- GCPlugin
Interface - Zero cost abstraction over the GCPluginInterface
- GCSerial
Interface - Zero-cost abstraction over Serial interface configuration from C ABI.
- GCSerial
Interface Type
Enums§
- Data
Bits - Serial port data bits configuration
- GCDatapoint
Value Type - Possible value types for a datapoint
- GCPluginABI
Error - Parity
- Serial port parity configuration
- Stop
Bits - Serial port stop bits configuration
Traits§
- GCPlugin
Instance - A trait that represents the internal state of a plugin. Every plugin must implement this trait.
Functions§
- set_
datapoint_ release_ fn - This function sets the release function for datapoint values, it must be called before the control flow is handled to the plugin. Note: This function is for internal use only and should not be called by plugins directly.
Type Aliases§
Attribute Macros§
- gc_
plugin - Writes all the necessary functions that a plugin needs to export and redirects the flow to the user defined function.