#![allow(non_camel_case_types, non_local_definitions)]
pub mod logging;
pub mod metrics;
pub mod types;
pub mod vtable;
pub use abi_stable;
pub use abi_stable::std_types::{
RBoxError, RHashMap, ROption, RResult, RSlice, RStr, RString, RVec, Tuple2,
};
pub use logging::{HUB_LOG_SINK, HubLogSink, LogLevel, LogSinkFn};
pub use metrics::{MetricKind, MetricLabel, MetricRecorder, RecordMetricFn};
pub use types::{
ConfigValue, Diagnostic, DiagnosticSeverity, PluginContext, ProcessingResult, SpoeMessage,
SpoeValue, TxnVariable, VarScope,
};
pub use vtable::{
GET_PLUGIN_VTABLE_SYMBOL, GetPluginVTableFn, PLUGIN_API_VERSION, PLUGIN_API_VERSION_V1,
PLUGIN_API_VERSION_V2, PLUGIN_API_VERSION_V3, PLUGIN_API_VERSION_V4, PluginVTable,
};
#[macro_export]
macro_rules! define_plugin {
(
$plugin_ty:ty, {
fn new() -> Self $new_body:block
fn init(&mut $init_self:ident, $ctx_param:ident : &PluginContext $(,)?)
-> Result<(), Box<dyn std::error::Error + Send + Sync>> $init_body:block
fn name(&$name_self:ident) -> &str $name_body:block
fn version(&$version_self:ident) -> &str $version_body:block
fn process(
&$process_self:ident,
$msg_param:ident : &SpoeMessage $(,)?
) -> Result<ProcessingResult, Box<dyn std::error::Error + Send + Sync>> $process_body:block
$(fn config_schema(&$schema_self:ident) -> Option<&str> $schema_body:block)?
$(fn validate(&$validate_self:ident, $vctx_param:ident : &PluginContext $(,)?) -> Vec<Diagnostic> $validate_body:block)?
$(fn drain(&$drain_self:ident, $drain_timeout_param:ident : u64 $(,)?) -> bool $drain_body:block)?
$(metrics_static = $metrics_static:path;)?
}
) => {
impl $plugin_ty {
#[allow(dead_code)]
fn __new() -> Self $new_body
#[allow(clippy::unnecessary_wraps, dead_code)]
fn __init(
&mut $init_self,
$ctx_param: &$crate::PluginContext,
) -> ::std::result::Result<(), ::std::boxed::Box<dyn ::std::error::Error + Send + Sync>>
$init_body
#[allow(dead_code)]
fn __name(&$name_self) -> &'static str $name_body
#[allow(dead_code)]
fn __version(&$version_self) -> &'static str $version_body
#[allow(clippy::unnecessary_wraps, dead_code)]
fn __process(
&$process_self,
$msg_param: &$crate::SpoeMessage,
) -> ::std::result::Result<
$crate::ProcessingResult,
::std::boxed::Box<dyn ::std::error::Error + Send + Sync>,
> $process_body
$(
#[allow(clippy::unnecessary_wraps, dead_code)]
fn __config_schema(&$schema_self) -> ::std::option::Option<&'static str> $schema_body
)?
$(
#[allow(clippy::unnecessary_wraps, dead_code)]
fn __validate(
&$validate_self,
$vctx_param: &$crate::PluginContext,
) -> ::std::vec::Vec<$crate::Diagnostic> $validate_body
)?
$(
#[allow(clippy::unnecessary_wraps, dead_code)]
fn __drain(&$drain_self, $drain_timeout_param: u64) -> bool $drain_body
)?
}
const _: () = {
use ::std::os::raw::c_void;
use ::std::panic::{AssertUnwindSafe, catch_unwind};
extern "C" fn create() -> $crate::RResult<*mut c_void, $crate::RBoxError> {
match catch_unwind(|| {
let plugin: ::std::boxed::Box<$plugin_ty> =
::std::boxed::Box::new(<$plugin_ty>::__new());
::std::boxed::Box::into_raw(plugin).cast::<c_void>()
}) {
Ok(raw) => $crate::RResult::ROk(raw),
Err(_) => $crate::RResult::RErr($crate::RBoxError::new(
$crate::PluginPanicError,
)),
}
}
extern "C" fn destroy(state: *mut c_void) {
if state.is_null() {
return;
}
let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
::std::mem::drop(::std::boxed::Box::from_raw(state.cast::<$plugin_ty>()));
}));
}
extern "C" fn init(
state: *mut c_void,
ctx: &$crate::PluginContext,
) -> $crate::RResult<(), $crate::RBoxError> {
let plugin = unsafe { &mut *state.cast::<$plugin_ty>() };
match catch_unwind(AssertUnwindSafe(|| plugin.__init(ctx))) {
Ok(Ok(())) => $crate::RResult::ROk(()),
Ok(Err(e)) => $crate::RResult::RErr($crate::RBoxError::from_box(e)),
Err(_) => $crate::RResult::RErr($crate::RBoxError::new(
$crate::PluginPanicError,
)),
}
}
extern "C" fn process(
state: *const c_void,
msg: &$crate::SpoeMessage,
) -> $crate::RResult<$crate::ProcessingResult, $crate::RBoxError> {
let plugin = unsafe { &*state.cast::<$plugin_ty>() };
match catch_unwind(AssertUnwindSafe(|| plugin.__process(msg))) {
Ok(Ok(result)) => $crate::RResult::ROk(result),
Ok(Err(e)) => $crate::RResult::RErr($crate::RBoxError::from_box(e)),
Err(_) => $crate::RResult::RErr($crate::RBoxError::new(
$crate::PluginPanicError,
)),
}
}
extern "C" fn name(state: *const c_void) -> $crate::RStr<'static> {
let plugin = unsafe { &*state.cast::<$plugin_ty>() };
match catch_unwind(AssertUnwindSafe(|| plugin.__name())) {
Ok(s) => $crate::RStr::from(s),
Err(_) => $crate::RStr::from("<plugin-panic>"),
}
}
extern "C" fn plugin_version(state: *const c_void) -> $crate::RStr<'static> {
let plugin = unsafe { &*state.cast::<$plugin_ty>() };
match catch_unwind(AssertUnwindSafe(|| plugin.__version())) {
Ok(s) => $crate::RStr::from(s),
Err(_) => $crate::RStr::from("<plugin-panic>"),
}
}
extern "C" fn shutdown(_state: *const c_void) {
let _ = catch_unwind(AssertUnwindSafe(|| {
let _ = _state;
}));
}
extern "C" fn config_schema(_state: *const c_void) -> $crate::ROption<$crate::RString> {
catch_unwind(AssertUnwindSafe(|| {
$crate::__define_plugin_config_schema_thunk!(_state, $plugin_ty $(, $schema_body)?)
}))
.unwrap_or($crate::ROption::RNone)
}
extern "C" fn validate(
_state: *const c_void,
_ctx: &$crate::PluginContext,
) -> $crate::RVec<$crate::Diagnostic> {
catch_unwind(AssertUnwindSafe(|| {
$crate::__define_plugin_validate_thunk!(_state, _ctx, $plugin_ty $(, $validate_body)?)
}))
.unwrap_or_else(|_| {
let mut diags = $crate::RVec::new();
diags.push($crate::Diagnostic::error(
0,
0,
"plugin's validate() panicked",
));
diags
})
}
extern "C" fn set_metric_recorder(
_state: *mut c_void,
_record_fn: $crate::RecordMetricFn,
_ctx: *const c_void,
) {
let _ = catch_unwind(AssertUnwindSafe(|| {
$crate::__define_plugin_metrics_thunk!(_state, _record_fn, _ctx $(, $metrics_static)?)
}));
}
extern "C" fn set_log_sink(
_state: *mut c_void,
sink_fn: $crate::LogSinkFn,
ctx: *const c_void,
max_level: $crate::LogLevel,
) {
let _ = catch_unwind(AssertUnwindSafe(|| {
$crate::HUB_LOG_SINK.install(sink_fn, ctx, max_level);
}));
}
extern "C" fn drain(_state: *const c_void, _timeout_ms: u64) -> bool {
catch_unwind(AssertUnwindSafe(|| {
$crate::__define_plugin_drain_thunk!(_state, _timeout_ms, $plugin_ty $(, $drain_body)?)
}))
.unwrap_or(false)
}
#[allow(non_upper_case_globals)]
static PLUGIN_VTABLE_INSTANCE: $crate::PluginVTable = $crate::PluginVTable {
api_version: $crate::PLUGIN_API_VERSION,
create,
destroy,
init,
process,
name,
plugin_version,
shutdown,
config_schema,
validate,
set_metric_recorder,
drain,
set_log_sink,
};
#[unsafe(no_mangle)]
pub extern "C" fn get_plugin_vtable() -> *const $crate::PluginVTable {
&PLUGIN_VTABLE_INSTANCE
}
};
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __define_plugin_config_schema_thunk {
($state:ident, $plugin_ty:ty) => {{
let _ = $state;
$crate::ROption::RNone
}};
($state:ident, $plugin_ty:ty, $body:block) => {{
let plugin = unsafe { &*$state.cast::<$plugin_ty>() };
match plugin.__config_schema() {
::std::option::Option::Some(s) => $crate::ROption::RSome($crate::RString::from(s)),
::std::option::Option::None => $crate::ROption::RNone,
}
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __define_plugin_validate_thunk {
($state:ident, $ctx:ident, $plugin_ty:ty) => {{
let _ = $state;
let _ = $ctx;
$crate::RVec::new()
}};
($state:ident, $ctx:ident, $plugin_ty:ty, $body:block) => {{
let plugin = unsafe { &*$state.cast::<$plugin_ty>() };
$crate::RVec::from(plugin.__validate($ctx))
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __define_plugin_drain_thunk {
($state:ident, $timeout_ms:ident, $plugin_ty:ty) => {{
let _ = $state;
let _ = $timeout_ms;
true
}};
($state:ident, $timeout_ms:ident, $plugin_ty:ty, $body:block) => {{
let plugin = unsafe { &*$state.cast::<$plugin_ty>() };
plugin.__drain($timeout_ms)
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __define_plugin_metrics_thunk {
($state:ident, $record_fn:ident, $ctx:ident) => {{
let _ = $state;
let _ = $record_fn;
let _ = $ctx;
}};
($state:ident, $record_fn:ident, $ctx:ident, $metrics_static:path) => {{
let _ = $state;
$metrics_static.install($record_fn, $ctx);
}};
}
#[derive(Debug)]
pub struct PluginPanicError;
impl std::fmt::Display for PluginPanicError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "plugin panicked during message processing")
}
}
impl std::error::Error for PluginPanicError {}
#[cfg(test)]
mod stateful_macro_tests {
use super::*;
#[derive(Debug)]
struct StatefulPlugin {
value: i32,
}
define_plugin!(StatefulPlugin, {
fn new() -> Self {
StatefulPlugin { value: 1 }
}
fn init(
&mut self,
context: &PluginContext,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
self.value = context
.get_config("value")
.and_then(ConfigValue::as_integer)
.unwrap_or(1) as i32;
Ok(())
}
fn name(&self) -> &str {
let _ = self.value;
"stateful-test"
}
fn version(&self) -> &str {
let _ = self.value;
"0.0.0"
}
fn process(
&self,
_message: &SpoeMessage,
) -> Result<ProcessingResult, Box<dyn std::error::Error + Send + Sync>> {
Ok(ProcessingResult::single(TxnVariable::transaction(
"value",
SpoeValue::Int32(self.value),
)))
}
fn config_schema(&self) -> Option<&str> {
let _ = self.value;
None
}
fn validate(&self, _context: &PluginContext) -> Vec<Diagnostic> {
let _ = self.value;
Vec::new()
}
fn drain(&self, _timeout_ms: u64) -> bool {
self.value > 0
}
});
#[test]
fn author_bodies_can_read_and_write_instance_state() {
let mut config = RHashMap::new();
config.insert("value".into(), ConfigValue::Integer(42));
let context = PluginContext {
name: "stateful-test".into(),
config,
};
let message = SpoeMessage {
name: "test".into(),
args: RHashMap::new(),
stream_id: 1,
frame_id: 1,
};
let mut plugin = StatefulPlugin::__new();
plugin.__init(&context).expect("init should succeed");
let result = plugin.__process(&message).expect("process should succeed");
assert!(matches!(result.variables[0].value, SpoeValue::Int32(42)));
assert!(plugin.__drain(0));
}
}