3 releases (breaking)
Uses new Rust 2024
| new 0.5.0 | May 7, 2026 |
|---|---|
| 0.3.0 | Mar 13, 2026 |
| 0.1.0 | Nov 27, 2025 |
#1382 in Rust patterns
79KB
1K
SLoC
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.
Dependencies
~0.6–3.5MB
~71K SLoC