Skip to main content

statevec_api/
plugin_abi_v1.rs

1// Copyright 2026 Jumpex Technology.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::ffi::c_void;
5
6use crate::{
7    BizInvariantReadContext, RecordKey, RecordKind, RuntimeCommandRef, RuntimeHostContext,
8    RuntimeHostError, RuntimePlugin, RuntimePluginFactory, SysId,
9};
10
11/// Status returned by FFI ABI calls.
12#[repr(C)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum RuntimeCallStatus {
15    /// Call completed successfully.
16    Success = 0,
17    /// Call failed and may have written an error buffer.
18    Failure = 1,
19}
20
21/// Phase in which an ABI error occurred.
22#[repr(C)]
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum RuntimeErrorPhase {
25    /// Plugin metadata or schema loading.
26    Load = 1,
27    /// Runtime plugin instance creation.
28    Create = 2,
29    /// Transaction execution.
30    RunTx = 3,
31    /// Business invariant validation.
32    ValidateBizInvariants = 4,
33    /// Plugin unload.
34    Unload = 5,
35}
36
37/// Stable numeric runtime error kind.
38#[repr(transparent)]
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub struct RuntimeErrorKind(pub u16);
41
42/// Well-known runtime error kinds used by ABI helpers.
43pub mod runtime_error_kind {
44    use super::RuntimeErrorKind;
45
46    /// ABI precondition violation.
47    pub const ABI_CONTRACT_VIOLATION: RuntimeErrorKind = RuntimeErrorKind(1);
48
49    /// Loaded plugin schema did not match host expectations.
50    pub const SCHEMA_MISMATCH: RuntimeErrorKind = RuntimeErrorKind(1001);
51    /// Plugin or host configuration was invalid.
52    pub const CONFIG_ERROR: RuntimeErrorKind = RuntimeErrorKind(1002);
53
54    /// Plugin rejected the command.
55    pub const PLUGIN_REJECTED: RuntimeErrorKind = RuntimeErrorKind(2001);
56    /// Command envelope or payload was invalid.
57    pub const INVALID_COMMAND: RuntimeErrorKind = RuntimeErrorKind(2002);
58
59    /// Host rejected the operation.
60    pub const HOST_REJECTED: RuntimeErrorKind = RuntimeErrorKind(4001);
61    /// Host encountered an internal error.
62    pub const HOST_INTERNAL_ERROR: RuntimeErrorKind = RuntimeErrorKind(4002);
63
64    /// Plugin encountered an internal error.
65    pub const PLUGIN_INTERNAL_ERROR: RuntimeErrorKind = RuntimeErrorKind(5001);
66}
67
68/// Borrowed immutable byte slice passed across the C ABI.
69///
70/// This is a view into memory owned by the caller. The callee must not retain
71/// the pointer or use it after the ABI call or visitor callback returns.
72#[repr(C)]
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct RuntimeBytesRef {
75    /// Pointer to the first byte, or null when `len == 0`.
76    pub ptr: *const u8,
77    /// Number of bytes available at `ptr`.
78    pub len: usize,
79}
80
81impl RuntimeBytesRef {
82    /// Returns an empty byte reference.
83    pub const fn empty() -> Self {
84        Self {
85            ptr: std::ptr::null(),
86            len: 0,
87        }
88    }
89
90    /// Creates a byte reference from a Rust slice.
91    pub fn from_slice(bytes: &[u8]) -> Self {
92        Self {
93            ptr: bytes.as_ptr(),
94            len: bytes.len(),
95        }
96    }
97}
98
99/// Borrowed mutable byte slice passed across the C ABI.
100///
101/// This is a uniquely borrowed view into memory owned by the caller. The callee
102/// may mutate the bytes only during the ABI call or visitor callback and must
103/// not retain the pointer after it returns.
104#[repr(C)]
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct RuntimeBytesMutRef {
107    /// Pointer to the first byte, or null when `len == 0`.
108    pub ptr: *mut u8,
109    /// Number of bytes available at `ptr`.
110    pub len: usize,
111}
112
113impl RuntimeBytesMutRef {
114    /// Returns an empty mutable byte reference.
115    pub const fn empty() -> Self {
116        Self {
117            ptr: std::ptr::null_mut(),
118            len: 0,
119        }
120    }
121
122    /// Creates a mutable byte reference from a Rust slice.
123    pub fn from_slice(bytes: &mut [u8]) -> Self {
124        Self {
125            ptr: bytes.as_mut_ptr(),
126            len: bytes.len(),
127        }
128    }
129}
130
131/// Fixed-size ABI error buffer.
132#[repr(C)]
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct RuntimeErrorBuf {
135    /// Error phase.
136    pub phase: RuntimeErrorPhase,
137    /// Error kind.
138    pub kind: RuntimeErrorKind,
139    /// Number of valid bytes in `message_buf`.
140    pub message_len: usize,
141    /// UTF-8 message bytes truncated to `RUNTIME_ERROR_MESSAGE_CAPACITY`.
142    pub message_buf: [u8; RUNTIME_ERROR_MESSAGE_CAPACITY],
143}
144
145impl RuntimeErrorBuf {
146    /// Creates an empty error buffer with phase and kind metadata.
147    pub const fn new(
148        phase: RuntimeErrorPhase,
149        kind: RuntimeErrorKind,
150        _message: RuntimeBytesRef,
151    ) -> Self {
152        Self {
153            phase,
154            kind,
155            message_len: 0,
156            message_buf: [0; RUNTIME_ERROR_MESSAGE_CAPACITY],
157        }
158    }
159}
160
161/// Maximum ABI error message bytes.
162pub const RUNTIME_ERROR_MESSAGE_CAPACITY: usize = 512;
163
164/// ABI view of a command envelope.
165#[repr(C)]
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub struct RuntimeCommandView {
168    /// Command kind.
169    pub command_kind: u8,
170    /// Source queue sequence.
171    pub ext_seq: u64,
172    /// Source-provided reference time in microseconds.
173    pub ref_ext_time_us: u64,
174    /// Encoded command payload.
175    pub payload: RuntimeBytesRef,
176}
177
178/// ABI view of a record key.
179#[repr(C)]
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub struct RuntimeRecordKeyView {
182    /// Stable record kind.
183    pub kind: RecordKind,
184    /// Host-assigned system id.
185    pub sys_id: SysId,
186}
187
188impl From<RecordKey> for RuntimeRecordKeyView {
189    fn from(value: RecordKey) -> Self {
190        Self {
191            kind: value.kind,
192            sys_id: value.sys_id,
193        }
194    }
195}
196
197impl From<RuntimeRecordKeyView> for RecordKey {
198    fn from(value: RuntimeRecordKeyView) -> Self {
199        Self {
200            kind: value.kind,
201            sys_id: value.sys_id,
202        }
203    }
204}
205
206/// Visitor callback for borrowed byte slices.
207pub type RuntimeBytesVisitor =
208    unsafe extern "C" fn(visitor_ctx: *mut c_void, bytes: RuntimeBytesRef);
209/// Visitor callback for uniquely borrowed mutable byte slices.
210pub type RuntimeBytesMutVisitor =
211    unsafe extern "C" fn(visitor_ctx: *mut c_void, bytes: RuntimeBytesMutRef);
212/// Visitor callback for record keys.
213pub type RuntimeRecordKeyVisitor =
214    unsafe extern "C" fn(visitor_ctx: *mut c_void, key: RuntimeRecordKeyView);
215
216/// ABI handle for mutable runtime host access.
217#[repr(C)]
218#[derive(Debug, Clone, Copy)]
219pub struct RuntimeHostContextV1 {
220    /// Host context pointer.
221    pub ctx_ptr: *mut c_void,
222    /// Host vtable pointer.
223    pub vtable: *const RuntimeHostVTableV1,
224}
225
226/// ABI handle for read-only runtime host access.
227#[repr(C)]
228#[derive(Debug, Clone, Copy)]
229pub struct RuntimeReadContextV1 {
230    /// Host context pointer.
231    pub ctx_ptr: *mut c_void,
232    /// Read-only host vtable pointer.
233    pub vtable: *const RuntimeReadVTableV1,
234}
235
236/// Mutable host vtable exposed to runtime plugins.
237#[repr(C)]
238pub struct RuntimeHostVTableV1 {
239    /// Read by system id.
240    pub with_read_typed_raw: unsafe extern "C" fn(
241        ctx_ptr: *const c_void,
242        record_kind: RecordKind,
243        sys_id: SysId,
244        visitor_ctx: *mut c_void,
245        visitor: RuntimeBytesVisitor,
246        out_found: *mut bool,
247        out_error: *mut RuntimeErrorBuf,
248    ) -> RuntimeCallStatus,
249    /// Read by primary key.
250    pub with_read_typed_by_pk_raw: unsafe extern "C" fn(
251        ctx_ptr: *const c_void,
252        record_kind: RecordKind,
253        pk: RuntimeBytesRef,
254        visitor_ctx: *mut c_void,
255        visitor: RuntimeBytesVisitor,
256        out_found: *mut bool,
257        out_error: *mut RuntimeErrorBuf,
258    ) -> RuntimeCallStatus,
259    /// Create a record.
260    pub create_typed_raw: unsafe extern "C" fn(
261        ctx_ptr: *mut c_void,
262        record_kind: RecordKind,
263        init_ctx: *mut c_void,
264        init: RuntimeBytesMutVisitor,
265        out_key: *mut RuntimeRecordKeyView,
266        out_error: *mut RuntimeErrorBuf,
267    ) -> RuntimeCallStatus,
268    /// Update a record by primary key.
269    pub update_typed_by_pk_raw: unsafe extern "C" fn(
270        ctx_ptr: *mut c_void,
271        record_kind: RecordKind,
272        pk: RuntimeBytesRef,
273        update_ctx: *mut c_void,
274        update: RuntimeBytesMutVisitor,
275        out_found: *mut bool,
276        out_error: *mut RuntimeErrorBuf,
277    ) -> RuntimeCallStatus,
278    /// Delete a record by primary key.
279    pub delete_by_pk_raw: unsafe extern "C" fn(
280        ctx_ptr: *mut c_void,
281        record_kind: RecordKind,
282        pk: RuntimeBytesRef,
283        out_deleted: *mut bool,
284        out_error: *mut RuntimeErrorBuf,
285    ) -> RuntimeCallStatus,
286    /// Emit an event.
287    pub emit_typed_event_raw: unsafe extern "C" fn(
288        ctx_ptr: *mut c_void,
289        event_kind: u8,
290        payload: RuntimeBytesRef,
291        out_error: *mut RuntimeErrorBuf,
292    ) -> RuntimeCallStatus,
293    /// Iterate record keys for one kind.
294    pub for_each_record_key_raw: unsafe extern "C" fn(
295        ctx_ptr: *const c_void,
296        kind: RecordKind,
297        visitor_ctx: *mut c_void,
298        visitor: RuntimeRecordKeyVisitor,
299        out_error: *mut RuntimeErrorBuf,
300    ) -> RuntimeCallStatus,
301    /// Emit host-side diagnostic text.
302    pub debug_log: unsafe extern "C" fn(
303        ctx_ptr: *mut c_void,
304        message: RuntimeBytesRef,
305        out_error: *mut RuntimeErrorBuf,
306    ) -> RuntimeCallStatus,
307}
308
309/// Read-only host vtable exposed to invariant validation.
310#[repr(C)]
311pub struct RuntimeReadVTableV1 {
312    /// Read by system id.
313    pub with_read_typed_raw: unsafe extern "C" fn(
314        ctx_ptr: *const c_void,
315        record_kind: RecordKind,
316        sys_id: SysId,
317        visitor_ctx: *mut c_void,
318        visitor: RuntimeBytesVisitor,
319        out_found: *mut bool,
320        out_error: *mut RuntimeErrorBuf,
321    ) -> RuntimeCallStatus,
322    /// Read by primary key.
323    pub with_read_typed_by_pk_raw: unsafe extern "C" fn(
324        ctx_ptr: *const c_void,
325        record_kind: RecordKind,
326        pk: RuntimeBytesRef,
327        visitor_ctx: *mut c_void,
328        visitor: RuntimeBytesVisitor,
329        out_found: *mut bool,
330        out_error: *mut RuntimeErrorBuf,
331    ) -> RuntimeCallStatus,
332    /// Iterate record keys for one kind.
333    pub for_each_record_key_raw: unsafe extern "C" fn(
334        ctx_ptr: *const c_void,
335        kind: RecordKind,
336        visitor_ctx: *mut c_void,
337        visitor: RuntimeRecordKeyVisitor,
338        out_error: *mut RuntimeErrorBuf,
339    ) -> RuntimeCallStatus,
340}
341
342/// Current stable runtime plugin ABI version.
343pub const RUNTIME_PLUGIN_ABI_VERSION_V1: u32 = 1;
344/// Null-terminated symbol name exported by StateVec runtime plugins.
345pub const RUNTIME_PLUGIN_ENTRY_V1_SYMBOL: &[u8] = b"statevec_runtime_plugin_entry_v1\0";
346
347/// ABI function table returned by a runtime plugin entrypoint.
348#[repr(C)]
349pub struct RuntimePluginApiV1 {
350    /// ABI version implemented by the plugin.
351    pub abi_version: u32,
352    /// Returns the plugin name.
353    pub plugin_name: unsafe extern "C" fn() -> RuntimeBytesRef,
354    /// Returns schema IDL JSON bytes.
355    pub schema_bytes: unsafe extern "C" fn() -> RuntimeBytesRef,
356    /// Creates a configured runtime plugin instance.
357    pub create_runtime: unsafe extern "C" fn(
358        config: RuntimeBytesRef,
359        out_runtime: *mut *mut c_void,
360        out_error: *mut RuntimeErrorBuf,
361    ) -> RuntimeCallStatus,
362    /// Destroys a runtime plugin instance.
363    pub destroy_runtime: unsafe extern "C" fn(runtime: *mut c_void),
364    /// Runs one command transaction.
365    pub run_tx: unsafe extern "C" fn(
366        runtime: *mut c_void,
367        host: RuntimeHostContextV1,
368        command: RuntimeCommandView,
369        out_error: *mut RuntimeErrorBuf,
370    ) -> RuntimeCallStatus,
371    /// Validates business invariants against read-only host state.
372    pub validate_biz_invariants: unsafe extern "C" fn(
373        runtime: *mut c_void,
374        host: RuntimeReadContextV1,
375        out_error: *mut RuntimeErrorBuf,
376    ) -> RuntimeCallStatus,
377    /// Unloads a runtime plugin instance.
378    pub on_unload: unsafe extern "C" fn(
379        runtime: *mut c_void,
380        out_error: *mut RuntimeErrorBuf,
381    ) -> RuntimeCallStatus,
382}
383
384/// Runtime plugin entrypoint function type.
385pub type RuntimePluginEntryV1 = unsafe extern "C" fn() -> RuntimePluginApiV1;
386
387/// Boxed plugin handle owned across the ABI boundary.
388pub struct ExportedRuntimePluginV1Handle {
389    /// Plugin instance.
390    pub plugin: Box<dyn RuntimePlugin>,
391}
392
393/// Converts an ABI byte reference into a Rust slice.
394///
395/// # Safety
396///
397/// The caller must ensure `bytes.ptr` is valid for `bytes.len` bytes for the
398/// returned lifetime.
399pub unsafe fn runtime_bytes_slice<'a>(bytes: RuntimeBytesRef) -> Result<&'a [u8], &'static str> {
400    if bytes.ptr.is_null() {
401        if bytes.len == 0 {
402            return Ok(&[]);
403        }
404        return Err("bytes pointer is null but len is non-zero");
405    }
406    Ok(unsafe { std::slice::from_raw_parts(bytes.ptr, bytes.len) })
407}
408
409/// Converts an ABI mutable byte reference into a mutable Rust slice.
410///
411/// # Safety
412///
413/// The caller must ensure `bytes.ptr` is valid and uniquely borrowed for
414/// `bytes.len` bytes for the returned lifetime.
415pub unsafe fn runtime_bytes_slice_mut<'a>(
416    bytes: RuntimeBytesMutRef,
417) -> Result<&'a mut [u8], &'static str> {
418    if bytes.ptr.is_null() {
419        if bytes.len == 0 {
420            return Ok(&mut []);
421        }
422        return Err("bytes pointer is null but len is non-zero");
423    }
424    Ok(unsafe { std::slice::from_raw_parts_mut(bytes.ptr, bytes.len) })
425}
426
427/// Resets an optional ABI error buffer.
428#[allow(clippy::not_unsafe_ptr_arg_deref)]
429pub fn clear_runtime_error(out_error: *mut RuntimeErrorBuf) {
430    if out_error.is_null() {
431        return;
432    }
433    unsafe {
434        *out_error = RuntimeErrorBuf::new(
435            RuntimeErrorPhase::Load,
436            runtime_error_kind::HOST_INTERNAL_ERROR,
437            RuntimeBytesRef::empty(),
438        );
439    }
440}
441
442/// Writes an error into an optional ABI error buffer.
443#[allow(clippy::not_unsafe_ptr_arg_deref)]
444pub fn write_runtime_error(
445    out_error: *mut RuntimeErrorBuf,
446    phase: RuntimeErrorPhase,
447    kind: RuntimeErrorKind,
448    message: &str,
449) {
450    if out_error.is_null() {
451        return;
452    }
453    let mut error = RuntimeErrorBuf::new(phase, kind, RuntimeBytesRef::empty());
454    let mut message_len = message.len().min(RUNTIME_ERROR_MESSAGE_CAPACITY);
455    while !message.is_char_boundary(message_len) {
456        message_len -= 1;
457    }
458    error.message_len = message_len;
459    error.message_buf[..message_len].copy_from_slice(&message.as_bytes()[..message_len]);
460    unsafe {
461        *out_error = error;
462    }
463}
464
465/// Returns the error text from an ABI error buffer.
466pub fn runtime_error_text(error: &RuntimeErrorBuf) -> String {
467    let message_len = error.message_len.min(RUNTIME_ERROR_MESSAGE_CAPACITY);
468    String::from_utf8_lossy(&error.message_buf[..message_len]).into_owned()
469}
470
471/// Formats an ABI error buffer for diagnostics.
472pub fn runtime_error_message(error: &RuntimeErrorBuf) -> String {
473    format!(
474        "phase={:?} kind={} message={}",
475        error.phase,
476        error.kind.0,
477        runtime_error_text(error)
478    )
479}
480
481/// ABI helper returning a plugin factory name.
482pub fn runtime_plugin_name_v1(factory: fn() -> Box<dyn RuntimePluginFactory>) -> RuntimeBytesRef {
483    let name = factory().plugin_name();
484    RuntimeBytesRef::from_slice(name.as_bytes())
485}
486
487/// ABI helper returning cached schema IDL JSON bytes.
488pub fn runtime_plugin_schema_bytes_v1(
489    factory: fn() -> Box<dyn RuntimePluginFactory>,
490    cache: &'static std::sync::OnceLock<String>,
491) -> RuntimeBytesRef {
492    let bytes = cache.get_or_init(|| factory().schema_registry().to_idl_json());
493    RuntimeBytesRef::from_slice(bytes.as_bytes())
494}
495
496unsafe fn exported_runtime_plugin_handle_mut(
497    runtime: *mut c_void,
498) -> Result<&'static mut ExportedRuntimePluginV1Handle, &'static str> {
499    if runtime.is_null() {
500        Err("runtime handle is null")
501    } else {
502        Ok(unsafe { &mut *runtime.cast::<ExportedRuntimePluginV1Handle>() })
503    }
504}
505
506/// Creates a runtime plugin instance through the v1 ABI helper.
507///
508/// # Safety
509///
510/// `out_runtime` and `out_error`, when non-null, must be valid for writes for
511/// the duration of the call. `config` must point to bytes that remain valid for
512/// the duration of the call.
513pub unsafe fn runtime_plugin_create_runtime_v1(
514    factory: fn() -> Box<dyn RuntimePluginFactory>,
515    config: RuntimeBytesRef,
516    out_runtime: *mut *mut c_void,
517    out_error: *mut RuntimeErrorBuf,
518) -> RuntimeCallStatus {
519    clear_runtime_error(out_error);
520
521    if out_runtime.is_null() {
522        write_runtime_error(
523            out_error,
524            RuntimeErrorPhase::Create,
525            runtime_error_kind::ABI_CONTRACT_VIOLATION,
526            "out_runtime is null",
527        );
528        return RuntimeCallStatus::Failure;
529    }
530
531    let config_bytes = match unsafe { runtime_bytes_slice(config) } {
532        Ok(bytes) => bytes,
533        Err(message) => {
534            write_runtime_error(
535                out_error,
536                RuntimeErrorPhase::Create,
537                runtime_error_kind::ABI_CONTRACT_VIOLATION,
538                message,
539            );
540            return RuntimeCallStatus::Failure;
541        }
542    };
543
544    let config_text = match std::str::from_utf8(config_bytes) {
545        Ok(value) => value,
546        Err(err) => {
547            write_runtime_error(
548                out_error,
549                RuntimeErrorPhase::Create,
550                runtime_error_kind::CONFIG_ERROR,
551                &format!("plugin config is not valid UTF-8: {err}"),
552            );
553            return RuntimeCallStatus::Failure;
554        }
555    };
556
557    let plugin = match factory().create(config_text) {
558        Ok(plugin) => plugin,
559        Err(err) => {
560            write_runtime_error(
561                out_error,
562                RuntimeErrorPhase::Create,
563                runtime_error_kind::CONFIG_ERROR,
564                &err.to_string(),
565            );
566            return RuntimeCallStatus::Failure;
567        }
568    };
569
570    let handle = Box::new(ExportedRuntimePluginV1Handle { plugin });
571    unsafe {
572        *out_runtime = Box::into_raw(handle).cast();
573    }
574    RuntimeCallStatus::Success
575}
576
577/// Destroys a runtime plugin instance created by
578/// [`runtime_plugin_create_runtime_v1`].
579///
580/// # Safety
581///
582/// `runtime` must be null or a handle previously returned by
583/// [`runtime_plugin_create_runtime_v1`] and not already destroyed.
584pub unsafe fn runtime_plugin_destroy_runtime_v1(runtime: *mut c_void) {
585    if runtime.is_null() {
586        return;
587    }
588    unsafe {
589        drop(Box::from_raw(
590            runtime.cast::<ExportedRuntimePluginV1Handle>(),
591        ));
592    }
593}
594
595/// Runs one command transaction through the v1 ABI helper.
596///
597/// # Safety
598///
599/// `runtime` must be a live plugin handle, `host` must contain a valid host
600/// context and vtable for the duration of the call, and `command.payload` and
601/// `out_error` must remain valid for the duration of the call.
602pub unsafe fn runtime_plugin_run_tx_v1(
603    runtime: *mut c_void,
604    host: RuntimeHostContextV1,
605    command: RuntimeCommandView,
606    out_error: *mut RuntimeErrorBuf,
607) -> RuntimeCallStatus {
608    clear_runtime_error(out_error);
609
610    let payload = match unsafe { runtime_bytes_slice(command.payload) } {
611        Ok(bytes) => bytes,
612        Err(message) => {
613            write_runtime_error(
614                out_error,
615                RuntimeErrorPhase::RunTx,
616                runtime_error_kind::ABI_CONTRACT_VIOLATION,
617                message,
618            );
619            return RuntimeCallStatus::Failure;
620        }
621    };
622
623    let handle = match unsafe { exported_runtime_plugin_handle_mut(runtime) } {
624        Ok(handle) => handle,
625        Err(message) => {
626            write_runtime_error(
627                out_error,
628                RuntimeErrorPhase::RunTx,
629                runtime_error_kind::ABI_CONTRACT_VIOLATION,
630                message,
631            );
632            return RuntimeCallStatus::Failure;
633        }
634    };
635
636    let mut host_adapter = RuntimeHostContextV1Adapter::new(host);
637    let command = RuntimeCommandRef::new(
638        command.command_kind,
639        command.ext_seq,
640        command.ref_ext_time_us,
641        payload,
642    );
643    match handle.plugin.run_tx(&mut host_adapter, &command) {
644        Ok(()) => RuntimeCallStatus::Success,
645        Err(err) => {
646            write_runtime_error(
647                out_error,
648                RuntimeErrorPhase::RunTx,
649                runtime_error_kind::PLUGIN_REJECTED,
650                &err.to_string(),
651            );
652            RuntimeCallStatus::Failure
653        }
654    }
655}
656
657/// Validates plugin business invariants through the v1 ABI helper.
658///
659/// # Safety
660///
661/// `runtime` must be a live plugin handle, `host` must contain a valid
662/// read-only host context and vtable for the duration of the call, and
663/// `out_error` must be null or valid for writes.
664pub unsafe fn runtime_plugin_validate_biz_invariants_v1(
665    runtime: *mut c_void,
666    host: RuntimeReadContextV1,
667    out_error: *mut RuntimeErrorBuf,
668) -> RuntimeCallStatus {
669    clear_runtime_error(out_error);
670
671    let handle = match unsafe { exported_runtime_plugin_handle_mut(runtime) } {
672        Ok(handle) => handle,
673        Err(message) => {
674            write_runtime_error(
675                out_error,
676                RuntimeErrorPhase::ValidateBizInvariants,
677                runtime_error_kind::ABI_CONTRACT_VIOLATION,
678                message,
679            );
680            return RuntimeCallStatus::Failure;
681        }
682    };
683
684    let host_adapter = RuntimeReadContextV1Adapter::new(host);
685    match handle.plugin.validate_biz_invariants(&host_adapter) {
686        Ok(()) => RuntimeCallStatus::Success,
687        Err(err) => {
688            write_runtime_error(
689                out_error,
690                RuntimeErrorPhase::ValidateBizInvariants,
691                runtime_error_kind::PLUGIN_REJECTED,
692                &err,
693            );
694            RuntimeCallStatus::Failure
695        }
696    }
697}
698
699/// Calls the plugin unload hook through the v1 ABI helper.
700///
701/// # Safety
702///
703/// `runtime` must be a live plugin handle and `out_error` must be null or valid
704/// for writes.
705pub unsafe fn runtime_plugin_on_unload_v1(
706    runtime: *mut c_void,
707    out_error: *mut RuntimeErrorBuf,
708) -> RuntimeCallStatus {
709    clear_runtime_error(out_error);
710
711    let handle = match unsafe { exported_runtime_plugin_handle_mut(runtime) } {
712        Ok(handle) => handle,
713        Err(message) => {
714            write_runtime_error(
715                out_error,
716                RuntimeErrorPhase::Unload,
717                runtime_error_kind::ABI_CONTRACT_VIOLATION,
718                message,
719            );
720            return RuntimeCallStatus::Failure;
721        }
722    };
723
724    match handle.plugin.on_unload() {
725        Ok(()) => RuntimeCallStatus::Success,
726        Err(err) => {
727            write_runtime_error(
728                out_error,
729                RuntimeErrorPhase::Unload,
730                runtime_error_kind::PLUGIN_INTERNAL_ERROR,
731                &err.to_string(),
732            );
733            RuntimeCallStatus::Failure
734        }
735    }
736}
737
738struct BytesVisitorForward<'a> {
739    callback: &'a mut dyn FnMut(&[u8]),
740}
741
742struct BytesVisitorForwardMut<'a> {
743    callback: &'a mut dyn FnMut(&mut [u8]),
744}
745
746struct RecordKeyVisitorForward<'a> {
747    callback: &'a mut dyn FnMut(RecordKey),
748}
749
750unsafe extern "C" fn forward_bytes_visitor(visitor_ctx: *mut c_void, bytes: RuntimeBytesRef) {
751    let forward = unsafe { &mut *visitor_ctx.cast::<BytesVisitorForward<'_>>() };
752    let bytes = unsafe { runtime_bytes_slice(bytes) }.expect("host returned invalid bytes");
753    (forward.callback)(bytes);
754}
755
756unsafe extern "C" fn forward_bytes_visitor_mut(
757    visitor_ctx: *mut c_void,
758    bytes: RuntimeBytesMutRef,
759) {
760    let forward = unsafe { &mut *visitor_ctx.cast::<BytesVisitorForwardMut<'_>>() };
761    let bytes =
762        unsafe { runtime_bytes_slice_mut(bytes) }.expect("host returned invalid mutable bytes");
763    (forward.callback)(bytes);
764}
765
766unsafe extern "C" fn forward_record_key_visitor(
767    visitor_ctx: *mut c_void,
768    key: RuntimeRecordKeyView,
769) {
770    let forward = unsafe { &mut *visitor_ctx.cast::<RecordKeyVisitorForward<'_>>() };
771    (forward.callback)(key.into());
772}
773
774#[derive(Debug, Clone, Copy)]
775pub struct RuntimeHostContextV1Adapter {
776    raw: RuntimeHostContextV1,
777}
778
779impl RuntimeHostContextV1Adapter {
780    pub const fn new(raw: RuntimeHostContextV1) -> Self {
781        Self { raw }
782    }
783
784    fn vtable(&self) -> Result<&RuntimeHostVTableV1, RuntimeHostError> {
785        unsafe { self.raw.vtable.as_ref() }
786            .ok_or_else(|| RuntimeHostError::new("runtime host vtable is null"))
787    }
788}
789
790impl RuntimeHostContext for RuntimeHostContextV1Adapter {
791    fn with_read_typed_raw(
792        &self,
793        record_kind: RecordKind,
794        sys_id: SysId,
795        f: &mut dyn FnMut(&[u8]),
796    ) -> Result<bool, RuntimeHostError> {
797        let mut found = false;
798        let mut error = RuntimeErrorBuf::new(
799            RuntimeErrorPhase::RunTx,
800            runtime_error_kind::HOST_INTERNAL_ERROR,
801            RuntimeBytesRef::empty(),
802        );
803        let mut visitor = BytesVisitorForward { callback: f };
804        let status = unsafe {
805            (self.vtable()?.with_read_typed_raw)(
806                self.raw.ctx_ptr.cast_const(),
807                record_kind,
808                sys_id,
809                (&mut visitor as *mut BytesVisitorForward<'_>).cast(),
810                forward_bytes_visitor,
811                &mut found,
812                &mut error,
813            )
814        };
815        match status {
816            RuntimeCallStatus::Success => Ok(found),
817            RuntimeCallStatus::Failure => Err(RuntimeHostError::new(runtime_error_message(&error))),
818        }
819    }
820
821    fn with_read_typed_by_pk_raw(
822        &self,
823        record_kind: RecordKind,
824        pk: &[u8],
825        f: &mut dyn FnMut(&[u8]),
826    ) -> Result<bool, RuntimeHostError> {
827        let mut found = false;
828        let mut error = RuntimeErrorBuf::new(
829            RuntimeErrorPhase::RunTx,
830            runtime_error_kind::HOST_INTERNAL_ERROR,
831            RuntimeBytesRef::empty(),
832        );
833        let mut visitor = BytesVisitorForward { callback: f };
834        let status = unsafe {
835            (self.vtable()?.with_read_typed_by_pk_raw)(
836                self.raw.ctx_ptr.cast_const(),
837                record_kind,
838                RuntimeBytesRef::from_slice(pk),
839                (&mut visitor as *mut BytesVisitorForward<'_>).cast(),
840                forward_bytes_visitor,
841                &mut found,
842                &mut error,
843            )
844        };
845        match status {
846            RuntimeCallStatus::Success => Ok(found),
847            RuntimeCallStatus::Failure => Err(RuntimeHostError::new(runtime_error_message(&error))),
848        }
849    }
850
851    fn create_typed_raw(
852        &mut self,
853        record_kind: RecordKind,
854        init: &mut dyn FnMut(&mut [u8]),
855    ) -> Result<RecordKey, RuntimeHostError> {
856        let mut key = RuntimeRecordKeyView { kind: 0, sys_id: 0 };
857        let mut error = RuntimeErrorBuf::new(
858            RuntimeErrorPhase::RunTx,
859            runtime_error_kind::HOST_INTERNAL_ERROR,
860            RuntimeBytesRef::empty(),
861        );
862        let mut visitor = BytesVisitorForwardMut { callback: init };
863        let status = unsafe {
864            (self.vtable()?.create_typed_raw)(
865                self.raw.ctx_ptr,
866                record_kind,
867                (&mut visitor as *mut BytesVisitorForwardMut<'_>).cast(),
868                forward_bytes_visitor_mut,
869                &mut key,
870                &mut error,
871            )
872        };
873        match status {
874            RuntimeCallStatus::Success => Ok(key.into()),
875            RuntimeCallStatus::Failure => Err(RuntimeHostError::new(runtime_error_message(&error))),
876        }
877    }
878
879    fn update_typed_by_pk_raw(
880        &mut self,
881        record_kind: RecordKind,
882        pk: &[u8],
883        f: &mut dyn FnMut(&mut [u8]),
884    ) -> Result<bool, RuntimeHostError> {
885        let mut found = false;
886        let mut error = RuntimeErrorBuf::new(
887            RuntimeErrorPhase::RunTx,
888            runtime_error_kind::HOST_INTERNAL_ERROR,
889            RuntimeBytesRef::empty(),
890        );
891        let mut visitor = BytesVisitorForwardMut { callback: f };
892        let status = unsafe {
893            (self.vtable()?.update_typed_by_pk_raw)(
894                self.raw.ctx_ptr,
895                record_kind,
896                RuntimeBytesRef::from_slice(pk),
897                (&mut visitor as *mut BytesVisitorForwardMut<'_>).cast(),
898                forward_bytes_visitor_mut,
899                &mut found,
900                &mut error,
901            )
902        };
903        match status {
904            RuntimeCallStatus::Success => Ok(found),
905            RuntimeCallStatus::Failure => Err(RuntimeHostError::new(runtime_error_message(&error))),
906        }
907    }
908
909    fn delete_by_pk_raw(
910        &mut self,
911        record_kind: RecordKind,
912        pk: &[u8],
913    ) -> Result<bool, RuntimeHostError> {
914        let mut deleted = false;
915        let mut error = RuntimeErrorBuf::new(
916            RuntimeErrorPhase::RunTx,
917            runtime_error_kind::HOST_INTERNAL_ERROR,
918            RuntimeBytesRef::empty(),
919        );
920        let status = unsafe {
921            (self.vtable()?.delete_by_pk_raw)(
922                self.raw.ctx_ptr,
923                record_kind,
924                RuntimeBytesRef::from_slice(pk),
925                &mut deleted,
926                &mut error,
927            )
928        };
929        match status {
930            RuntimeCallStatus::Success => Ok(deleted),
931            RuntimeCallStatus::Failure => Err(RuntimeHostError::new(runtime_error_message(&error))),
932        }
933    }
934
935    fn emit_typed_event_raw(
936        &mut self,
937        event_kind: u8,
938        payload: &[u8],
939    ) -> Result<(), RuntimeHostError> {
940        let mut error = RuntimeErrorBuf::new(
941            RuntimeErrorPhase::RunTx,
942            runtime_error_kind::HOST_INTERNAL_ERROR,
943            RuntimeBytesRef::empty(),
944        );
945        let status = unsafe {
946            (self.vtable()?.emit_typed_event_raw)(
947                self.raw.ctx_ptr,
948                event_kind,
949                RuntimeBytesRef::from_slice(payload),
950                &mut error,
951            )
952        };
953        match status {
954            RuntimeCallStatus::Success => Ok(()),
955            RuntimeCallStatus::Failure => Err(RuntimeHostError::new(runtime_error_message(&error))),
956        }
957    }
958
959    fn for_each_record_key_raw(
960        &self,
961        kind: RecordKind,
962        f: &mut dyn FnMut(RecordKey),
963    ) -> Result<(), RuntimeHostError> {
964        let mut error = RuntimeErrorBuf::new(
965            RuntimeErrorPhase::RunTx,
966            runtime_error_kind::HOST_INTERNAL_ERROR,
967            RuntimeBytesRef::empty(),
968        );
969        let mut visitor = RecordKeyVisitorForward { callback: f };
970        let status = unsafe {
971            (self.vtable()?.for_each_record_key_raw)(
972                self.raw.ctx_ptr.cast_const(),
973                kind,
974                (&mut visitor as *mut RecordKeyVisitorForward<'_>).cast(),
975                forward_record_key_visitor,
976                &mut error,
977            )
978        };
979        match status {
980            RuntimeCallStatus::Success => Ok(()),
981            RuntimeCallStatus::Failure => Err(RuntimeHostError::new(runtime_error_message(&error))),
982        }
983    }
984
985    fn debug_log(&mut self, message: String) -> Result<(), RuntimeHostError> {
986        let mut error = RuntimeErrorBuf::new(
987            RuntimeErrorPhase::RunTx,
988            runtime_error_kind::HOST_INTERNAL_ERROR,
989            RuntimeBytesRef::empty(),
990        );
991        let status = unsafe {
992            (self.vtable()?.debug_log)(
993                self.raw.ctx_ptr,
994                RuntimeBytesRef::from_slice(message.as_bytes()),
995                &mut error,
996            )
997        };
998        match status {
999            RuntimeCallStatus::Success => Ok(()),
1000            RuntimeCallStatus::Failure => Err(RuntimeHostError::new(runtime_error_message(&error))),
1001        }
1002    }
1003}
1004
1005#[derive(Debug, Clone, Copy)]
1006pub struct RuntimeReadContextV1Adapter {
1007    raw: RuntimeReadContextV1,
1008}
1009
1010impl RuntimeReadContextV1Adapter {
1011    pub const fn new(raw: RuntimeReadContextV1) -> Self {
1012        Self { raw }
1013    }
1014
1015    fn vtable(&self) -> Result<&RuntimeReadVTableV1, RuntimeHostError> {
1016        unsafe { self.raw.vtable.as_ref() }
1017            .ok_or_else(|| RuntimeHostError::new("runtime read vtable is null"))
1018    }
1019}
1020
1021impl BizInvariantReadContext for RuntimeReadContextV1Adapter {
1022    fn with_read_typed_raw(
1023        &self,
1024        record_kind: RecordKind,
1025        sys_id: SysId,
1026        f: &mut dyn FnMut(&[u8]),
1027    ) -> Result<bool, RuntimeHostError> {
1028        let mut found = false;
1029        let mut error = RuntimeErrorBuf::new(
1030            RuntimeErrorPhase::ValidateBizInvariants,
1031            runtime_error_kind::HOST_INTERNAL_ERROR,
1032            RuntimeBytesRef::empty(),
1033        );
1034        let mut visitor = BytesVisitorForward { callback: f };
1035        let status = unsafe {
1036            (self.vtable()?.with_read_typed_raw)(
1037                self.raw.ctx_ptr.cast_const(),
1038                record_kind,
1039                sys_id,
1040                (&mut visitor as *mut BytesVisitorForward<'_>).cast(),
1041                forward_bytes_visitor,
1042                &mut found,
1043                &mut error,
1044            )
1045        };
1046        match status {
1047            RuntimeCallStatus::Success => Ok(found),
1048            RuntimeCallStatus::Failure => Err(RuntimeHostError::new(runtime_error_message(&error))),
1049        }
1050    }
1051
1052    fn with_read_typed_by_pk_raw(
1053        &self,
1054        record_kind: RecordKind,
1055        pk: &[u8],
1056        f: &mut dyn FnMut(&[u8]),
1057    ) -> Result<bool, RuntimeHostError> {
1058        let mut found = false;
1059        let mut error = RuntimeErrorBuf::new(
1060            RuntimeErrorPhase::ValidateBizInvariants,
1061            runtime_error_kind::HOST_INTERNAL_ERROR,
1062            RuntimeBytesRef::empty(),
1063        );
1064        let mut visitor = BytesVisitorForward { callback: f };
1065        let status = unsafe {
1066            (self.vtable()?.with_read_typed_by_pk_raw)(
1067                self.raw.ctx_ptr.cast_const(),
1068                record_kind,
1069                RuntimeBytesRef::from_slice(pk),
1070                (&mut visitor as *mut BytesVisitorForward<'_>).cast(),
1071                forward_bytes_visitor,
1072                &mut found,
1073                &mut error,
1074            )
1075        };
1076        match status {
1077            RuntimeCallStatus::Success => Ok(found),
1078            RuntimeCallStatus::Failure => Err(RuntimeHostError::new(runtime_error_message(&error))),
1079        }
1080    }
1081
1082    fn for_each_record_key_raw(
1083        &self,
1084        kind: RecordKind,
1085        f: &mut dyn FnMut(RecordKey),
1086    ) -> Result<(), RuntimeHostError> {
1087        let mut error = RuntimeErrorBuf::new(
1088            RuntimeErrorPhase::ValidateBizInvariants,
1089            runtime_error_kind::HOST_INTERNAL_ERROR,
1090            RuntimeBytesRef::empty(),
1091        );
1092        let mut visitor = RecordKeyVisitorForward { callback: f };
1093        let status = unsafe {
1094            (self.vtable()?.for_each_record_key_raw)(
1095                self.raw.ctx_ptr.cast_const(),
1096                kind,
1097                (&mut visitor as *mut RecordKeyVisitorForward<'_>).cast(),
1098                forward_record_key_visitor,
1099                &mut error,
1100            )
1101        };
1102        match status {
1103            RuntimeCallStatus::Success => Ok(()),
1104            RuntimeCallStatus::Failure => Err(RuntimeHostError::new(runtime_error_message(&error))),
1105        }
1106    }
1107}