Skip to main content

define_plugin

Macro define_plugin 

Source
macro_rules! define_plugin {
    (
        $plugin_ty:ty, {
            fn new() -> Self $new_body:block

            fn init(&mut self, $ctx_param:ident : &PluginContext $(,)?)
                -> Result<(), Box<dyn std::error::Error + Send + Sync>> $init_body:block

            fn name(&self) -> &str $name_body:block

            fn version(&self) -> &str $version_body:block

            fn process(
                &self,
                $msg_param:ident : &SpoeMessage $(,)?
            ) -> Result<ProcessingResult, Box<dyn std::error::Error + Send + Sync>> $process_body:block

            $(fn config_schema(&self) -> Option<&str> $schema_body:block)?

            $(fn validate(&self, $vctx_param:ident : &PluginContext $(,)?) -> Vec<Diagnostic> $validate_body:block)?

            $(metrics_static = $metrics_static:path;)?
        }
    ) => { ... };
}
Expand description

Define a plugin and emit the FFI surface (vtable + entry symbol).

Plugin authors write a regular impl-style block; the macro emits extern "C" thunks that bridge into it, the static PluginVTable, and the get_plugin_vtable symbol the hub looks up after dlopen.

process is automatically wrapped in std::panic::catch_unwind so a panic during request handling does not abort the hub process.

§Usage

use haproxy_spoa_hub_plugin_api::{ProcessingResult, SpoeMessage, SpoeValue, TxnVariable, define_plugin};

#[derive(Debug, Default)]
struct MyPlugin;

define_plugin!(MyPlugin, {
    fn new() -> Self { MyPlugin }

    fn init(&mut self, _: &PluginContext) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
        Ok(())
    }

    fn name(&self) -> &str { "my-plugin" }
    fn version(&self) -> &str { env!("CARGO_PKG_VERSION") }

    fn process(&self, _: &SpoeMessage)
        -> Result<ProcessingResult, Box<dyn std::error::Error + Send + Sync>>
    {
        Ok(ProcessingResult::single(TxnVariable::session(
            "result", SpoeValue::String("ok".into()),
        )))
    }
});

config_schema and validate are optional. Add them at the end of the block in that order if you need them. Plugins that omit them get the default no-op behavior (no schema, empty diagnostics).

§validate() contract: do semantic checks only

The hub guarantees that by the time your validate() body runs, the PluginContext passed in has already been validated against your config_schema() (in BOTH the load path and the --validate-socket path; see crates/hub/src/plugin_loader.rs’s collect_schema_errors and how it’s called from crates/hub/src/validate/orchestrator.rs). Schema errors short- circuit before validate() is called and surface to the operator as the same diagnostic shape your override would emit.

You should therefore never reimplement structural validation inside validate(). No re-checking that a field is a string, no walking arrays-vs-tables defensively, no type-shape assumptions. Use whatever typed accessor your plugin already builds for init() (typically from_context(ctx) populating a PluginConfig struct) — the structure is guaranteed valid.

validate() is for semantic checks the JSON Schema can’t express. Examples:

  • Compiling SecLang directives via Coraza so a typo’d SecBogusDirective becomes a line-numbered diagnostic (haproxy-spoa-hub-plugin-coraza).
  • Resolving a remote auth-server URL to verify it’s reachable (a hypothetical external-auth deep check).
  • Anything else that requires runtime context the schema can’t capture.

Historical context: pre-hub-v0.5.2, the --validate-socket path did NOT schema-check before calling validate(), so plugins that added their own structural walking could (and did) drift away from config_schema() and the runtime parser. The coraza plugin’s applications-as-array vs applications-as-table mismatch in v0.4.0–v0.4.1 was that bug. Centralising the check in the hub makes it impossible for a plugin author to accidentally repeat.

§Lifetime constraints

name and version MUST return &'static str. The macro emits an FFI thunk whose return type is RStr<'static>, so a &str borrowed from &self will not compile. In practice this means returning either a string literal or env!("CARGO_PKG_VERSION"). If you need to compute the name from instance fields, store it in a &'static str (e.g. via Box::leak(...) at construction time) or pre-register the strings as consts.

§Panic safety

Every author-supplied body is wrapped in std::panic::catch_unwind inside its FFI thunk. A panic surfaces as:

  • process / init / create: RResult::RErr(PluginPanicError).
  • validate: a single Diagnostic::error entry returned to the host (not an abort — required for --validate-socket mode).
  • config_schema: treated as RNone.
  • name / version: returned as the literal "<plugin-panic>".
  • shutdown / destroy: absorbed; memory may leak but the hub stays up.