Skip to main content

Crate gc_plugin_abi

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.

So it is very important that whenever creating a new thread, the handle gets stored in the [GCPluginInstance] object, so that it can be joined when the plugin is unloaded/shutdown. If this is not done, the thread will leak when the plugin is unloaded/shutdown which is considered undefined behavior. Of course the plugin is also responsible for ensuring this doesn't create any deadlocks by implementing the necessary synchronization mechanisms.

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

Only a single instance of a plugin (marked by the macro ```#[gc_plugin]```) can coexist in the same crate! Having more than one plugin is considered undefined behavior and will likely not compile.

§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 CRITICAL level
critical_limit
Rate-limited critical logging
debug
Logs a message with the DEBUG level
debug_limit
Rate-limited debug logging
error
Logs a message with the ERROR level
error_limit
Rate-limited error logging
info
Logs a message with the INFO level
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 WARN level
warn_limit
Rate-limited warning logging

Structs§

GCBorrowedDatapointValue
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.
GCDatapointPublicationName
GCDatapointSubscriptionName
GCDatapointValue
Represents a datapoint value that can be published to the core
GCDatapointValueQuality
The quality of a datapoint value Represents a u16 bit field similar to the IEC 61850 protocol with the following structure:
GCEthernetInterface
Zero-cost abstraction over Ethernet interface configuration from C ABI.
GCEthernetInterfaceType
GCPluginInfo
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.
GCPluginInterface
Zero cost abstraction over the GCPluginInterface
GCSerialInterface
Zero-cost abstraction over Serial interface configuration from C ABI.
GCSerialInterfaceType

Enums§

DataBits
Serial port data bits configuration
GCDatapointValueType
Possible value types for a datapoint
GCPluginABIError
Parity
Serial port parity configuration
StopBits
Serial port stop bits configuration

Traits§

GCPluginInstance
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§

GCDatapointID

Attribute Macros§

gc_plugin
Writes all the necessary functions that a plugin needs to export and redirects the flow to the user defined function.