Skip to main content

drasi_plugin_sdk/
lib.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![allow(unexpected_cfgs)]
16
17//! # Drasi Plugin SDK
18//!
19//! The Drasi Plugin SDK provides the traits, types, and utilities needed to build
20//! plugins for the Drasi Server. Plugins can be compiled directly into the server
21//! binary (static linking) or built as shared libraries for dynamic loading.
22//!
23//! ## Quick Start
24//!
25//! ```rust,ignore
26//! use drasi_plugin_sdk::prelude::*;
27//!
28//! // 1. Define your configuration DTO with OpenAPI schema support
29//! #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
30//! #[serde(rename_all = "camelCase")]
31//! pub struct MySourceConfigDto {
32//!     /// The hostname to connect to
33//!     #[schema(value_type = ConfigValueString)]
34//!     pub host: ConfigValue<String>,
35//!
36//!     /// The port number
37//!     #[schema(value_type = ConfigValueU16)]
38//!     pub port: ConfigValue<u16>,
39//!
40//!     /// Optional timeout in milliseconds
41//!     #[serde(skip_serializing_if = "Option::is_none")]
42//!     #[schema(value_type = Option<ConfigValueU32>)]
43//!     pub timeout_ms: Option<ConfigValue<u32>>,
44//! }
45//!
46//! // 2. Implement the appropriate descriptor trait
47//! pub struct MySourceDescriptor;
48//!
49//! #[async_trait]
50//! impl SourcePluginDescriptor for MySourceDescriptor {
51//!     fn kind(&self) -> &str { "my-source" }
52//!     fn config_version(&self) -> &str { "1.0.0" }
53//!
54//!     fn config_schema_json(&self) -> String {
55//!         let schema = <MySourceConfigDto as utoipa::ToSchema>::schema();
56//!         serde_json::to_string(&schema).unwrap()
57//!     }
58//!
59//!     async fn create_source(
60//!         &self,
61//!         id: &str,
62//!         config_json: &serde_json::Value,
63//!         auto_start: bool,
64//!     ) -> anyhow::Result<Box<dyn drasi_lib::sources::Source>> {
65//!         let dto: MySourceConfigDto = serde_json::from_value(config_json.clone())?;
66//!         let mapper = DtoMapper::new();
67//!         let host = mapper.resolve_string(&dto.host).await?;
68//!         let port = mapper.resolve_typed(&dto.port).await?;
69//!         // Build and return your source implementation...
70//!         todo!()
71//!     }
72//! }
73//!
74//! // 3. Create a plugin registration
75//! pub fn register() -> PluginRegistration {
76//!     PluginRegistration::new()
77//!         .with_source(Box::new(MySourceDescriptor))
78//! }
79//! ```
80//!
81//! ## Static vs. Dynamic Plugins
82//!
83//! Plugins can be integrated with Drasi Server in two ways:
84//!
85//! ### Static Linking
86//!
87//! Compile the plugin directly into the server binary. Create a
88//! [`PluginRegistration`](registration::PluginRegistration) and pass its descriptors
89//! to the server's plugin registry at startup. This is the simplest approach and
90//! is shown in the Quick Start above.
91//!
92//! ### Dynamic Loading
93//!
94//! Build the plugin as a shared library (`cdylib`) that the server loads at runtime
95//! from a plugins directory. This allows deploying new plugins without recompiling
96//! the server. See [Creating a Dynamic Plugin](#creating-a-dynamic-plugin) below
97//! for the full workflow.
98//!
99//! ## Creating a Dynamic Plugin
100//!
101//! Dynamic plugins are compiled as shared libraries (`.so` on Linux, `.dylib` on
102//! macOS, `.dll` on Windows) and placed in the server's plugins directory. The server
103//! discovers and loads them automatically at startup.
104//!
105//! ### Step 1: Set up the crate
106//!
107//! In your plugin's `Cargo.toml`, set the crate type to `cdylib`:
108//!
109//! ```toml
110//! [lib]
111//! crate-type = ["cdylib"]
112//!
113//! [dependencies]
114//! drasi-plugin-sdk = "..."  # Must match the server's version exactly
115//! drasi-lib = "..."
116//! ```
117//!
118//! ### Step 2: Implement descriptor(s)
119//!
120//! Implement [`SourcePluginDescriptor`](descriptor::SourcePluginDescriptor),
121//! [`ReactionPluginDescriptor`](descriptor::ReactionPluginDescriptor), and/or
122//! [`BootstrapPluginDescriptor`](descriptor::BootstrapPluginDescriptor) for your
123//! plugin. See the [`descriptor`] module docs for the full trait requirements.
124//!
125//! ### Step 3: Export the entry point
126//!
127//! Every dynamic plugin shared library **must** export a C function named
128//! `drasi_plugin_init` that returns a heap-allocated
129//! [`PluginRegistration`](registration::PluginRegistration) via raw pointer:
130//!
131//! ```rust,ignore
132//! use drasi_plugin_sdk::prelude::*;
133//!
134//! #[no_mangle]
135//! pub extern "C" fn drasi_plugin_init() -> *mut PluginRegistration {
136//!     let registration = PluginRegistration::new()
137//!         .with_source(Box::new(MySourceDescriptor))
138//!         .with_reaction(Box::new(MyReactionDescriptor));
139//!     Box::into_raw(Box::new(registration))
140//! }
141//! ```
142//!
143//! **Important details:**
144//!
145//! - The function must be `#[no_mangle]` and `extern "C"` so the server can find it
146//!   via the C ABI.
147//! - The `PluginRegistration` must be heap-allocated with `Box::new` and returned as
148//!   a raw pointer via [`Box::into_raw`]. The server takes ownership by calling
149//!   `Box::from_raw`.
150//! - The [`PluginRegistration::new()`](registration::PluginRegistration::new) constructor
151//!   automatically embeds the [`SDK_VERSION`](registration::SDK_VERSION) constant.
152//!   The server checks this at load time and **rejects plugins built with a different
153//!   SDK version**.
154//!
155//! ### Step 4: Build and deploy
156//!
157//! ```bash
158//! cargo build --release
159//! # Copy the shared library to the server's plugins directory
160//! cp target/release/libmy_plugin.so /path/to/plugins/
161//! ```
162//!
163//! ### Compatibility Requirements
164//!
165//! Both the plugin and the server **must** be compiled with:
166//!
167//! - The **same Rust toolchain** version (the Rust ABI is not stable across versions).
168//! - The **same `drasi-plugin-sdk` version**. The server compares
169//!   [`SDK_VERSION`](registration::SDK_VERSION) at load time and rejects mismatches.
170//!
171//! Failing to meet these requirements will result in the plugin being rejected at
172//! load time or, in the worst case, undefined behavior from ABI incompatibility.
173//!
174//! ## Modules
175//!
176//! - [`config_value`] — The [`ConfigValue<T>`](config_value::ConfigValue) enum for
177//!   configuration fields that support static values, environment variables, and secrets.
178//! - [`resolver`] — Value resolvers that convert config references to actual values.
179//! - [`mapper`] — The [`DtoMapper`](mapper::DtoMapper) service and mapping errors
180//!   trait for DTO-to-domain conversions.
181//! - [`descriptor`] — Plugin descriptor traits
182//!   ([`SourcePluginDescriptor`](descriptor::SourcePluginDescriptor),
183//!   [`ReactionPluginDescriptor`](descriptor::ReactionPluginDescriptor),
184//!   [`BootstrapPluginDescriptor`](descriptor::BootstrapPluginDescriptor)).
185//! - [`registration`] — The [`PluginRegistration`](registration::PluginRegistration) struct
186//!   returned by plugin entry points.
187//! - [`prelude`] — Convenience re-exports for plugin authors.
188//!
189//! ## Configuration Values
190//!
191//! Plugin DTOs use [`ConfigValue<T>`](config_value::ConfigValue) for fields that may
192//! be provided as static values, environment variable references, or secret references.
193//! See the [`config_value`] module for the full documentation and supported formats.
194//!
195//! ## OpenAPI Schema Generation
196//!
197//! Each plugin provides its configuration schema as a JSON-serialized utoipa `Schema`.
198//! The server deserializes these schemas and assembles them into the OpenAPI specification.
199//! This approach preserves strongly-typed OpenAPI documentation while keeping schema
200//! ownership with the plugins.
201//!
202//! ## DTO Versioning
203//!
204//! Each plugin independently versions its configuration DTO using semver. The server
205//! tracks config versions and can reject incompatible plugins. See the [`descriptor`]
206//! module docs for versioning rules.
207
208pub mod config_value;
209pub mod descriptor;
210pub mod ffi;
211pub mod mapper;
212pub mod prelude;
213pub mod registration;
214pub mod resolver;
215pub mod schema_ui;
216
217// Top-level re-exports for convenience
218pub use config_value::ConfigValue;
219pub use descriptor::{
220    BootstrapPluginDescriptor, IdentityProviderPluginDescriptor, ReactionPluginDescriptor,
221    SourcePluginDescriptor,
222};
223pub use mapper::{DtoMapper, MappingError};
224pub use registration::{PluginRegistration, SDK_VERSION};
225pub use resolver::{register_secret_resolver, ResolverError};
226
227/// Re-export tokio so the `export_plugin!` macro can reference it
228/// without requiring plugins to declare a direct tokio dependency.
229#[doc(hidden)]
230pub use tokio as __tokio;
231
232/// Re-export serde_json so the `export_plugin!` macro can reference it
233/// without requiring plugins to declare a direct serde_json dependency.
234#[doc(hidden)]
235pub use serde_json as __serde_json;
236
237/// Re-export async_trait so the `export_plugin!` macro can reference it
238/// without requiring plugins to declare a direct async_trait dependency.
239#[doc(hidden)]
240pub use async_trait as __async_trait;
241
242/// Export dynamic plugin entry points with FFI vtables.
243///
244/// Generates:
245/// - `drasi_plugin_metadata()` → version info for validation
246/// - `drasi_plugin_init()` → `FfiPluginRegistration` with vtable factories
247/// - Plugin-local tokio runtime, FfiLogger, lifecycle callbacks
248///
249/// # Usage
250///
251/// ```rust,ignore
252/// drasi_plugin_sdk::export_plugin!(
253///     plugin_id = "postgres",
254///     core_version = "0.1.0",
255///     lib_version = "0.3.8",
256///     plugin_version = "1.0.0",
257///     source_descriptors = [PostgresSourceDescriptor],
258///     reaction_descriptors = [],
259///     bootstrap_descriptors = [PostgresBootstrapDescriptor],
260/// );
261/// ```
262///
263/// An optional `worker_threads` parameter sets the default number of tokio
264/// worker threads for the plugin's runtime (default: 2). This can be
265/// overridden at deploy time via the `DRASI_PLUGIN_WORKERS` environment
266/// variable.
267///
268/// ```rust,ignore
269/// drasi_plugin_sdk::export_plugin!(
270///     plugin_id = "postgres",
271///     // ...
272///     bootstrap_descriptors = [PostgresBootstrapDescriptor],
273///     identity_provider_descriptors = [],
274///     worker_threads = 4,
275/// );
276/// ```
277#[macro_export]
278macro_rules! export_plugin {
279    // ── Full form: all descriptor types + worker_threads ──
280    (
281        plugin_id = $plugin_id:expr,
282        core_version = $core_ver:expr,
283        lib_version = $lib_ver:expr,
284        plugin_version = $plugin_ver:expr,
285        source_descriptors = [ $($source_desc:expr),* $(,)? ],
286        reaction_descriptors = [ $($reaction_desc:expr),* $(,)? ],
287        bootstrap_descriptors = [ $($bootstrap_desc:expr),* $(,)? ],
288        identity_provider_descriptors = [ $($ip_desc:expr),* $(,)? ],
289        secret_store_descriptors = [ $($ss_desc:expr),* $(,)? ],
290        worker_threads = $workers:expr $(,)?
291    ) => {
292        fn __auto_create_plugin_vtables() -> (
293            Vec<$crate::ffi::SourcePluginVtable>,
294            Vec<$crate::ffi::ReactionPluginVtable>,
295            Vec<$crate::ffi::BootstrapPluginVtable>,
296            Vec<$crate::ffi::IdentityProviderPluginVtable>,
297            Vec<$crate::ffi::SecretStorePluginVtable>,
298        ) {
299            let source_descs = vec![
300                $( $crate::ffi::build_source_plugin_vtable(
301                    $source_desc,
302                    __plugin_executor,
303                    __emit_lifecycle,
304                    __plugin_runtime,
305                ), )*
306            ];
307            let reaction_descs = vec![
308                $( $crate::ffi::build_reaction_plugin_vtable(
309                    $reaction_desc,
310                    __plugin_executor,
311                    __emit_lifecycle,
312                    __plugin_runtime,
313                ), )*
314            ];
315            let bootstrap_descs = vec![
316                $( $crate::ffi::build_bootstrap_plugin_vtable(
317                    $bootstrap_desc,
318                    __plugin_executor,
319                    __emit_lifecycle,
320                    __plugin_runtime,
321                ), )*
322            ];
323            let identity_provider_descs = vec![
324                $( $crate::ffi::build_identity_provider_plugin_vtable(
325                    $ip_desc,
326                    __plugin_executor,
327                    __emit_lifecycle,
328                    __plugin_runtime,
329                ), )*
330            ];
331            let secret_store_descs = vec![
332                $( $crate::ffi::build_secret_store_plugin_vtable(
333                    $ss_desc,
334                    __plugin_executor,
335                    __emit_lifecycle,
336                    __plugin_runtime,
337                ), )*
338            ];
339            (source_descs, reaction_descs, bootstrap_descs, identity_provider_descs, secret_store_descs)
340        }
341
342        $crate::export_plugin!(
343            @internal
344            plugin_id = $plugin_id,
345            core_version = $core_ver,
346            lib_version = $lib_ver,
347            plugin_version = $plugin_ver,
348            init_fn = __auto_create_plugin_vtables,
349            default_workers = $workers,
350        );
351    };
352    // ── Full form: all descriptor types (default worker threads) ──
353    (
354        plugin_id = $plugin_id:expr,
355        core_version = $core_ver:expr,
356        lib_version = $lib_ver:expr,
357        plugin_version = $plugin_ver:expr,
358        source_descriptors = [ $($source_desc:expr),* $(,)? ],
359        reaction_descriptors = [ $($reaction_desc:expr),* $(,)? ],
360        bootstrap_descriptors = [ $($bootstrap_desc:expr),* $(,)? ],
361        identity_provider_descriptors = [ $($ip_desc:expr),* $(,)? ],
362        secret_store_descriptors = [ $($ss_desc:expr),* $(,)? ] $(,)?
363    ) => {
364        $crate::export_plugin!(
365            plugin_id = $plugin_id,
366            core_version = $core_ver,
367            lib_version = $lib_ver,
368            plugin_version = $plugin_ver,
369            source_descriptors = [ $($source_desc),* ],
370            reaction_descriptors = [ $($reaction_desc),* ],
371            bootstrap_descriptors = [ $($bootstrap_desc),* ],
372            identity_provider_descriptors = [ $($ip_desc),* ],
373            secret_store_descriptors = [ $($ss_desc),* ],
374            worker_threads = 2usize,
375        );
376    };
377    // ── Backward-compat: identity_provider_descriptors but no secret_store_descriptors (with worker_threads) ──
378    (
379        plugin_id = $plugin_id:expr,
380        core_version = $core_ver:expr,
381        lib_version = $lib_ver:expr,
382        plugin_version = $plugin_ver:expr,
383        source_descriptors = [ $($source_desc:expr),* $(,)? ],
384        reaction_descriptors = [ $($reaction_desc:expr),* $(,)? ],
385        bootstrap_descriptors = [ $($bootstrap_desc:expr),* $(,)? ],
386        identity_provider_descriptors = [ $($ip_desc:expr),* $(,)? ],
387        worker_threads = $workers:expr $(,)?
388    ) => {
389        $crate::export_plugin!(
390            plugin_id = $plugin_id,
391            core_version = $core_ver,
392            lib_version = $lib_ver,
393            plugin_version = $plugin_ver,
394            source_descriptors = [ $($source_desc),* ],
395            reaction_descriptors = [ $($reaction_desc),* ],
396            bootstrap_descriptors = [ $($bootstrap_desc),* ],
397            identity_provider_descriptors = [ $($ip_desc),* ],
398            secret_store_descriptors = [],
399            worker_threads = $workers,
400        );
401    };
402    // ── Backward-compat: identity_provider_descriptors but no secret_store_descriptors (default workers) ──
403    (
404        plugin_id = $plugin_id:expr,
405        core_version = $core_ver:expr,
406        lib_version = $lib_ver:expr,
407        plugin_version = $plugin_ver:expr,
408        source_descriptors = [ $($source_desc:expr),* $(,)? ],
409        reaction_descriptors = [ $($reaction_desc:expr),* $(,)? ],
410        bootstrap_descriptors = [ $($bootstrap_desc:expr),* $(,)? ],
411        identity_provider_descriptors = [ $($ip_desc:expr),* $(,)? ] $(,)?
412    ) => {
413        $crate::export_plugin!(
414            plugin_id = $plugin_id,
415            core_version = $core_ver,
416            lib_version = $lib_ver,
417            plugin_version = $plugin_ver,
418            source_descriptors = [ $($source_desc),* ],
419            reaction_descriptors = [ $($reaction_desc),* ],
420            bootstrap_descriptors = [ $($bootstrap_desc),* ],
421            identity_provider_descriptors = [ $($ip_desc),* ],
422            secret_store_descriptors = [],
423        );
424    };
425    // ── Backward-compat: no identity_provider_descriptors, no secret_store_descriptors (with worker_threads) ──
426    (
427        plugin_id = $plugin_id:expr,
428        core_version = $core_ver:expr,
429        lib_version = $lib_ver:expr,
430        plugin_version = $plugin_ver:expr,
431        source_descriptors = [ $($source_desc:expr),* $(,)? ],
432        reaction_descriptors = [ $($reaction_desc:expr),* $(,)? ],
433        bootstrap_descriptors = [ $($bootstrap_desc:expr),* $(,)? ],
434        worker_threads = $workers:expr $(,)?
435    ) => {
436        $crate::export_plugin!(
437            plugin_id = $plugin_id,
438            core_version = $core_ver,
439            lib_version = $lib_ver,
440            plugin_version = $plugin_ver,
441            source_descriptors = [ $($source_desc),* ],
442            reaction_descriptors = [ $($reaction_desc),* ],
443            bootstrap_descriptors = [ $($bootstrap_desc),* ],
444            identity_provider_descriptors = [],
445            secret_store_descriptors = [],
446            worker_threads = $workers,
447        );
448    };
449    // ── Backward-compat: no identity_provider_descriptors, no secret_store_descriptors (default workers) ──
450    (
451        plugin_id = $plugin_id:expr,
452        core_version = $core_ver:expr,
453        lib_version = $lib_ver:expr,
454        plugin_version = $plugin_ver:expr,
455        source_descriptors = [ $($source_desc:expr),* $(,)? ],
456        reaction_descriptors = [ $($reaction_desc:expr),* $(,)? ],
457        bootstrap_descriptors = [ $($bootstrap_desc:expr),* $(,)? ] $(,)?
458    ) => {
459        $crate::export_plugin!(
460            plugin_id = $plugin_id,
461            core_version = $core_ver,
462            lib_version = $lib_ver,
463            plugin_version = $plugin_ver,
464            source_descriptors = [ $($source_desc),* ],
465            reaction_descriptors = [ $($reaction_desc),* ],
466            bootstrap_descriptors = [ $($bootstrap_desc),* ],
467            identity_provider_descriptors = [],
468            secret_store_descriptors = [],
469        );
470    };
471
472    // ── Internal form: custom init function ──
473    (
474        @internal
475        plugin_id = $plugin_id:expr,
476        core_version = $core_ver:expr,
477        lib_version = $lib_ver:expr,
478        plugin_version = $plugin_ver:expr,
479        init_fn = $init_fn:ident,
480        default_workers = $default_workers:expr $(,)?
481    ) => {
482        // ── Tokio runtime (accessible to plugin code) ──
483        //
484        // The runtime is stored behind an `AtomicPtr` so that
485        // `drasi_plugin_shutdown()` can take ownership and call
486        // `shutdown_timeout()` to cleanly stop all worker threads.
487        // A `OnceLock<()>` ensures one-time initialization.
488        static __RT_INIT: ::std::sync::OnceLock<()> = ::std::sync::OnceLock::new();
489        static __RT_PTR: ::std::sync::atomic::AtomicPtr<$crate::__tokio::runtime::Runtime> =
490            ::std::sync::atomic::AtomicPtr::new(::std::ptr::null_mut());
491
492        fn __init_plugin_runtime() {
493            let default_threads: usize = $default_workers;
494            let kind_var = format!(
495                "DRASI_PLUGIN_WORKERS_{}",
496                $plugin_id.to_uppercase().replace('-', "_")
497            );
498            let threads = ::std::env::var(&kind_var)
499                .ok()
500                .and_then(|v| v.parse().ok())
501                .or_else(|| {
502                    ::std::env::var("DRASI_PLUGIN_WORKERS")
503                        .ok()
504                        .and_then(|v| v.parse().ok())
505                })
506                .unwrap_or(default_threads);
507            let rt = Box::new(
508                $crate::__tokio::runtime::Builder::new_multi_thread()
509                    .worker_threads(threads)
510                    .enable_all()
511                    .thread_name(concat!($plugin_id, "-worker"))
512                    .build()
513                    .expect("Failed to create plugin tokio runtime"),
514            );
515            __RT_PTR.store(Box::into_raw(rt), ::std::sync::atomic::Ordering::Release);
516        }
517
518        pub fn __plugin_runtime() -> &'static $crate::__tokio::runtime::Runtime {
519            __RT_INIT.get_or_init(|| __init_plugin_runtime());
520            // Safety: after init, __RT_PTR is non-null and valid for 'static
521            // until drasi_plugin_shutdown() is called.
522            unsafe { &*__RT_PTR.load(::std::sync::atomic::Ordering::Acquire) }
523        }
524
525        /// Shut down the plugin's tokio runtime, stopping all worker threads.
526        ///
527        /// Must be called before dlclose / library unload. After this call,
528        /// any `&'static Runtime` references obtained from `__plugin_runtime()`
529        /// are dangling — no further FFI calls into this plugin are safe.
530        #[no_mangle]
531        pub extern "C" fn drasi_plugin_shutdown() {
532            // Just null the pointer so no further FFI calls use the runtime.
533            // The runtime itself is intentionally leaked — its worker threads
534            // will be killed when the process exits.
535            __RT_PTR.swap(
536                ::std::ptr::null_mut(),
537                ::std::sync::atomic::Ordering::AcqRel,
538            );
539        }
540
541        struct __SendPtr(*mut ::std::ffi::c_void);
542        unsafe impl Send for __SendPtr {}
543
544        /// Async executor dispatching to this plugin's tokio runtime.
545        pub extern "C" fn __plugin_executor(
546            future_ptr: *mut ::std::ffi::c_void,
547        ) -> *mut ::std::ffi::c_void {
548            let boxed: Box<
549                ::std::pin::Pin<
550                    Box<dyn ::std::future::Future<Output = *mut ::std::ffi::c_void> + Send>,
551                >,
552            > = unsafe { Box::from_raw(future_ptr as *mut _) };
553            let handle = __plugin_runtime().handle().clone();
554            let (tx, rx) = ::std::sync::mpsc::sync_channel::<__SendPtr>(0);
555            handle.spawn(async move {
556                let raw = (*boxed).await;
557                let _ = tx.send(__SendPtr(raw));
558            });
559            rx.recv().expect("Plugin executor task dropped").0
560        }
561
562        /// Run an async future on the plugin runtime, blocking until complete.
563        #[allow(dead_code)]
564        pub fn plugin_block_on<F>(f: F) -> F::Output
565        where
566            F: ::std::future::Future + Send + 'static,
567            F::Output: Send + 'static,
568        {
569            let handle = __plugin_runtime().handle().clone();
570            ::std::thread::spawn(move || handle.block_on(f))
571                .join()
572                .expect("plugin_block_on: spawned thread panicked")
573        }
574
575        // ── Log/lifecycle callback storage ──
576        static __LOG_CB: ::std::sync::atomic::AtomicPtr<()> =
577            ::std::sync::atomic::AtomicPtr::new(::std::ptr::null_mut());
578        static __LOG_CTX: ::std::sync::atomic::AtomicPtr<::std::ffi::c_void> =
579            ::std::sync::atomic::AtomicPtr::new(::std::ptr::null_mut());
580        static __LIFECYCLE_CB: ::std::sync::atomic::AtomicPtr<()> =
581            ::std::sync::atomic::AtomicPtr::new(::std::ptr::null_mut());
582        static __LIFECYCLE_CTX: ::std::sync::atomic::AtomicPtr<::std::ffi::c_void> =
583            ::std::sync::atomic::AtomicPtr::new(::std::ptr::null_mut());
584
585        // Note: FfiLogger (log::Log) is no longer used. All log crate events are
586        // bridged to tracing via tracing-log's LogTracer, then handled by
587        // FfiTracingLayer which has access to span context for correct routing.
588
589        extern "C" fn __set_log_callback_impl(
590            ctx: *mut ::std::ffi::c_void,
591            callback: $crate::ffi::LogCallbackFn,
592        ) {
593            __LOG_CTX.store(ctx, ::std::sync::atomic::Ordering::Release);
594            __LOG_CB.store(callback as *mut (), ::std::sync::atomic::Ordering::Release);
595
596            // Set up tracing subscriber with LogTracer bridge.
597            // LogTracer redirects log crate events → tracing, and FfiTracingLayer
598            // forwards all tracing events (including log-bridged ones) through FFI
599            // with span context for correct routing.
600            $crate::ffi::tracing_bridge::init_tracing_subscriber(
601                &__LOG_CB,
602                &__LOG_CTX,
603                $plugin_id,
604            );
605        }
606
607        extern "C" fn __set_lifecycle_callback_impl(
608            ctx: *mut ::std::ffi::c_void,
609            callback: $crate::ffi::LifecycleCallbackFn,
610        ) {
611            __LIFECYCLE_CTX.store(ctx, ::std::sync::atomic::Ordering::Release);
612            __LIFECYCLE_CB.store(
613                callback as *mut (),
614                ::std::sync::atomic::Ordering::Release,
615            );
616        }
617
618        // ── Config resolver callback storage ──
619        static __CONFIG_RESOLVER_CB: ::std::sync::atomic::AtomicPtr<()> =
620            ::std::sync::atomic::AtomicPtr::new(::std::ptr::null_mut());
621        static __CONFIG_RESOLVER_CTX: ::std::sync::atomic::AtomicPtr<::std::ffi::c_void> =
622            ::std::sync::atomic::AtomicPtr::new(::std::ptr::null_mut());
623
624        extern "C" fn __set_config_resolver_impl(
625            ctx: *mut ::std::ffi::c_void,
626            callback: $crate::ffi::ConfigResolverFn,
627        ) {
628            __CONFIG_RESOLVER_CTX.store(ctx, ::std::sync::atomic::Ordering::Release);
629            __CONFIG_RESOLVER_CB.store(callback as *mut (), ::std::sync::atomic::Ordering::Release);
630
631            // Create a ValueResolver that calls the host's callback to resolve
632            // ConfigValue references (secrets, env vars, etc.) across the FFI boundary.
633            struct FfiHostValueResolver;
634
635            #[$crate::__async_trait::async_trait]
636            impl $crate::resolver::ValueResolver for FfiHostValueResolver {
637                async fn resolve_to_string(
638                    &self,
639                    value: &$crate::ConfigValue<String>,
640                ) -> ::std::result::Result<String, $crate::resolver::ResolverError> {
641                    let ptr = __CONFIG_RESOLVER_CB.load(::std::sync::atomic::Ordering::Acquire);
642                    if ptr.is_null() {
643                        return Err($crate::resolver::ResolverError::SecretResolutionFailed(
644                            "No config resolver callback registered".to_string(),
645                        ));
646                    }
647                    let cb: $crate::ffi::ConfigResolverFn =
648                        unsafe { ::std::mem::transmute(ptr) };
649                    let ctx =
650                        __CONFIG_RESOLVER_CTX.load(::std::sync::atomic::Ordering::Acquire);
651
652                    let json = $crate::__serde_json::to_string(value).map_err(|e| {
653                        $crate::resolver::ResolverError::ParseError(format!(
654                            "Failed to serialize ConfigValue: {e}"
655                        ))
656                    })?;
657
658                    let ffi_result =
659                        cb(ctx as *const ::std::ffi::c_void, $crate::ffi::FfiStr::from_str(&json));
660                    unsafe {
661                        ffi_result.into_result().map_err(|e| {
662                            $crate::resolver::ResolverError::SecretResolutionFailed(format!(
663                                "Host config resolver failed: {e}"
664                            ))
665                        })
666                    }
667                }
668            }
669
670            // Register the FFI resolver as the global secret resolver.
671            $crate::resolver::register_secret_resolver(
672                ::std::sync::Arc::new(FfiHostValueResolver),
673            );
674        }
675
676        /// Emit a lifecycle event to the host.
677        pub fn __emit_lifecycle(
678            component_id: &str,
679            event_type: $crate::ffi::FfiLifecycleEventType,
680            message: &str,
681        ) {
682            let ptr = __LIFECYCLE_CB.load(::std::sync::atomic::Ordering::Acquire);
683            if !ptr.is_null() {
684                let cb: $crate::ffi::LifecycleCallbackFn =
685                    unsafe { ::std::mem::transmute(ptr) };
686                let ctx = __LIFECYCLE_CTX.load(::std::sync::atomic::Ordering::Acquire);
687                let event = $crate::ffi::FfiLifecycleEvent {
688                    component_id: $crate::ffi::FfiStr::from_str(component_id),
689                    component_type: $crate::ffi::FfiStr::from_str("plugin"),
690                    event_type,
691                    message: $crate::ffi::FfiStr::from_str(message),
692                    timestamp_us: $crate::ffi::now_us(),
693                };
694                cb(ctx, &event);
695            }
696        }
697
698        // ── Plugin metadata ──
699        static __PLUGIN_METADATA: $crate::ffi::PluginMetadata = $crate::ffi::PluginMetadata {
700            sdk_version: $crate::ffi::FfiStr {
701                ptr: $crate::ffi::FFI_SDK_VERSION.as_ptr() as *const ::std::os::raw::c_char,
702                len: $crate::ffi::FFI_SDK_VERSION.len(),
703            },
704            core_version: $crate::ffi::FfiStr {
705                ptr: $core_ver.as_ptr() as *const ::std::os::raw::c_char,
706                len: $core_ver.len(),
707            },
708            lib_version: $crate::ffi::FfiStr {
709                ptr: $lib_ver.as_ptr() as *const ::std::os::raw::c_char,
710                len: $lib_ver.len(),
711            },
712            plugin_version: $crate::ffi::FfiStr {
713                ptr: $plugin_ver.as_ptr() as *const ::std::os::raw::c_char,
714                len: $plugin_ver.len(),
715            },
716            target_triple: $crate::ffi::FfiStr {
717                ptr: $crate::ffi::TARGET_TRIPLE.as_ptr() as *const ::std::os::raw::c_char,
718                len: $crate::ffi::TARGET_TRIPLE.len(),
719            },
720            git_commit: $crate::ffi::FfiStr {
721                ptr: $crate::ffi::GIT_COMMIT_SHA.as_ptr() as *const ::std::os::raw::c_char,
722                len: $crate::ffi::GIT_COMMIT_SHA.len(),
723            },
724            build_timestamp: $crate::ffi::FfiStr {
725                ptr: $crate::ffi::BUILD_TIMESTAMP.as_ptr() as *const ::std::os::raw::c_char,
726                len: $crate::ffi::BUILD_TIMESTAMP.len(),
727            },
728        };
729
730        /// Returns plugin metadata for version validation. Called BEFORE init.
731        #[no_mangle]
732        pub extern "C" fn drasi_plugin_metadata() -> *const $crate::ffi::PluginMetadata {
733            &__PLUGIN_METADATA
734        }
735
736        /// Plugin entry point. Called AFTER metadata validation passes.
737        #[no_mangle]
738        pub extern "C" fn drasi_plugin_init() -> *mut $crate::ffi::FfiPluginRegistration {
739            match ::std::panic::catch_unwind(|| {
740                let _ = __plugin_runtime();
741                let (source_descs, reaction_descs, bootstrap_descs, identity_provider_descs, secret_store_descs) = $init_fn();
742
743                // Convert each Vec to a boxed slice so the underlying allocation's
744                // capacity exactly matches its length. This is required by the host's
745                // ABI contract: the host reconstructs each array via
746                // `Vec::from_raw_parts(ptr, len, len)`, which is only sound when the
747                // original allocation has capacity == len. `Vec::into_boxed_slice`
748                // shrinks the allocation to fit before yielding the raw pointer.
749                let mut source_descs = source_descs.into_boxed_slice();
750                let mut reaction_descs = reaction_descs.into_boxed_slice();
751                let mut bootstrap_descs = bootstrap_descs.into_boxed_slice();
752                let mut identity_provider_descs = identity_provider_descs.into_boxed_slice();
753                let mut secret_store_descs = secret_store_descs.into_boxed_slice();
754
755                let registration = Box::new($crate::ffi::FfiPluginRegistration {
756                    source_plugins: source_descs.as_mut_ptr(),
757                    source_plugin_count: source_descs.len(),
758                    reaction_plugins: reaction_descs.as_mut_ptr(),
759                    reaction_plugin_count: reaction_descs.len(),
760                    bootstrap_plugins: bootstrap_descs.as_mut_ptr(),
761                    bootstrap_plugin_count: bootstrap_descs.len(),
762                    identity_provider_plugins: identity_provider_descs.as_mut_ptr(),
763                    identity_provider_plugin_count: identity_provider_descs.len(),
764                    secret_store_plugins: secret_store_descs.as_mut_ptr(),
765                    secret_store_plugin_count: secret_store_descs.len(),
766                    set_log_callback: __set_log_callback_impl,
767                    set_lifecycle_callback: __set_lifecycle_callback_impl,
768                    set_config_resolver: __set_config_resolver_impl,
769                });
770                // Host takes ownership of the boxed-slice allocations. It will
771                // reclaim them via `Vec::from_raw_parts(ptr, len, len)` + drop.
772                let _ = ::std::boxed::Box::into_raw(source_descs);
773                let _ = ::std::boxed::Box::into_raw(reaction_descs);
774                let _ = ::std::boxed::Box::into_raw(bootstrap_descs);
775                let _ = ::std::boxed::Box::into_raw(identity_provider_descs);
776                let _ = ::std::boxed::Box::into_raw(secret_store_descs);
777                Box::into_raw(registration)
778            }) {
779                Ok(ptr) => ptr,
780                Err(_) => ::std::ptr::null_mut(),
781            }
782        }
783    };
784}