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