drasi-plugin-sdk 0.8.4

SDK for building Drasi plugins (sources, reactions, bootstrappers)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
// Copyright 2025 The Drasi Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! `#[repr(C)]` vtable structs for the FFI boundary.
//!
//! These structs are the "contracts" between host and plugin. Each vtable
//! holds a `state` pointer (the opaque `self`) plus function pointers that
//! the caller dispatches through.

use std::ffi::c_void;

use super::types::{
    AsyncExecutorFn, FfiChangeOp, FfiComponentStatus, FfiCreateResult, FfiDispatchMode,
    FfiGetResult, FfiOwnedStr, FfiResult, FfiStr, FfiStringArray,
};

// ============================================================================
// Source events — carries SourceChange / SourceEventWrapper across FFI
// ============================================================================

/// Represents a source change event that crosses the FFI boundary.
/// The opaque pointer holds a heap-allocated Rust type (e.g., `SourceEventWrapper`)
/// that the plugin owns. The FFI metadata fields expose key information so the
/// host can route/filter without deserializing.
#[repr(C)]
pub struct FfiSourceEvent {
    pub opaque: *mut c_void,
    pub source_id: FfiStr,
    pub timestamp_us: i64,
    pub op: FfiChangeOp,
    pub label: FfiStr,
    pub entity_id: FfiStr,
    pub drop_fn: extern "C" fn(*mut c_void),
}

/// Bootstrap event — initial state data loaded before streaming begins.
#[repr(C)]
pub struct FfiBootstrapEvent {
    pub opaque: *mut c_void,
    pub source_id: FfiStr,
    pub timestamp_us: i64,
    pub sequence: u64,
    pub label: FfiStr,
    pub entity_id: FfiStr,
    pub drop_fn: extern "C" fn(*mut c_void),
}

// ============================================================================
// Receivers — streaming events from plugin to host
// ============================================================================

/// Callback function type for push-based change delivery.
/// Called by the plugin forwarder for each source event.
/// `ctx` is the host-owned context pointer, `event` is the event to deliver.
/// Returns `true` if the event was accepted, `false` to signal shutdown.
pub type FfiChangePushCallbackFn =
    extern "C" fn(ctx: *mut c_void, event: *mut FfiSourceEvent) -> bool;

/// Change receiver — push-based model.
///
/// Instead of the host polling via `recv_fn`, the host calls `start_push_fn`
/// with a callback. The plugin spawns a forwarder task that reads from the
/// underlying channel and invokes the callback for each event. This avoids
/// the `spawn_blocking` + `dispatch_to_runtime` round-trip per event.
#[repr(C)]
pub struct FfiChangeReceiver {
    pub state: *mut c_void,
    pub executor: AsyncExecutorFn,
    /// Start pushing events to the provided callback.
    /// The plugin spawns a forwarder task on its runtime.
    /// The callback is called once per event until the channel closes
    /// or the callback returns `false`.
    pub start_push_fn: extern "C" fn(
        state: *mut c_void,
        callback: FfiChangePushCallbackFn,
        callback_ctx: *mut c_void,
    ),
    pub drop_fn: extern "C" fn(state: *mut c_void),
}

unsafe impl Send for FfiChangeReceiver {}
unsafe impl Sync for FfiChangeReceiver {}

/// Callback function type for push-based bootstrap delivery.
/// Called by the plugin forwarder for each bootstrap event.
/// `ctx` is the host-owned context pointer, `event` is the event to deliver (null signals end-of-stream).
/// Returns `true` if the event was accepted, `false` to signal shutdown.
pub type FfiBootstrapPushCallbackFn =
    extern "C" fn(ctx: *mut c_void, event: *mut FfiBootstrapEvent) -> bool;

/// Bootstrap receiver — push-based finite stream of bootstrap events.
///
/// Like `FfiChangeReceiver`, uses a push model: the host calls `start_push_fn`
/// with a callback, and the plugin spawns a forwarder that pushes events via
/// the callback until the stream is exhausted (sends null) or the callback
/// returns `false`.
#[repr(C)]
pub struct FfiBootstrapReceiver {
    pub state: *mut c_void,
    pub start_push_fn: extern "C" fn(
        state: *mut c_void,
        callback: FfiBootstrapPushCallbackFn,
        callback_ctx: *mut c_void,
    ),
    pub drop_fn: extern "C" fn(state: *mut c_void),
}

unsafe impl Send for FfiBootstrapReceiver {}
unsafe impl Sync for FfiBootstrapReceiver {}

/// Subscription response — streaming receiver + optional bootstrap receiver.
#[repr(C)]
pub struct FfiSubscriptionResponse {
    pub query_id: FfiOwnedStr,
    pub source_id: FfiOwnedStr,
    pub receiver: *mut FfiChangeReceiver,
    /// Null if no bootstrap data available.
    pub bootstrap_receiver: *mut FfiBootstrapReceiver,
    /// Shared position handle for the query to report its last durably-processed
    /// sequence. The plugin source allocates this via `SourceBase::create_position_handle()`
    /// and transfers one `Arc` ref-count across FFI via `Arc::into_raw`. The host
    /// reconstructs with `Arc::from_raw` and writes to the `AtomicU64` as it commits
    /// events. Null if position tracking was not requested.
    pub position_handle_ptr: *const std::ffi::c_void,
    /// Null if no bootstrap result is expected (no bootstrap active).
    pub bootstrap_result_receiver: *mut FfiBootstrapResultReceiver,
}

/// Callback type for delivering a bootstrap result from plugin to host.
/// The plugin calls this when the bootstrap `oneshot::Receiver` resolves.
/// `result` is a heap-allocated `FfiBootstrapResult` — the host takes ownership.
pub type FfiBootstrapResultCallbackFn =
    extern "C" fn(ctx: *mut std::ffi::c_void, result: *mut FfiBootstrapResult);

/// Push-based receiver for the bootstrap handover result.
///
/// The host calls `start_fn` with a callback + context. The plugin spawns an
/// async task on its runtime that awaits the underlying `oneshot::Receiver`,
/// converts the `BootstrapResult` to `FfiBootstrapResult`, and calls the
/// callback. This avoids blocking any thread during long-running bootstraps.
#[repr(C)]
pub struct FfiBootstrapResultReceiver {
    pub state: *mut std::ffi::c_void,
    /// Start listening for the result. Plugin spawns async task that calls
    /// `callback(ctx, result)` when the bootstrap result is ready.
    /// Returns immediately — does not block.
    pub start_fn: extern "C" fn(
        state: *mut std::ffi::c_void,
        callback: FfiBootstrapResultCallbackFn,
        ctx: *mut std::ffi::c_void,
    ),
    pub drop_fn: extern "C" fn(state: *mut std::ffi::c_void),
}

unsafe impl Send for FfiBootstrapResultReceiver {}
unsafe impl Sync for FfiBootstrapResultReceiver {}

// ============================================================================
// Query result events — carries QueryResult across FFI to reactions
// ============================================================================

/// Callback for push-based query result delivery to reactions.
/// The plugin calls this to receive the next QueryResult from the host.
/// Returns a `*mut QueryResult` (ownership transfers to the plugin),
/// or null to signal end-of-stream / shutdown.
/// The `result` parameter is unused (reserved).
pub type FfiResultPushCallbackFn =
    extern "C" fn(ctx: *mut c_void, result: *mut c_void) -> *mut c_void;

// ============================================================================
// Bootstrap sender — callback for sending bootstrap records from provider
// ============================================================================

/// FFI-safe callback for sending bootstrap records from a BootstrapProvider.
/// The host creates this and passes it to the provider.
#[repr(C)]
pub struct FfiBootstrapSender {
    pub state: *mut c_void,
    /// Send a bootstrap record. Returns 0 on success, non-zero if channel closed.
    pub send_fn: extern "C" fn(state: *mut c_void, event: *mut FfiBootstrapEvent) -> i32,
    pub drop_fn: extern "C" fn(state: *mut c_void),
}

unsafe impl Send for FfiBootstrapSender {}
unsafe impl Sync for FfiBootstrapSender {}

/// FFI-safe bootstrap result returned across the plugin boundary.
///
/// Heap-allocated by the callee (vtable_gen) and freed by the caller
/// (host proxy or plugin proxy) using `Box::from_raw`.
#[repr(C)]
pub struct FfiBootstrapResult {
    /// Number of events sent (>= 0), or -1 on error.
    pub event_count: i64,
    /// Last sequence number, or -1 if not available.
    pub last_sequence: i64,
    /// Whether bootstrap/stream sequences are aligned for dedup.
    pub sequences_aligned: bool,
    /// Pointer to source position bytes (null if not available).
    /// Callee owns the allocation; caller must free via `source_position_drop_fn`.
    pub source_position_ptr: *const u8,
    /// Length of source position bytes (0 if ptr is null).
    pub source_position_len: usize,
    /// Drop function for the source position allocation.
    /// Null if `source_position_ptr` is null.
    pub source_position_drop_fn: Option<extern "C" fn(*mut u8, usize)>,
}

unsafe impl Send for FfiBootstrapResult {}
unsafe impl Sync for FfiBootstrapResult {}

// ============================================================================
// Runtime context — host services provided to plugins during initialization
// ============================================================================

/// Runtime context passed from host to plugin during initialization.
/// Contains vtable-based callbacks for host services — no trait objects.
#[repr(C)]
pub struct FfiRuntimeContext {
    pub instance_id: FfiStr,
    pub component_id: FfiStr,
    /// Nullable — not all plugins need state store.
    pub state_store: *const StateStoreVtable,
    /// Per-instance log callback (nullable — falls back to global if null).
    pub log_callback: Option<super::callbacks::LogCallbackFn>,
    /// Opaque context for per-instance log callback.
    pub log_ctx: *mut c_void,
    /// Per-instance lifecycle callback (nullable — falls back to global if null).
    pub lifecycle_callback: Option<super::callbacks::LifecycleCallbackFn>,
    /// Opaque context for per-instance lifecycle callback.
    pub lifecycle_ctx: *mut c_void,
    /// Nullable — identity provider for credential injection.
    pub identity_provider: *const super::identity::IdentityProviderVtable,
    /// Nullable — snapshot fetcher for on-demand query snapshot access.
    pub snapshot_fetcher: *const SnapshotFetcherVtable,
}

// Safety: FfiRuntimeContext contains raw pointers that point to thread-safe data.
// The vtables contain only function pointers and Send+Sync state.
// The ctx pointers point to Arc-backed structures that are Send+Sync.
unsafe impl Send for FfiRuntimeContext {}
unsafe impl Sync for FfiRuntimeContext {}

// ============================================================================
// Source vtable
// ============================================================================

drasi_ffi_primitives::ffi_vtable! {
    /// FFI-safe vtable for a Source instance.
    pub struct SourceVtable {
        // Identity
        fn id_fn(state: *const) -> FfiStr,
        fn type_name_fn(state: *const) -> FfiStr,
        fn auto_start_fn(state: *const) -> bool,
        fn dispatch_mode_fn(state: *const) -> FfiDispatchMode,

        // Configuration inspection
        /// Returns the source's configuration properties as a JSON string.
        fn properties_fn(state: *const) -> FfiOwnedStr,
        /// Returns the source's best-effort graph schema as JSON, or `null`.
        fn describe_schema_fn(state: *const) -> FfiOwnedStr,

        // Lifecycle
        fn start_fn(state: *mut) -> FfiResult,
        fn stop_fn(state: *mut) -> FfiResult,
        fn status_fn(state: *const) -> FfiComponentStatus,
        fn deprovision_fn(state: *mut) -> FfiResult,

        // Initialization
        fn initialize_fn(state: *mut, ctx: *const FfiRuntimeContext),

        // Subscriptions
        /// Subscribe with query_id, node_labels JSON, relation_labels JSON,
        /// optional resume_from position bytes, and optional last_sequence
        /// for sequence counter recovery.
        fn subscribe_fn(state: *mut, source_id: FfiStr, enable_bootstrap: bool, query_id: FfiStr, nodes_json: FfiStr, relations_json: FfiStr, resume_from_ptr: *const u8, resume_from_len: u32, has_last_sequence: bool, last_sequence: u64, request_position_handle: bool) -> *mut FfiSubscriptionResponse,

        /// Host calls this to inject an external bootstrap provider (from another plugin).
        fn set_bootstrap_provider_fn(state: *mut, provider: *mut BootstrapProviderVtable),

        // Recovery / checkpoint support
        /// Returns true if this source can replay events from a checkpointed
        /// position, enabling checkpoint-based recovery for persistent queries.
        fn supports_replay_fn(state: *const) -> bool,

        /// Tells the source that the given query no longer needs its position
        /// handle. The source can stop tracking the resume position for this
        /// query.
        fn remove_position_handle_fn(state: *mut, query_id: FfiStr) -> FfiResult,
    }
}

// ============================================================================
// Reaction vtable
// ============================================================================

// --- FFI types for the reaction bootstrap bridge ---

/// FFI-safe checkpoint data.
#[repr(C)]
#[derive(Clone, Copy)]
pub struct FfiCheckpoint {
    pub sequence: u64,
    pub config_hash: u64,
}

/// Result of a checkpoint read — `found=false` means no checkpoint exists.
#[repr(C)]
pub struct FfiCheckpointResult {
    pub found: bool,
    pub checkpoint: FfiCheckpoint,
    /// Non-null on error.
    pub error: FfiOwnedStr,
}

/// FFI-safe iterator for streaming snapshot rows one at a time.
///
/// The host creates this iterator and the plugin pulls rows via `next_fn`.
/// Each `next_fn` call returns one JSON-serialized row, or an empty string
/// when the iterator is exhausted.
///
/// **Ownership:** The plugin MUST call `drop_fn` when done — even if it
/// doesn't exhaust the iterator. The host allocates the iterator state
/// and `drop_fn` is the only way to reclaim it.
#[repr(C)]
pub struct FfiSnapshotIterator {
    /// Opaque host-allocated iterator state.
    pub iter_ctx: *mut c_void,
    /// Returns the next row as JSON, or an empty string when exhausted.
    pub next_fn: extern "C" fn(*mut c_void) -> FfiOwnedStr,
    /// Drops the iterator state. Must be called exactly once.
    pub drop_fn: extern "C" fn(*mut c_void),
}

/// FFI-safe response from a snapshot fetch — streaming variant.
///
/// On success (`error` is empty): `iterator` is valid and the plugin
/// pulls rows via `iterator.next_fn`. On error: `error` is non-empty
/// and `iterator` fields are invalid (must not be called).
#[repr(C)]
pub struct FfiSnapshotIteratorResponse {
    pub iterator: FfiSnapshotIterator,
    pub as_of_sequence: u64,
    pub config_hash: u64,
    /// Non-empty on error — iterator fields are invalid in that case.
    pub error: FfiOwnedStr,
}

/// FFI-safe iterator for streaming outbox entries one at a time.
///
/// Same ownership model as `FfiSnapshotIterator`.
#[repr(C)]
pub struct FfiOutboxIterator {
    /// Opaque host-allocated iterator state.
    pub iter_ctx: *mut c_void,
    /// Returns the next `QueryResult` as JSON, or an empty string when exhausted.
    pub next_fn: extern "C" fn(*mut c_void) -> FfiOwnedStr,
    /// Drops the iterator state. Must be called exactly once.
    pub drop_fn: extern "C" fn(*mut c_void),
}

/// FFI-safe response from an outbox fetch — streaming variant.
#[repr(C)]
pub struct FfiOutboxIteratorResponse {
    pub iterator: FfiOutboxIterator,
    pub latest_sequence: u64,
    pub config_hash: u64,
    /// Non-empty on error — iterator fields are invalid in that case.
    pub error: FfiOwnedStr,
}

/// Callback signatures for FfiBootstrapContext.
pub type FfiBootstrapFetchSnapshotFn = extern "C" fn(*mut c_void) -> FfiSnapshotIteratorResponse;
pub type FfiBootstrapFetchOutboxFn = extern "C" fn(*mut c_void, u64) -> FfiOutboxIteratorResponse;
pub type FfiBootstrapReadCheckpointFn = extern "C" fn(*mut c_void) -> FfiCheckpointResult;
pub type FfiBootstrapWriteCheckpointFn = extern "C" fn(*mut c_void, FfiCheckpoint) -> FfiResult;

/// FFI-safe bootstrap context passed to `bootstrap_fn`.
///
/// The host builds this and passes a pointer. The plugin calls the callback
/// function pointers with `callback_ctx` to invoke `fetch_snapshot()`, etc.
#[repr(C)]
pub struct FfiBootstrapContext {
    /// Query ID this bootstrap is for.
    pub query_id: FfiStr,
    /// `true` when a prior checkpoint is being discarded (reset/recovery).
    /// `false` on a fresh start with no prior checkpoint.
    pub is_reset: bool,
    /// Opaque host context passed as first arg to all callbacks.
    pub callback_ctx: *mut c_void,
    /// Fetch a snapshot of the query's live result set.
    pub fetch_snapshot_fn: FfiBootstrapFetchSnapshotFn,
    /// Fetch outbox entries after a given sequence.
    pub fetch_outbox_fn: FfiBootstrapFetchOutboxFn,
    /// Read the persisted checkpoint for this query subscription.
    pub read_checkpoint_fn: FfiBootstrapReadCheckpointFn,
    /// Write a checkpoint for this query subscription.
    pub write_checkpoint_fn: FfiBootstrapWriteCheckpointFn,
}

drasi_ffi_primitives::ffi_vtable! {
    /// FFI-safe vtable for a Reaction instance.
    pub struct ReactionVtable {
        // Identity
        fn id_fn(state: *const) -> FfiStr,
        fn type_name_fn(state: *const) -> FfiStr,
        fn auto_start_fn(state: *const) -> bool,
        fn query_ids_fn(state: *const) -> FfiStringArray,

        // Configuration inspection
        /// Returns the reaction's configuration properties as a JSON string.
        fn properties_fn(state: *const) -> FfiOwnedStr,

        // Lifecycle
        fn start_fn(state: *mut) -> FfiResult,
        fn stop_fn(state: *mut) -> FfiResult,
        fn status_fn(state: *const) -> FfiComponentStatus,
        fn deprovision_fn(state: *mut) -> FfiResult,

        // Initialization
        fn initialize_fn(state: *mut, ctx: *const FfiRuntimeContext),

        // Host-managed query subscription forwarding (push-based)
        /// The host calls this once to start push-based delivery.
        /// The plugin spawns a forwarder task that reads from an internal channel
        /// and calls `reaction.enqueue_query_result()` for each item.
        fn start_result_push_fn(state: *mut, callback: FfiResultPushCallbackFn, callback_ctx: *mut c_void),

        // Recovery archetype methods
        /// Whether this reaction requires a durable state store.
        fn is_durable_fn(state: *const) -> bool,
        /// Whether this reaction needs a full snapshot on first start.
        fn needs_snapshot_on_fresh_start_fn(state: *const) -> bool,
        /// Default recovery policy (returned as u8 ordinal: 0=Strict, 1=AutoReset, 2=AutoSkipGap).
        fn default_recovery_policy_fn(state: *const) -> u8,

        // Bootstrap hook
        /// Called during startup when bootstrap or recovery is needed.
        /// The FfiBootstrapContext provides callbacks for fetch_snapshot, fetch_outbox,
        /// and checkpoint read/write.
        fn bootstrap_fn(state: *mut, ctx: *const FfiBootstrapContext) -> FfiResult,
    }
}

// ============================================================================
// Bootstrap provider vtable
// ============================================================================

drasi_ffi_primitives::ffi_vtable! {
    /// FFI-safe vtable for a BootstrapProvider.
    /// The bootstrap plugin creates this; the host wraps it and passes it to the source plugin.
    pub struct BootstrapProviderVtable {
        /// Perform bootstrap. Sends records via the FfiBootstrapSender.
        /// Returns a heap-allocated FfiBootstrapResult (caller must free via Box::from_raw),
        /// or null on catastrophic failure.
        fn bootstrap_fn(state: *mut, query_id: FfiStr, node_labels: *const FfiStr, node_labels_count: usize, relation_labels: *const FfiStr, relation_labels_count: usize, request_id: FfiStr, server_id: FfiStr, source_id: FfiStr, sender: *mut FfiBootstrapSender) -> *mut FfiBootstrapResult,
    }
}

// ============================================================================
// Plugin descriptor vtables — factories that create Source/Reaction instances
// ============================================================================

drasi_ffi_primitives::ffi_vtable! {
    /// FFI-safe vtable for a SourcePluginDescriptor (factory).
    /// The host calls `create_source_fn` to construct a SourceVtable from config.
    pub struct SourcePluginVtable {
        fn kind_fn(state: *const) -> FfiStr,
        fn config_version_fn(state: *const) -> FfiStr,
        fn config_schema_json_fn(state: *const) -> FfiOwnedStr,
        fn config_schema_name_fn(state: *const) -> FfiStr,

        fn create_source_fn(state: *mut, id: FfiStr, config_json: FfiStr, auto_start: bool) -> FfiCreateResult,
    }
}

drasi_ffi_primitives::ffi_vtable! {
    /// FFI-safe vtable for a ReactionPluginDescriptor (factory).
    pub struct ReactionPluginVtable {
        fn kind_fn(state: *const) -> FfiStr,
        fn config_version_fn(state: *const) -> FfiStr,
        fn config_schema_json_fn(state: *const) -> FfiOwnedStr,
        fn config_schema_name_fn(state: *const) -> FfiStr,

        /// Factory: create a ReactionVtable from JSON config.
        fn create_reaction_fn(state: *mut, id: FfiStr, query_ids_json: FfiStr, config_json: FfiStr, auto_start: bool) -> FfiCreateResult,
    }
}

drasi_ffi_primitives::ffi_vtable! {
    /// FFI-safe vtable for a BootstrapPluginDescriptor (factory).
    pub struct BootstrapPluginVtable {
        fn kind_fn(state: *const) -> FfiStr,
        fn config_version_fn(state: *const) -> FfiStr,
        fn config_schema_json_fn(state: *const) -> FfiOwnedStr,
        fn config_schema_name_fn(state: *const) -> FfiStr,

        /// Factory: create a BootstrapProviderVtable from JSON config.
        fn create_bootstrap_provider_fn(state: *mut, config_json: FfiStr, source_config_json: FfiStr) -> FfiCreateResult,
    }
}

drasi_ffi_primitives::ffi_vtable! {
    /// FFI-safe vtable for an IdentityProviderPluginDescriptor (factory).
    /// The host calls `create_identity_provider_fn` to construct an
    /// `IdentityProviderVtable` from config JSON.
    pub struct IdentityProviderPluginVtable {
        fn kind_fn(state: *const) -> FfiStr,
        fn config_version_fn(state: *const) -> FfiStr,
        fn config_schema_json_fn(state: *const) -> FfiOwnedStr,
        fn config_schema_name_fn(state: *const) -> FfiStr,

        /// Factory: create an IdentityProviderVtable from JSON config.
        fn create_identity_provider_fn(state: *mut, config_json: FfiStr) -> *mut super::identity::IdentityProviderVtable,
    }
}

drasi_ffi_primitives::ffi_vtable! {
    /// FFI-safe vtable for a SecretStorePluginDescriptor (factory).
    /// The host calls `create_secret_store_fn` to construct a
    /// `SecretStoreProviderVtable` from config JSON.
    pub struct SecretStorePluginVtable {
        fn kind_fn(state: *const) -> FfiStr,
        fn config_version_fn(state: *const) -> FfiStr,
        fn config_schema_json_fn(state: *const) -> FfiOwnedStr,
        fn config_schema_name_fn(state: *const) -> FfiStr,

        /// Factory: create a SecretStoreProviderVtable from JSON config.
        fn create_secret_store_fn(state: *mut, config_json: FfiStr) -> *mut super::secret_store::SecretStoreProviderVtable,
    }
}

// ============================================================================
// State store vtable — reverse direction (host → plugin)
// ============================================================================

/// State store vtable — host creates and provides to plugins.
/// Plugins call these function pointers to access persistent state.
#[repr(C)]
pub struct StateStoreVtable {
    pub state: *mut c_void,
    // Basic operations
    pub get_fn: extern "C" fn(state: *mut c_void, store_id: FfiStr, key: FfiStr) -> FfiGetResult,
    pub set_fn: extern "C" fn(
        state: *mut c_void,
        store_id: FfiStr,
        key: FfiStr,
        value: *const u8,
        value_len: usize,
    ) -> FfiResult,
    pub delete_fn: extern "C" fn(state: *mut c_void, store_id: FfiStr, key: FfiStr) -> FfiResult,
    pub contains_key_fn:
        extern "C" fn(state: *mut c_void, store_id: FfiStr, key: FfiStr) -> FfiResult,
    // Batch operations (keys passed as FfiStr arrays)
    pub get_many_fn: extern "C" fn(
        state: *mut c_void,
        store_id: FfiStr,
        keys: *const FfiStr,
        keys_count: usize,
        out_values: *mut FfiGetResult,
    ) -> FfiResult,
    pub set_many_fn: extern "C" fn(
        state: *mut c_void,
        store_id: FfiStr,
        keys: *const FfiStr,
        values: *const *const u8,
        value_lens: *const usize,
        count: usize,
    ) -> FfiResult,
    pub delete_many_fn: extern "C" fn(
        state: *mut c_void,
        store_id: FfiStr,
        keys: *const FfiStr,
        keys_count: usize,
    ) -> i64,
    // Store-level operations
    pub clear_store_fn: extern "C" fn(state: *mut c_void, store_id: FfiStr) -> i64,
    pub list_keys_fn: extern "C" fn(state: *mut c_void, store_id: FfiStr) -> FfiStringArray,
    pub store_exists_fn: extern "C" fn(state: *mut c_void, store_id: FfiStr) -> FfiResult,
    pub key_count_fn: extern "C" fn(state: *mut c_void, store_id: FfiStr) -> i64,
    pub sync_fn: extern "C" fn(state: *mut c_void) -> FfiResult,
    // Cleanup
    pub drop_fn: extern "C" fn(state: *mut c_void),
}

unsafe impl Send for StateStoreVtable {}
unsafe impl Sync for StateStoreVtable {}

// ============================================================================
// Snapshot fetcher vtable — host creates and provides to plugins for on-demand
// query snapshot access at runtime (not just during bootstrap).
// ============================================================================

/// Snapshot fetcher vtable — host creates and provides to plugins.
///
/// Plugins call `fetch_snapshot_fn` to get the current result set of a query
/// as a streaming iterator. The callback accepts a `query_id` parameter
/// (unlike bootstrap callbacks which are per-query).
///
/// Follows the same ownership pattern as [`StateStoreVtable`]:
/// - Host `Box::into_raw`s an `Arc<dyn SnapshotFetcher>` into `state`
/// - Plugin calls `fetch_snapshot_fn` with `state` and `query_id`
/// - `drop_fn` reclaims the `Arc` when the plugin drops the proxy
#[repr(C)]
pub struct SnapshotFetcherVtable {
    /// Opaque host-owned state (Arc<dyn SnapshotFetcher> behind a raw pointer).
    pub state: *mut c_void,
    /// Fetch a snapshot for the given query ID.
    /// Returns an `FfiSnapshotIteratorResponse` with a streaming iterator on success,
    /// or a non-empty error string on failure.
    pub fetch_snapshot_fn:
        extern "C" fn(state: *mut c_void, query_id: FfiStr) -> FfiSnapshotIteratorResponse,
    /// Drop the host-owned state. Must be called exactly once when the plugin
    /// no longer needs the fetcher.
    pub drop_fn: extern "C" fn(state: *mut c_void),
}

unsafe impl Send for SnapshotFetcherVtable {}
unsafe impl Sync for SnapshotFetcherVtable {}

// ============================================================================
// Plugin registration — returned by drasi_plugin_init() for cdylib builds
// ============================================================================

/// FFI-safe plugin registration returned by `drasi_plugin_init()`.
/// Contains factory vtables for all plugin types this shared library provides.
///
/// **ABI note**: This struct is `#[repr(C)]`. New fields must always be appended
/// at the end so that plugins compiled against an older SDK layout remain
/// compatible (the host simply treats trailing fields as absent).
#[repr(C)]
pub struct FfiPluginRegistration {
    pub source_plugins: *mut SourcePluginVtable,
    pub source_plugin_count: usize,
    pub reaction_plugins: *mut ReactionPluginVtable,
    pub reaction_plugin_count: usize,
    pub bootstrap_plugins: *mut BootstrapPluginVtable,
    pub bootstrap_plugin_count: usize,
    /// Host calls this to provide a log callback with an opaque context pointer.
    /// The plugin stores both the callback and context, passing context back on every call.
    pub set_log_callback:
        extern "C" fn(ctx: *mut ::std::ffi::c_void, callback: super::callbacks::LogCallbackFn),
    /// Host calls this to provide a lifecycle event callback with an opaque context pointer.
    pub set_lifecycle_callback: extern "C" fn(
        ctx: *mut ::std::ffi::c_void,
        callback: super::callbacks::LifecycleCallbackFn,
    ),
    // --- Fields below were added after the initial ABI. They MUST remain at
    // the end so that older plugin binaries (which allocate a smaller struct)
    // are still layout-compatible with the host.
    pub identity_provider_plugins: *mut IdentityProviderPluginVtable,
    pub identity_provider_plugin_count: usize,
    pub secret_store_plugins: *mut SecretStorePluginVtable,
    pub secret_store_plugin_count: usize,
    /// Host calls this to provide a config value resolver callback.
    /// The plugin stores both the callback and context, using them in
    /// `DtoMapper` to resolve `ConfigValue::Secret` and other references
    /// back through the host.
    pub set_config_resolver:
        extern "C" fn(ctx: *mut ::std::ffi::c_void, callback: super::callbacks::ConfigResolverFn),
}