sz_rust_sdk/core/environment.rs
1//! Core implementation of SzEnvironment trait
2
3use crate::{
4 error::{SzError, SzResult},
5 ffi_call,
6 traits::*,
7 types::*,
8};
9use std::mem::ManuallyDrop;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, Mutex, Once, OnceLock};
12
13/// Core implementation of the SzEnvironment trait.
14///
15/// `SzEnvironmentCore` manages the lifecycle of the Senzing environment and serves
16/// as a factory for obtaining instances of other SDK components. It implements a
17/// singleton pattern required by the Senzing native library.
18///
19/// # Complete Lifecycle Example
20///
21/// This example shows the full lifecycle: creation, usage, and cleanup.
22///
23/// ```no_run
24/// # use sz_rust_sdk::helpers::ExampleEnvironment;
25/// use sz_rust_sdk::prelude::*;
26/// use std::sync::Arc;
27///
28/// # let env = ExampleEnvironment::initialize("doctest_lifecycle")?;
29/// // Get SDK components
30/// let engine = env.get_engine()?;
31/// let product = env.get_product()?;
32///
33/// // Use the SDK
34/// let version = product.get_version()?;
35/// println!("Senzing version: {}", version);
36///
37/// engine.add_record("TEST", "LIFECYCLE_1",
38/// r#"{"NAME_FULL": "John Smith"}"#, None)?;
39///
40/// // Drop components first (they borrow the environment)
41/// drop(engine);
42/// drop(product);
43///
44/// // Destroy releases native resources
45/// env.destroy()?;
46/// # Ok::<(), SzError>(())
47/// ```
48///
49/// # Singleton Pattern
50///
51/// The Senzing native library requires a single global instance per process.
52/// `get_instance()` enforces this by storing one `Arc` reference in a static
53/// variable. This means:
54///
55/// - All `get_instance()` calls with the same parameters return the same instance
56/// - The returned `Arc` always has `strong_count() >= 2` (singleton + caller)
57/// - Calling `destroy()` removes the singleton reference and cleans up native
58/// resources only when you hold the last reference
59///
60/// # Thread Safety
61///
62/// `SzEnvironmentCore` is thread-safe (`Send + Sync`). The `Arc<SzEnvironmentCore>`
63/// can be cloned and shared across threads. Each call to `get_engine()`, etc.,
64/// returns an independent component instance that can be used in its own thread.
65///
66/// ```
67/// # use sz_rust_sdk::helpers::ExampleEnvironment;
68/// use sz_rust_sdk::prelude::*;
69///
70/// # let env = ExampleEnvironment::initialize("doctest_thread_safety")?;
71/// let handles: Vec<_> = (0..4).map(|i| {
72/// let env = env.clone();
73/// std::thread::spawn(move || {
74/// let engine = env.get_engine().unwrap();
75/// engine.add_record("TEST", &format!("THREAD_{}", i),
76/// r#"{"NAME_FULL": "Test User"}"#, None)
77/// })
78/// }).collect();
79///
80/// for h in handles { h.join().unwrap()?; }
81/// # drop(env);
82/// # Ok::<(), SzError>(())
83/// ```
84pub struct SzEnvironmentCore {
85 is_destroyed: Arc<AtomicBool>,
86 /// Guards Sz_init() - ensures it runs exactly once and other threads wait
87 init_once: Arc<Once>,
88 /// Stores any error that occurred during Sz_init
89 init_error: Arc<Mutex<Option<String>>>,
90 /// Guards SzConfigMgr_init() - ensures it runs exactly once and other threads wait
91 config_mgr_init_once: Arc<Once>,
92 /// Stores any error that occurred during SzConfigMgr_init
93 config_mgr_init_error: Arc<Mutex<Option<String>>>,
94 /// Guards SzProduct_init() - ensures it runs exactly once and other threads wait
95 product_init_once: Arc<Once>,
96 /// Stores any error that occurred during SzProduct_init
97 product_init_error: Arc<Mutex<Option<String>>>,
98 module_name: String,
99 ini_params: String,
100 verbose_logging: bool,
101}
102
103// Singleton storage for the global SzEnvironmentCore instance
104// Using ManuallyDrop to prevent static destructor from running at exit,
105// which avoids conflicts with Senzing's internal static mutex destruction order
106static GLOBAL_ENVIRONMENT: OnceLock<ManuallyDrop<Mutex<Option<Arc<SzEnvironmentCore>>>>> =
107 OnceLock::new();
108
109impl SzEnvironmentCore {
110 /// Creates a new SzEnvironment instance
111 ///
112 /// # Arguments
113 ///
114 /// * `module_name` - Name of the module for logging purposes
115 /// * `ini_params` - JSON string containing initialization parameters
116 /// * `verbose_logging` - Whether to enable verbose logging
117 pub fn new(module_name: &str, ini_params: &str, verbose_logging: bool) -> SzResult<Self> {
118 Ok(Self {
119 is_destroyed: Arc::new(AtomicBool::new(false)),
120 init_once: Arc::new(Once::new()),
121 init_error: Arc::new(Mutex::new(None)),
122 config_mgr_init_once: Arc::new(Once::new()),
123 config_mgr_init_error: Arc::new(Mutex::new(None)),
124 product_init_once: Arc::new(Once::new()),
125 product_init_error: Arc::new(Mutex::new(None)),
126 module_name: module_name.to_string(),
127 ini_params: ini_params.to_string(),
128 verbose_logging,
129 })
130 }
131
132 /// Creates a new SzEnvironment instance with default parameters
133 pub fn new_default() -> SzResult<Self> {
134 Self::new("SzRustSDK", "{}", false)
135 }
136
137 /// Gets or creates the global singleton SzEnvironmentCore instance
138 ///
139 /// This method ensures that only one SzEnvironmentCore instance exists
140 /// per process, which is required by the Senzing SDK.
141 ///
142 /// # Arguments
143 ///
144 /// * `module_name` - Name of the module for logging purposes
145 /// * `ini_params` - JSON string containing initialization parameters
146 /// * `verbose_logging` - Whether to enable verbose logging
147 ///
148 /// # Arc Reference Count Behavior
149 ///
150 /// **Important**: The returned `Arc<SzEnvironmentCore>` will always have
151 /// `strong_count() >= 2` due to the singleton pattern:
152 ///
153 /// - **Reference 1**: Stored in `GLOBAL_ENVIRONMENT` (the singleton storage)
154 /// - **Reference 2+**: Returned to the caller (and any clones they make)
155 ///
156 /// This is by design and is NOT a memory leak. The singleton reference ensures
157 /// that subsequent calls to `get_instance()` return the same instance.
158 ///
159 /// ```
160 /// # use sz_rust_sdk::helpers::ExampleEnvironment;
161 /// use sz_rust_sdk::prelude::*;
162 /// use std::sync::Arc;
163 ///
164 /// # let env = ExampleEnvironment::initialize("doctest_get_instance_arc")?;
165 /// assert!(Arc::strong_count(&env) >= 2); // Expected: singleton + caller
166 ///
167 /// let env2 = SzEnvironmentCore::get_existing_instance()?;
168 /// assert!(Arc::ptr_eq(&env, &env2)); // Same instance
169 /// assert!(Arc::strong_count(&env) >= 3); // singleton + 2 callers
170 /// # drop(env2);
171 /// # drop(env);
172 /// # Ok::<(), SzError>(())
173 /// ```
174 ///
175 /// # Cleanup
176 ///
177 /// To properly release resources, use `destroy()` which removes the singleton
178 /// reference and calls native cleanup when you hold the last reference:
179 ///
180 /// ```no_run
181 /// # use sz_rust_sdk::helpers::ExampleEnvironment;
182 /// use sz_rust_sdk::prelude::*;
183 ///
184 /// # let env = ExampleEnvironment::initialize("doctest_get_instance_cleanup")?;
185 /// let env2 = SzEnvironmentCore::get_existing_instance()?;
186 /// // Drop all other references first
187 /// drop(env2);
188 /// // Then destroy - this removes singleton ref and cleans up native resources
189 /// env.destroy()?;
190 /// # Ok::<(), SzError>(())
191 /// ```
192 pub fn get_instance(
193 module_name: &str,
194 ini_params: &str,
195 verbose_logging: bool,
196 ) -> SzResult<Arc<Self>> {
197 let global_env = GLOBAL_ENVIRONMENT.get_or_init(|| ManuallyDrop::new(Mutex::new(None)));
198 let mut env_guard = global_env.lock().unwrap();
199
200 match env_guard.as_ref() {
201 Some(existing_env) => {
202 // Check if the existing environment is still valid
203 if existing_env.is_destroyed() {
204 let new_env = Arc::new(Self::new(module_name, ini_params, verbose_logging)?);
205 *env_guard = Some(new_env.clone());
206 Ok(new_env)
207 } else {
208 // Validate critical parameters match existing instance (ini_params and verbose_logging)
209 // Module name can be different as it's only used for logging
210 if existing_env.ini_params != ini_params
211 || existing_env.verbose_logging != verbose_logging
212 {
213 return Err(SzError::configuration(
214 "Cannot change critical initialization parameters (ini_params, verbose_logging) after SzEnvironmentCore instance is created",
215 ));
216 }
217 // Return the existing valid environment (module name can be different)
218 Ok(existing_env.clone())
219 }
220 }
221 None => {
222 // Create the first instance
223 let new_env = Arc::new(Self::new(module_name, ini_params, verbose_logging)?);
224 *env_guard = Some(new_env.clone());
225 Ok(new_env)
226 }
227 }
228 }
229
230 /// Gets the existing global singleton SzEnvironmentCore instance
231 ///
232 /// This method returns the existing singleton instance without creating a new one.
233 /// It will return an error if no instance has been created yet.
234 ///
235 /// # Returns
236 ///
237 /// Returns the existing singleton instance or an error if none exists.
238 pub fn get_existing_instance() -> SzResult<Arc<Self>> {
239 let global_env = GLOBAL_ENVIRONMENT.get_or_init(|| ManuallyDrop::new(Mutex::new(None)));
240 let env_guard = global_env.lock().unwrap();
241
242 match env_guard.as_ref() {
243 Some(existing_env) => {
244 if existing_env.is_destroyed() {
245 Err(SzError::unrecoverable(
246 "SzEnvironmentCore instance has been destroyed",
247 ))
248 } else {
249 Ok(existing_env.clone())
250 }
251 }
252 None => Err(SzError::unrecoverable(
253 "No SzEnvironmentCore instance has been created yet. Call get_instance() first.",
254 )),
255 }
256 }
257
258 /// Gets the global singleton instance if it exists
259 ///
260 /// Returns None if no instance has been created yet.
261 pub fn try_get_instance() -> Option<Arc<Self>> {
262 GLOBAL_ENVIRONMENT
263 .get()?
264 .lock()
265 .unwrap()
266 .as_ref()
267 .map(|env| env.clone())
268 }
269
270 /// Destroys the environment, consuming the Arc and releasing all native resources.
271 ///
272 /// This method uses Rust's ownership semantics to ensure safe cleanup:
273 /// - Only succeeds if the caller holds the sole reference to the environment
274 /// - If other references exist (e.g., other threads still using the environment),
275 /// returns an error and the environment remains valid
276 ///
277 /// # Ownership Requirements
278 ///
279 /// The Senzing native library is a global resource. Destroying the environment
280 /// while other code still holds references would cause undefined behavior.
281 /// This method enforces that you can only destroy when you're the sole owner.
282 ///
283 /// # Example
284 ///
285 /// ```no_run
286 /// # use sz_rust_sdk::helpers::ExampleEnvironment;
287 /// use sz_rust_sdk::prelude::*;
288 ///
289 /// # let env = ExampleEnvironment::initialize("doctest_destroy_basic")?;
290 /// // ... use env ...
291 ///
292 /// // When done, destroy (only works if this is the only reference)
293 /// env.destroy()?;
294 /// # Ok::<(), SzError>(())
295 /// ```
296 ///
297 /// # Calling `destroy()` from a `Drop` Implementation
298 ///
299 /// Because `destroy()` takes `Arc<Self>` by value, calling it from a `Drop`
300 /// impl requires moving the Arc out of your struct. **Use `Option<Arc<...>>`
301 /// with `.take()`** — this is exactly what [`SenzingGuard`](super::SenzingGuard) does internally.
302 ///
303 /// ```no_run
304 /// use sz_rust_sdk::prelude::*;
305 /// use std::sync::Arc;
306 ///
307 /// // CORRECT: Use Option<Arc<...>> + take()
308 /// struct MyWrapper {
309 /// env: Option<Arc<SzEnvironmentCore>>,
310 /// }
311 ///
312 /// impl Drop for MyWrapper {
313 /// fn drop(&mut self) {
314 /// if let Some(env) = self.env.take() {
315 /// let _ = env.destroy();
316 /// }
317 /// }
318 /// }
319 ///
320 /// # use sz_rust_sdk::helpers::ExampleEnvironment;
321 /// # let env = ExampleEnvironment::initialize("doctest_destroy_drop_correct")?;
322 /// let wrapper = MyWrapper { env: Some(env) };
323 /// drop(wrapper); // Cleanup happens via Drop
324 /// # Ok::<(), SzError>(())
325 /// ```
326 ///
327 /// ```no_run
328 /// use sz_rust_sdk::prelude::*;
329 /// use std::sync::Arc;
330 ///
331 /// // WRONG: ptr::read causes double-free / heap corruption
332 /// struct MyWrapper {
333 /// env: Arc<SzEnvironmentCore>, // Not wrapped in Option!
334 /// }
335 ///
336 /// impl Drop for MyWrapper {
337 /// fn drop(&mut self) {
338 /// // BUG: ptr::read creates a second Arc without incrementing the
339 /// // reference count. After destroy() frees the inner data, the
340 /// // compiler also drops self.env -> double-free -> heap corruption.
341 /// let env = unsafe { std::ptr::read(&self.env) };
342 /// let _ = env.destroy();
343 /// // <- compiler drops self.env here (use-after-free!)
344 /// }
345 /// }
346 /// ```
347 ///
348 /// **Note:** Using `ManuallyDrop` instead of `ptr::read` avoids the
349 /// double-free but is unnecessarily complex. Prefer `Option::take()` or
350 /// use [`SenzingGuard`](super::SenzingGuard) directly — it handles all of this correctly.
351 ///
352 /// # Errors
353 ///
354 /// Returns `SzError::Unrecoverable` if:
355 /// - Other references to the environment still exist
356 /// - The environment was already destroyed
357 pub fn destroy(self: Arc<Self>) -> SzResult<()> {
358 // First, remove from global singleton storage
359 // This drops the global reference, leaving only the caller's reference
360 if let Some(global_env) = GLOBAL_ENVIRONMENT.get() {
361 let mut env_guard = match global_env.lock() {
362 Ok(guard) => guard,
363 Err(poisoned) => poisoned.into_inner(),
364 };
365 // Only take if it's the same instance
366 if let Some(stored) = env_guard.as_ref()
367 && Arc::ptr_eq(stored, &self)
368 {
369 env_guard.take();
370 }
371 }
372
373 // Now try to get exclusive ownership
374 match Arc::try_unwrap(self) {
375 Ok(env) => {
376 // We have sole ownership - safe to destroy
377 if env.is_destroyed.load(Ordering::Relaxed) {
378 return Ok(()); // Already destroyed, nothing to do
379 }
380
381 // Mark as destroyed
382 env.is_destroyed.store(true, Ordering::Relaxed);
383
384 // Cleanup all Senzing modules
385 // Note: SzConfig_destroy() is not needed here - it manages config handles,
386 // not the config system itself. Config handles have their own lifecycle.
387 unsafe {
388 let _ = crate::ffi::SzDiagnostic_destroy();
389 let _ = crate::ffi::SzProduct_destroy();
390 let _ = crate::ffi::SzConfigMgr_destroy(); // CRITICAL: Clears cached config state
391 let _ = crate::ffi::Sz_destroy();
392
393 // Clear exception states
394 crate::ffi::Sz_clearLastException();
395 crate::ffi::SzDiagnostic_clearLastException();
396 crate::ffi::SzProduct_clearLastException();
397 crate::ffi::SzConfigMgr_clearLastException();
398 }
399
400 // Give the native library time to fully clean up internal state
401 std::thread::sleep(std::time::Duration::from_millis(100));
402
403 Ok(())
404 }
405 Err(arc) => {
406 // Other references exist - put it back in global storage and return error
407 // We must restore it since we removed it earlier
408 if let Some(global_env) = GLOBAL_ENVIRONMENT.get()
409 && let Ok(mut env_guard) = global_env.lock()
410 {
411 *env_guard = Some(arc);
412 }
413 Err(SzError::unrecoverable(
414 "Cannot destroy environment: other references still exist. \
415 Ensure all Arc<SzEnvironmentCore> clones are dropped before calling destroy().",
416 ))
417 }
418 }
419 }
420
421 /// Get the initialization parameters used by this environment
422 pub fn get_ini_params(&self) -> &str {
423 &self.ini_params
424 }
425
426 /// Get the verbose logging setting used by this environment
427 pub fn get_verbose_logging(&self) -> bool {
428 self.verbose_logging
429 }
430
431 /// Ensures Sz_init has been called - should be called before any engine operations
432 ///
433 /// This method is thread-safe: the first thread to call this will run Sz_init(),
434 /// and all other threads will block until initialization is complete.
435 fn ensure_initialized(&self) -> SzResult<()> {
436 // Clone Arcs for use in closure (can't capture &self in call_once)
437 let module_name = self.module_name.clone();
438 let ini_params = self.ini_params.clone();
439 let verbose_logging = self.verbose_logging;
440 let init_error = Arc::clone(&self.init_error);
441
442 // call_once blocks all threads until the closure completes
443 self.init_once.call_once(|| {
444 let result = (|| -> SzResult<()> {
445 let module_name_c = crate::ffi::helpers::str_to_c_string(&module_name)?;
446 let ini_params_c = crate::ffi::helpers::str_to_c_string(&ini_params)?;
447 let verbose = if verbose_logging { 1 } else { 0 };
448
449 ffi_call!(crate::ffi::Sz_init(
450 module_name_c.as_ptr(),
451 ini_params_c.as_ptr(),
452 verbose as i64
453 ));
454 Ok(())
455 })();
456
457 // Store any error for other threads to see
458 if let Err(e) = result
459 && let Ok(mut guard) = init_error.lock()
460 {
461 *guard = Some(e.to_string());
462 }
463 });
464
465 // Check if initialization failed
466 if let Ok(guard) = self.init_error.lock()
467 && let Some(err_msg) = guard.as_ref()
468 {
469 return Err(SzError::unrecoverable(format!("Sz_init failed: {err_msg}")));
470 }
471
472 Ok(())
473 }
474
475 /// Ensures SzConfigMgr_init has been called - should be called before any config manager operations
476 ///
477 /// This method is thread-safe: the first thread to call this will run SzConfigMgr_init(),
478 /// and all other threads will block until initialization is complete.
479 fn ensure_config_mgr_initialized(&self) -> SzResult<()> {
480 // Clone Arcs for use in closure (can't capture &self in call_once)
481 let module_name = self.module_name.clone();
482 let ini_params = self.ini_params.clone();
483 let verbose_logging = self.verbose_logging;
484 let init_error = Arc::clone(&self.config_mgr_init_error);
485
486 // call_once blocks all threads until the closure completes
487 self.config_mgr_init_once.call_once(|| {
488 let result = (|| -> SzResult<()> {
489 let module_name_c = crate::ffi::helpers::str_to_c_string(&module_name)?;
490 let ini_params_c = crate::ffi::helpers::str_to_c_string(&ini_params)?;
491 let verbose = if verbose_logging { 1 } else { 0 };
492
493 // Call the FFI directly and check with the proper config_mgr error handler
494 let return_code = unsafe {
495 crate::ffi::SzConfigMgr_init(
496 module_name_c.as_ptr(),
497 ini_params_c.as_ptr(),
498 verbose,
499 )
500 };
501 crate::ffi::helpers::check_config_mgr_return_code(return_code)?;
502 Ok(())
503 })();
504
505 // Store any error for other threads to see
506 if let Err(e) = result
507 && let Ok(mut guard) = init_error.lock()
508 {
509 *guard = Some(e.to_string());
510 }
511 });
512
513 // Check if initialization failed
514 if let Ok(guard) = self.config_mgr_init_error.lock()
515 && let Some(err_msg) = guard.as_ref()
516 {
517 return Err(SzError::unrecoverable(format!(
518 "SzConfigMgr_init failed: {err_msg}"
519 )));
520 }
521
522 Ok(())
523 }
524
525 /// Ensures SzProduct_init has been called - should be called before any product operations
526 ///
527 /// This method is thread-safe: the first thread to call this will run SzProduct_init(),
528 /// and all other threads will block until initialization is complete.
529 fn ensure_product_initialized(&self) -> SzResult<()> {
530 // Clone Arcs for use in closure (can't capture &self in call_once)
531 let module_name = self.module_name.clone();
532 let ini_params = self.ini_params.clone();
533 let verbose_logging = self.verbose_logging;
534 let init_error = Arc::clone(&self.product_init_error);
535
536 // call_once blocks all threads until the closure completes
537 self.product_init_once.call_once(|| {
538 let result = (|| -> SzResult<()> {
539 let module_name_c = crate::ffi::helpers::str_to_c_string(&module_name)?;
540 let ini_params_c = crate::ffi::helpers::str_to_c_string(&ini_params)?;
541 let verbose = if verbose_logging { 1 } else { 0 };
542
543 // Call the FFI directly and check with the proper product error handler
544 let return_code = unsafe {
545 crate::ffi::SzProduct_init(
546 module_name_c.as_ptr(),
547 ini_params_c.as_ptr(),
548 verbose,
549 )
550 };
551 crate::ffi::helpers::check_product_return_code(return_code)?;
552 Ok(())
553 })();
554
555 // Store any error for other threads to see
556 if let Err(e) = result
557 && let Ok(mut guard) = init_error.lock()
558 {
559 *guard = Some(e.to_string());
560 }
561 });
562
563 // Check if initialization failed
564 if let Ok(guard) = self.product_init_error.lock()
565 && let Some(err_msg) = guard.as_ref()
566 {
567 return Err(SzError::unrecoverable(format!(
568 "SzProduct_init failed: {err_msg}"
569 )));
570 }
571
572 Ok(())
573 }
574}
575
576impl SzEnvironment for SzEnvironmentCore {
577 fn is_destroyed(&self) -> bool {
578 self.is_destroyed.load(Ordering::Relaxed)
579 }
580
581 fn reinitialize(&self, config_id: ConfigId) -> SzResult<()> {
582 if self.is_destroyed() {
583 return Err(SzError::unrecoverable("Environment has been destroyed"));
584 }
585
586 // Ensure Sz_init has been called before reinitializing
587 self.ensure_initialized()?;
588
589 ffi_call!(crate::ffi::Sz_reinit(config_id));
590 Ok(())
591 }
592
593 fn get_active_config_id(&self) -> SzResult<ConfigId> {
594 if self.is_destroyed() {
595 return Err(SzError::unrecoverable("Environment has been destroyed"));
596 }
597
598 // Ensure Sz_init has been called before getting active config ID
599 self.ensure_initialized()?;
600
601 let mut config_id: i64 = 0;
602 let return_code = unsafe { crate::ffi::Sz_getActiveConfigID(&mut config_id) };
603 crate::ffi::helpers::check_return_code(return_code)?;
604 Ok(config_id)
605 }
606
607 fn get_product(&self) -> SzResult<Box<dyn SzProduct>> {
608 if self.is_destroyed() {
609 return Err(SzError::unrecoverable("Environment has been destroyed"));
610 }
611
612 // Ensure SzProduct_init has been called (thread-safe, all threads wait for completion)
613 self.ensure_product_initialized()?;
614
615 // Create product instance (init already done, so this is safe)
616 let product_core = super::product::SzProductCore::new()?;
617 Ok(Box::new(product_core))
618 }
619
620 fn get_engine(&self) -> SzResult<Box<dyn SzEngine>> {
621 if self.is_destroyed() {
622 return Err(SzError::unrecoverable("Environment has been destroyed"));
623 }
624
625 // Ensure Sz_init has been called before creating engine
626 self.ensure_initialized()?;
627
628 let engine_core = super::engine::SzEngineCore::new()?;
629 Ok(Box::new(engine_core))
630 }
631
632 fn get_config_manager(&self) -> SzResult<Box<dyn SzConfigManager>> {
633 if self.is_destroyed() {
634 return Err(SzError::unrecoverable("Environment has been destroyed"));
635 }
636
637 // Ensure SzConfigMgr_init has been called (thread-safe, all threads wait for completion)
638 // Note: SzConfigMgr does NOT require Sz_init - it initializes independently
639 // This allows config setup before engine initialization
640 self.ensure_config_mgr_initialized()?;
641
642 // Create config manager instance (init already done, so this is safe)
643 let config_mgr_core = super::config_manager::SzConfigManagerCore::new()?;
644 Ok(Box::new(config_mgr_core))
645 }
646
647 fn get_diagnostic(&self) -> SzResult<Box<dyn SzDiagnostic>> {
648 if self.is_destroyed() {
649 return Err(SzError::unrecoverable("Environment has been destroyed"));
650 }
651
652 // Ensure Sz_init has been called before creating diagnostic
653 self.ensure_initialized()?;
654
655 let diagnostic_core = super::diagnostic::SzDiagnosticCore::new_with_params(
656 &self.module_name,
657 &self.ini_params,
658 self.verbose_logging,
659 )?;
660 Ok(Box::new(diagnostic_core))
661 }
662}
663
664/// # Drop Behavior - Intentionally Does NOT Clean Up Native Resources
665///
666/// The `Drop` implementation for `SzEnvironmentCore` is **intentionally a no-op**
667/// that only marks the environment as destroyed without releasing native Senzing
668/// resources. This design is deliberate for several important reasons:
669///
670/// ## Why Drop Doesn't Clean Up
671///
672/// 1. **Singleton Pattern Complexity**: Due to the global singleton pattern,
673/// multiple `Arc` references exist (one in `GLOBAL_ENVIRONMENT`, one or more
674/// held by callers). The `Drop` trait cannot detect if this is the "last"
675/// reference being dropped.
676///
677/// 2. **Native Library Safety**: The Senzing native library has specific
678/// destruction ordering requirements. Calling `Sz_destroy()` while other
679/// threads might still be using the library can cause undefined behavior.
680///
681/// 3. **Controlled Shutdown**: Users need explicit control over when native
682/// resources are released, especially in applications that may reinitialize
683/// with different configurations.
684///
685/// ## Proper Cleanup Pattern
686///
687/// **Recommended:** Use [`SenzingGuard`](super::SenzingGuard) for automatic RAII cleanup:
688///
689/// ```no_run
690/// # use sz_rust_sdk::helpers::ExampleEnvironment;
691/// use sz_rust_sdk::prelude::*;
692///
693/// // Explicit destroy (ownership-based)
694/// # let env = ExampleEnvironment::initialize("doctest_drop_cleanup")?;
695/// let engine = env.get_engine()?;
696/// // ... use engine ...
697/// drop(engine);
698/// env.destroy()?; // Explicitly release native resources
699/// # Ok::<(), SzError>(())
700/// ```
701///
702/// If you need to call `destroy()` from your own `Drop` implementation,
703/// see the safety notes on [`destroy()`](Self::destroy) — you **must** store
704/// the Arc in an `Option` and use `.take()` to avoid double-free bugs.
705///
706/// ## What This Implementation Does
707///
708/// - Marks the environment as "destroyed" to prevent further API calls
709/// - Does **NOT** call `Sz_destroy()` or other native cleanup functions
710/// - Allows garbage collection of Rust-side resources
711impl Drop for SzEnvironmentCore {
712 fn drop(&mut self) {
713 // Mark as destroyed to prevent further use through any remaining references.
714 // We intentionally do NOT call Sz_destroy() here because:
715 // 1. We can't detect if this is the last Arc reference (singleton has one too)
716 // 2. Calling native cleanup during Drop can race with other threads
717 // 3. Users should use destroy() for explicit cleanup or SenzingGuard for RAII
718 if !self.is_destroyed() {
719 self.is_destroyed
720 .store(true, std::sync::atomic::Ordering::Relaxed);
721 }
722 }
723}