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

Apache-2.0

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.

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.

Dependencies

~0.6–3.5MB
~71K SLoC