Skip to main content

haproxy_spoa_hub_plugin_api/
vtable.rs

1//! Hand-rolled `#[repr(C)]` plugin vtable.
2//!
3//! Design rationale: see ADR-001 / docs/abi-evolution.md (TBD). In short:
4//! `abi_stable`'s prefix-type model rejects loads where the host's vtable
5//! has more fields than the plugin's, blocking the host-grows-faster
6//! direction we need. The hand-rolled C-style vtable with a leading
7//! `api_version: u32` field, version-gated host access, and an
8//! append-only evolution discipline gives us bidirectional ABI compat
9//! without a third-party load-time check that fights us.
10//!
11//! # Layout invariants (load-bearing)
12//!
13//! 1. `api_version` MUST be the first field.
14//! 2. Existing fields MUST NOT be reordered or removed in any future
15//!    crate version. New fields MUST be appended after the existing ones.
16//! 3. `PLUGIN_API_VERSION` increments by 1 each time a new field is
17//!    appended and the corresponding accessor lands.
18//! 4. Hosts MUST read `api_version` before accessing any field beyond
19//!    the v1 baseline. Reading newer fields on a plugin that reports
20//!    a lower `api_version` is undefined behavior — the plugin's
21//!    allocation does not include those bytes.
22//!
23//! See `crates/hub/tests/abi_matrix.rs` for the regression test that
24//! enforces invariants 1–3 by loading every published plugin version
25//! against the current hub binary.
26
27use std::os::raw::c_void;
28
29use abi_stable::std_types::{RBoxError, ROption, RResult, RStr, RString, RVec};
30
31use crate::types::{Diagnostic, PluginContext, ProcessingResult, SpoeMessage};
32
33/// API version corresponding to the v1 baseline (initial release of the
34/// hand-rolled vtable). Every field declared in `PluginVTable` is part
35/// of v1 and is present on every plugin built against this crate.
36pub const PLUGIN_API_VERSION_V1: u32 = 1;
37
38/// Latest API version this crate's `PluginVTable` exposes. Plugin
39/// authors set this as the `api_version` field. When future versions
40/// of this crate append a field, this constant bumps by 1.
41pub const PLUGIN_API_VERSION: u32 = PLUGIN_API_VERSION_V1;
42
43/// Symbol name the hub looks up via `dlsym` after `dlopen`.
44pub const GET_PLUGIN_VTABLE_SYMBOL: &[u8] = b"get_plugin_vtable\0";
45
46/// Type of the exported entry-point symbol.
47pub type GetPluginVTableFn = extern "C" fn() -> *const PluginVTable;
48
49/// The function table a plugin shared library exports.
50///
51/// `#[repr(C)]` is mandatory: it pins field offsets and padding rules
52/// so plugins built against an older version of this crate keep
53/// working when the hub is built against a newer one (and vice versa).
54///
55/// # Safety contract for hosts
56///
57/// Fields below the v1 baseline marker are version-gated. Hosts MUST
58/// NOT access them via `&PluginVTable` directly — the borrow would
59/// implicitly cover the whole struct, which on an older plugin is
60/// past the end of its allocation. Use the `read_*_field` accessors
61/// or read individual fields through the raw pointer (e.g.
62/// `unsafe { (*ptr).api_version }`) which read only the bytes for that
63/// field.
64#[repr(C)]
65pub struct PluginVTable {
66    // ============================================================
67    // v1 baseline — always present on every plugin built against
68    // this crate. All fields above the marker comment are mandatory
69    // and never change order.
70    // ============================================================
71    /// Plugin's reported API version. The hub uses this to gate
72    /// access to fields beyond the v1 baseline.
73    pub api_version: u32,
74
75    /// Factory: allocate and return a new plugin state pointer.
76    /// `RErr` aborts plugin loading.
77    pub create: extern "C" fn() -> RResult<*mut c_void, RBoxError>,
78
79    /// Destructor: free the plugin state pointer. Called once at hub
80    /// shutdown or when a plugin fails post-create. MUST tolerate
81    /// being called with a state pointer that `init()` has not yet
82    /// observed.
83    pub destroy: extern "C" fn(state: *mut c_void),
84
85    /// Initialize the plugin. Called once after `create` and any
86    /// schema/validate pre-checks. `RErr` aborts plugin loading.
87    pub init: extern "C" fn(state: *mut c_void, ctx: &PluginContext) -> RResult<(), RBoxError>,
88
89    /// Handle one SPOE message. Called per request. The implementation
90    /// is wrapped in `catch_unwind` by the `define_plugin!` macro so a
91    /// panic does not abort the hub process.
92    pub process: extern "C" fn(
93        state: *const c_void,
94        msg: &SpoeMessage,
95    ) -> RResult<ProcessingResult, RBoxError>,
96
97    /// Plugin's display name. Borrowed for the lifetime of the plugin
98    /// instance (until `destroy` is called). Plugins typically return
99    /// a `&'static str` literal.
100    pub name: extern "C" fn(state: *const c_void) -> RStr<'static>,
101
102    /// Plugin's `SemVer` string. Same lifetime contract as `name`.
103    pub plugin_version: extern "C" fn(state: *const c_void) -> RStr<'static>,
104
105    /// Shutdown hook. Called once before `destroy`.
106    pub shutdown: extern "C" fn(state: *const c_void),
107
108    /// Optional JSON Schema string for config validation. `RNone`
109    /// skips schema validation. The default `define_plugin!` macro
110    /// emits a thunk that returns `RNone` when the plugin author has
111    /// not declared one.
112    pub config_schema: extern "C" fn(state: *const c_void) -> ROption<RString>,
113
114    /// Deep config validation. Returns an empty `RVec` when the
115    /// plugin's config is valid. Errors block loading; warnings are
116    /// surfaced but do not block.
117    pub validate: extern "C" fn(state: *const c_void, ctx: &PluginContext) -> RVec<Diagnostic>,
118    // ============================================================
119    // End of v1 baseline. Future additions appear below this line.
120    // Each new field MUST be guarded on `api_version` in the host
121    // and bumps `PLUGIN_API_VERSION` by 1.
122    // ============================================================
123}
124
125impl PluginVTable {
126    /// Read just the `api_version` field of a plugin's vtable.
127    ///
128    /// # Safety
129    ///
130    /// `vtable_ptr` must be a non-null pointer to a `PluginVTable`
131    /// allocation that is at least 4 bytes long (every plugin has at
132    /// least the `api_version` field).
133    #[must_use]
134    pub unsafe fn read_api_version(vtable_ptr: *const PluginVTable) -> u32 {
135        // SAFETY: caller guarantees vtable_ptr is at least 4 bytes long.
136        // Reads only the first field; does not borrow the full struct.
137        unsafe { (*vtable_ptr).api_version }
138    }
139}