1use std::{marker::PhantomData, panic::AssertUnwindSafe};
2
3use radixdb_plugin_abi::{
4 RadixAbiBatchViewV1, RadixAbiCallContextV1, RadixAbiColumnViewV1, RadixAbiDiagnosticV1,
5 RadixAbiHashSinkV1, RadixAbiHeaderV1, RadixAbiResultBuilderV1, RadixAbiSliceV1,
6 RadixAbiStatusV1, RadixAbiValueV1, RADIX_COLUMN_LAYOUT_FIXED, RADIX_DIAGNOSTIC_CANCELLED,
7 RADIX_DIAGNOSTIC_DOMAIN, RADIX_DIAGNOSTIC_INTERNAL, RADIX_DIAGNOSTIC_INVALID_INPUT,
8 RADIX_DIAGNOSTIC_LIMIT, RADIX_DIAGNOSTIC_PLUGIN_PANIC, RADIX_HASH_COMPONENT_BYTES,
9 RADIX_HASH_COMPONENT_F64_BITS, RADIX_HASH_COMPONENT_I64, RADIX_HASH_COMPONENT_U64,
10 RADIX_RESULT_ITEM_FLAG_NULL, RADIX_STATUS_CANCELLED, RADIX_STATUS_CONTRACT_VIOLATION,
11 RADIX_STATUS_DOMAIN_ERROR, RADIX_STATUS_INTERNAL_ERROR, RADIX_STATUS_INVALID_ARGUMENT,
12 RADIX_STATUS_LIMIT_EXCEEDED, RADIX_STATUS_OK, RADIX_STATUS_PANIC, RADIX_TYPE_REF_EXTERNAL,
13};
14
15use crate::{value::AbiValue, PluginError, PluginErrorKind, PluginResult, RadixType, ValueType};
16
17pub struct CallContext<'a> {
18 raw: &'a RadixAbiCallContextV1,
19}
20
21impl CallContext<'_> {
22 pub fn check_cancelled(&self) -> PluginResult<()> {
23 let callback = self
24 .raw
25 .check_cancelled
26 .ok_or_else(|| PluginError::internal("missing cancellation callback"))?;
27 match unsafe { callback(self.raw.handle) } {
29 RADIX_STATUS_OK => Ok(()),
30 RADIX_STATUS_CANCELLED => Err(PluginError::cancelled()),
31 _ => Err(PluginError::internal(
32 "host cancellation callback violated its contract",
33 )),
34 }
35 }
36
37 pub fn charge_work(&self, units: u32) -> PluginResult<()> {
38 let callback = self
39 .raw
40 .charge_work
41 .ok_or_else(|| PluginError::internal("missing work-accounting callback"))?;
42 match unsafe { callback(self.raw.handle, units) } {
44 RADIX_STATUS_OK => Ok(()),
45 RADIX_STATUS_LIMIT_EXCEEDED => {
46 Err(PluginError::limit_exceeded("plugin work budget exhausted"))
47 }
48 RADIX_STATUS_CANCELLED => Err(PluginError::cancelled()),
49 _ => Err(PluginError::internal(
50 "host work callback violated its contract",
51 )),
52 }
53 }
54
55 pub fn deadline_unix_ns(&self) -> u64 {
56 self.raw.deadline_unix_ns
57 }
58
59 pub fn max_output_bytes(&self) -> u32 {
60 self.raw.max_output_bytes
61 }
62}
63
64pub struct HashSink<'a> {
65 backend: HashSinkBackend<'a>,
66}
67
68enum HashSinkBackend<'a> {
69 Abi(&'a RadixAbiHashSinkV1),
70 Test(&'a mut Vec<(u16, Vec<u8>)>),
71}
72
73impl HashSink<'_> {
74 pub(crate) fn for_testing(components: &mut Vec<(u16, Vec<u8>)>) -> HashSink<'_> {
75 HashSink {
76 backend: HashSinkBackend::Test(components),
77 }
78 }
79
80 pub fn bytes(&mut self, value: &[u8]) -> PluginResult<()> {
81 self.append(RADIX_HASH_COMPONENT_BYTES, value)
82 }
83
84 pub fn i64(&mut self, value: i64) -> PluginResult<()> {
85 self.append(RADIX_HASH_COMPONENT_I64, &value.to_le_bytes())
86 }
87
88 pub fn u64(&mut self, value: u64) -> PluginResult<()> {
89 self.append(RADIX_HASH_COMPONENT_U64, &value.to_le_bytes())
90 }
91
92 pub fn f64_bits(&mut self, value: f64) -> PluginResult<()> {
93 self.append(
94 RADIX_HASH_COMPONENT_F64_BITS,
95 &value.to_bits().to_le_bytes(),
96 )
97 }
98
99 fn append(&mut self, kind: u16, bytes: &[u8]) -> PluginResult<()> {
100 if bytes.len() > u32::MAX as usize {
101 return Err(PluginError::limit_exceeded("hash component is too large"));
102 }
103 match &mut self.backend {
104 HashSinkBackend::Abi(raw) => {
105 let callback = raw
106 .append
107 .ok_or_else(|| PluginError::internal("missing hash sink callback"))?;
108 let status = unsafe {
110 callback(
111 raw.handle,
112 kind,
113 0,
114 RadixAbiSliceV1 {
115 ptr: bytes.as_ptr(),
116 len: bytes.len() as u32,
117 reserved: 0,
118 },
119 )
120 };
121 status_result(status, "hash sink rejected component")
122 }
123 HashSinkBackend::Test(components) => {
124 if components.len() >= radixdb_plugin_abi::RADIX_MAX_HASH_COMPONENTS as usize
125 || components
126 .iter()
127 .map(|(_, value)| value.len())
128 .sum::<usize>()
129 + bytes.len()
130 > radixdb_plugin_abi::RADIX_MAX_HASH_BYTES as usize
131 {
132 return Err(PluginError::limit_exceeded(
133 "semantic hash components exceed SDK test bounds",
134 ));
135 }
136 components.push((kind, bytes.to_vec()));
137 Ok(())
138 }
139 }
140 }
141}
142
143pub struct ResultBuilder<'a> {
144 raw: &'a RadixAbiResultBuilderV1,
145 items: u32,
146 bytes: u32,
147 finished: bool,
148}
149
150impl ResultBuilder<'_> {
151 pub fn push<T: ValueType>(&mut self, value: T) -> PluginResult<()> {
152 if value.is_null() {
153 return self.push_null();
154 }
155 let bytes = value.encode_abi()?;
156 self.write(0, &bytes)
157 }
158
159 pub fn push_null(&mut self) -> PluginResult<()> {
160 self.write(RADIX_RESULT_ITEM_FLAG_NULL, &[])
161 }
162
163 fn write(&mut self, flags: u32, bytes: &[u8]) -> PluginResult<()> {
164 if self.finished {
165 return Err(PluginError::internal("write after result finish"));
166 }
167 let next_items = self
168 .items
169 .checked_add(1)
170 .ok_or_else(|| PluginError::limit_exceeded("result item count overflow"))?;
171 let next_bytes = self
172 .bytes
173 .checked_add(bytes.len() as u32)
174 .ok_or_else(|| PluginError::limit_exceeded("result byte count overflow"))?;
175 if next_items > self.raw.max_items || next_bytes > self.raw.max_bytes {
176 return Err(PluginError::limit_exceeded(
177 "result exceeds host-owned builder bounds",
178 ));
179 }
180 let callback = self
181 .raw
182 .write
183 .ok_or_else(|| PluginError::internal("missing result writer"))?;
184 let status = unsafe {
186 callback(
187 self.raw.handle,
188 flags,
189 0,
190 RadixAbiSliceV1 {
191 ptr: bytes.as_ptr(),
192 len: bytes.len() as u32,
193 reserved: 0,
194 },
195 )
196 };
197 status_result(status, "result builder rejected output")?;
198 self.items = next_items;
199 self.bytes = next_bytes;
200 Ok(())
201 }
202
203 fn finish(&mut self) -> PluginResult<()> {
204 if self.finished {
205 return Err(PluginError::internal("result builder finished twice"));
206 }
207 let callback = self
208 .raw
209 .finish
210 .ok_or_else(|| PluginError::internal("missing result finisher"))?;
211 let status = unsafe { callback(self.raw.handle) };
213 status_result(status, "result builder finish failed")?;
214 self.finished = true;
215 Ok(())
216 }
217}
218
219pub struct ColumnBuilder<'borrow, 'host, T> {
220 output: &'borrow mut ResultBuilder<'host>,
221 marker: PhantomData<T>,
222}
223
224impl<T: ValueType> ColumnBuilder<'_, '_, T> {
225 pub fn push(&mut self, value: T) -> PluginResult<()> {
226 self.output.push(value)
227 }
228
229 pub fn push_null(&mut self) -> PluginResult<()> {
230 self.output.push_null()
231 }
232}
233
234#[derive(Clone, Copy)]
235pub struct ColumnView<'a, T> {
236 raw: &'a RadixAbiColumnViewV1,
237 row: u32,
238 marker: PhantomData<T>,
239}
240
241impl<'a, T: ValueType> Iterator for ColumnView<'a, T> {
242 type Item = PluginResult<T>;
243
244 fn next(&mut self) -> Option<Self::Item> {
245 if self.row >= self.raw.row_count {
246 return None;
247 }
248 let row = self.row;
249 self.row += 1;
250 Some(decode_column_value::<T>(self.raw, row))
251 }
252}
253
254#[derive(Debug, Clone, Copy)]
255pub struct PredicateView<'a> {
256 bytes: &'a [u8],
257}
258
259impl<'a> PredicateView<'a> {
260 pub fn as_bytes(&self) -> &'a [u8] {
261 self.bytes
262 }
263
264 pub fn target_function_id(&self) -> PluginResult<[u8; 16]> {
265 self.validate_header()?;
266 Ok(self.bytes[4..20].try_into().expect("validated fixed width"))
267 }
268
269 pub fn operator_class_id(&self) -> PluginResult<[u8; 16]> {
270 self.validate_header()?;
271 Ok(self.bytes[20..36]
272 .try_into()
273 .expect("validated fixed width"))
274 }
275
276 pub fn indexed_argument(&self) -> PluginResult<u16> {
277 self.validate_header()?;
278 Ok(u16::from_le_bytes(
279 self.bytes[36..38]
280 .try_into()
281 .expect("validated fixed width"),
282 ))
283 }
284
285 pub fn argument_count(&self) -> PluginResult<u16> {
286 self.validate_header()?;
287 Ok(u16::from_le_bytes(
288 self.bytes[38..40]
289 .try_into()
290 .expect("validated fixed width"),
291 ))
292 }
293
294 pub fn constant<T: ValueType>(&self, index: u16) -> PluginResult<Option<T>> {
297 let argument = self.argument(index)?;
298 if argument.kind != 2 {
299 return Err(PluginError::invalid_input(
300 "requested normalized argument is the indexed column",
301 ));
302 }
303 if argument.is_null {
304 return Ok(None);
305 }
306 let mut inline_bytes = [0; 16];
307 let variable = argument.type_ref.kind == RADIX_TYPE_REF_EXTERNAL
308 || matches!(
309 argument.type_ref.builtin_tag,
310 radixdb_plugin_abi::RADIX_BUILTIN_TEXT
311 | radixdb_plugin_abi::RADIX_BUILTIN_JSON
312 | radixdb_plugin_abi::RADIX_BUILTIN_VECTOR
313 | radixdb_plugin_abi::RADIX_BUILTIN_DECIMAL
314 | radixdb_plugin_abi::RADIX_BUILTIN_BYTES
315 );
316 let borrowed_bytes = if variable {
317 RadixAbiSliceV1 {
318 ptr: argument.bytes.as_ptr(),
319 len: argument.bytes.len() as u32,
320 reserved: 0,
321 }
322 } else {
323 if argument.bytes.len() > inline_bytes.len() {
324 return Err(PluginError::invalid_input(
325 "normalized fixed value exceeds ABI width",
326 ));
327 }
328 inline_bytes[..argument.bytes.len()].copy_from_slice(argument.bytes);
329 RadixAbiSliceV1::EMPTY
330 };
331 let raw = RadixAbiValueV1 {
332 type_ref: argument.type_ref,
333 flags: 0,
334 reserved: 0,
335 inline_bytes,
336 borrowed_bytes,
337 };
338 let value = unsafe { AbiValue::new(&raw)? };
340 T::decode_abi(&value).map(Some)
341 }
342
343 fn validate_header(&self) -> PluginResult<()> {
344 if self.bytes.len() < 40 || self.bytes[..4] != *b"RPN1" {
345 return Err(PluginError::invalid_input(
346 "invalid normalized predicate header",
347 ));
348 }
349 Ok(())
350 }
351
352 fn argument(&self, requested: u16) -> PluginResult<NormalizedArgument<'a>> {
353 self.validate_header()?;
354 let count = self.argument_count()?;
355 if requested >= count {
356 return Err(PluginError::invalid_input(
357 "normalized predicate argument is out of bounds",
358 ));
359 }
360 let mut offset = 40usize;
361 for index in 0..count {
362 let header_end = offset
363 .checked_add(32)
364 .ok_or_else(|| PluginError::invalid_input("predicate length overflow"))?;
365 if header_end > self.bytes.len() {
366 return Err(PluginError::invalid_input(
367 "truncated normalized predicate argument",
368 ));
369 }
370 let kind = self.bytes[offset];
371 let flags = self.bytes[offset + 1];
372 if !matches!(kind, 1 | 2)
373 || flags & !1 != 0
374 || self.bytes[offset + 2..offset + 4] != [0, 0]
375 {
376 return Err(PluginError::invalid_input(
377 "invalid normalized predicate argument header",
378 ));
379 }
380 let type_ref = radixdb_plugin_abi::RadixAbiTypeRefV1 {
381 kind: u16::from_le_bytes(
382 self.bytes[offset + 4..offset + 6]
383 .try_into()
384 .expect("fixed width"),
385 ),
386 builtin_tag: u16::from_le_bytes(
387 self.bytes[offset + 6..offset + 8]
388 .try_into()
389 .expect("fixed width"),
390 ),
391 object_id: self.bytes[offset + 8..offset + 24]
392 .try_into()
393 .expect("fixed width"),
394 codec_version: u32::from_le_bytes(
395 self.bytes[offset + 24..offset + 28]
396 .try_into()
397 .expect("fixed width"),
398 ),
399 };
400 abi_contract(radixdb_plugin_abi::validate_type_ref(&type_ref))?;
401 let len = u32::from_le_bytes(
402 self.bytes[offset + 28..header_end]
403 .try_into()
404 .expect("fixed width"),
405 ) as usize;
406 let end = header_end
407 .checked_add(len)
408 .ok_or_else(|| PluginError::invalid_input("predicate length overflow"))?;
409 if end > self.bytes.len()
410 || (kind == 1 && (len != 0 || flags != 0))
411 || (flags & 1 != 0 && len != 0)
412 {
413 return Err(PluginError::invalid_input(
414 "invalid normalized predicate argument payload",
415 ));
416 }
417 if index == requested {
418 return Ok(NormalizedArgument {
419 kind,
420 is_null: flags & 1 != 0,
421 type_ref,
422 bytes: &self.bytes[header_end..end],
423 });
424 }
425 offset = end;
426 }
427 Err(PluginError::invalid_input(
428 "normalized predicate argument is missing",
429 ))
430 }
431}
432
433struct NormalizedArgument<'a> {
434 kind: u8,
435 is_null: bool,
436 type_ref: radixdb_plugin_abi::RadixAbiTypeRefV1,
437 bytes: &'a [u8],
438}
439
440#[derive(Debug, Clone, PartialEq, Eq)]
441pub struct CandidateSpan {
442 pub start: Vec<u8>,
443 pub end: Vec<u8>,
444}
445
446pub struct CandidatePlanBuilder<'borrow, 'host> {
447 output: &'borrow mut ResultBuilder<'host>,
448 context: &'borrow CallContext<'host>,
449}
450
451impl CandidatePlanBuilder<'_, '_> {
452 pub fn set_estimate(&mut self, estimated_rows: u64, cost_hint: u32) -> PluginResult<()> {
453 self.context.check_cancelled()?;
454 self.context.charge_work(1)?;
455 let mut bytes = Vec::with_capacity(20);
456 bytes.extend_from_slice(&[2, 0, 0, 0]);
457 bytes.extend_from_slice(&estimated_rows.to_le_bytes());
458 bytes.extend_from_slice(&cost_hint.to_le_bytes());
459 bytes.extend_from_slice(&0_u32.to_le_bytes());
460 self.output.write(0, &bytes)
461 }
462
463 pub fn push_span(&mut self, span: CandidateSpan) -> PluginResult<()> {
464 self.context.check_cancelled()?;
465 self.context.charge_work(1)?;
466 let start_len = u32::try_from(span.start.len())
467 .map_err(|_| PluginError::limit_exceeded("candidate start key is too large"))?;
468 let end_len = u32::try_from(span.end.len())
469 .map_err(|_| PluginError::limit_exceeded("candidate end key is too large"))?;
470 let mut bytes = Vec::with_capacity(12 + span.start.len() + span.end.len());
471 bytes.extend_from_slice(&[1, 0, 0, 0]);
472 bytes.extend_from_slice(&start_len.to_le_bytes());
473 bytes.extend_from_slice(&end_len.to_le_bytes());
474 bytes.extend_from_slice(&span.start);
475 bytes.extend_from_slice(&span.end);
476 self.output.write(0, &bytes)
477 }
478}
479
480fn decode_column_value<T: ValueType>(column: &RadixAbiColumnViewV1, row: u32) -> PluginResult<T> {
481 let is_null = if column.null_bitmap.len == 0 {
482 false
483 } else {
484 let bitmap = unsafe {
486 std::slice::from_raw_parts(column.null_bitmap.ptr, column.null_bitmap.len as usize)
487 };
488 bitmap[row as usize / 8] & (1 << (row % 8)) != 0
489 };
490 let (inline_bytes, borrowed_bytes) = if column.layout == RADIX_COLUMN_LAYOUT_FIXED {
491 let start = row as usize * column.stride as usize;
492 let width = column.element_width as usize;
493 let data = unsafe { std::slice::from_raw_parts(column.data.ptr, column.data.len as usize) };
495 if column.type_ref.kind == RADIX_TYPE_REF_EXTERNAL {
496 ([0; 16], &data[start..start + width])
497 } else {
498 let mut inline = [0; 16];
499 if width > inline.len() {
500 return Err(PluginError::invalid_input(
501 "fixed built-in column element exceeds inline ABI width",
502 ));
503 }
504 inline[..width].copy_from_slice(&data[start..start + width]);
505 (inline, &[][..])
506 }
507 } else {
508 let offsets =
510 unsafe { std::slice::from_raw_parts(column.offsets.ptr, column.offsets.len as usize) };
511 let data = unsafe { std::slice::from_raw_parts(column.data.ptr, column.data.len as usize) };
513 let start = offsets[row as usize] as usize;
514 let end = offsets[row as usize + 1] as usize;
515 ([0; 16], &data[start..end])
516 };
517 let raw = RadixAbiValueV1 {
518 type_ref: column.type_ref,
519 flags: if is_null { 1 } else { 0 },
520 reserved: 0,
521 inline_bytes,
522 borrowed_bytes: RadixAbiSliceV1 {
523 ptr: borrowed_bytes.as_ptr(),
524 len: borrowed_bytes.len() as u32,
525 reserved: 0,
526 },
527 };
528 let value = unsafe { AbiValue::new(&raw)? };
530 T::decode_abi(&value)
531}
532
533pub(crate) fn status_result(status: u32, detail: &'static str) -> PluginResult<()> {
534 match status {
535 RADIX_STATUS_OK => Ok(()),
536 RADIX_STATUS_INVALID_ARGUMENT | RADIX_STATUS_CONTRACT_VIOLATION => {
537 Err(PluginError::invalid_input(detail))
538 }
539 RADIX_STATUS_DOMAIN_ERROR => Err(PluginError::domain(detail)),
540 RADIX_STATUS_LIMIT_EXCEEDED => Err(PluginError::limit_exceeded(detail)),
541 RADIX_STATUS_CANCELLED => Err(PluginError::cancelled()),
542 _ => Err(PluginError::internal(detail)),
543 }
544}
545
546fn abi_contract<T>(
547 result: Result<T, radixdb_plugin_abi::RadixAbiValidationError>,
548) -> PluginResult<T> {
549 result.map_err(|error| PluginError::invalid_input(format!("ABI contract violation: {error:?}")))
550}
551
552fn error_status(error: &PluginError) -> RadixAbiStatusV1 {
553 match error.kind() {
554 PluginErrorKind::InvalidInput => RADIX_STATUS_INVALID_ARGUMENT,
555 PluginErrorKind::Domain => RADIX_STATUS_DOMAIN_ERROR,
556 PluginErrorKind::LimitExceeded => RADIX_STATUS_LIMIT_EXCEEDED,
557 PluginErrorKind::Cancelled => RADIX_STATUS_CANCELLED,
558 PluginErrorKind::Internal => RADIX_STATUS_INTERNAL_ERROR,
559 }
560}
561
562fn report_error(context: Option<&RadixAbiCallContextV1>, error: &PluginError, panic: bool) {
563 let Some(context) = context else {
564 return;
565 };
566 let Some(sink) = (unsafe { context.diagnostics.as_ref() }) else {
568 return;
569 };
570 let Some(write) = sink.write else {
571 return;
572 };
573 let detail = error.detail().as_bytes();
574 let field = error.field().unwrap_or_default().as_bytes();
575 let category = if panic {
576 RADIX_DIAGNOSTIC_PLUGIN_PANIC
577 } else {
578 match error.kind() {
579 PluginErrorKind::InvalidInput => RADIX_DIAGNOSTIC_INVALID_INPUT,
580 PluginErrorKind::Domain => RADIX_DIAGNOSTIC_DOMAIN,
581 PluginErrorKind::LimitExceeded => RADIX_DIAGNOSTIC_LIMIT,
582 PluginErrorKind::Cancelled => RADIX_DIAGNOSTIC_CANCELLED,
583 PluginErrorKind::Internal => RADIX_DIAGNOSTIC_INTERNAL,
584 }
585 };
586 let diagnostic = RadixAbiDiagnosticV1 {
587 header: RadixAbiHeaderV1::new::<RadixAbiDiagnosticV1>(0),
588 category,
589 status: if panic {
590 RADIX_STATUS_PANIC
591 } else {
592 error_status(error)
593 },
594 detail: RadixAbiSliceV1 {
595 ptr: detail.as_ptr(),
596 len: detail.len() as u32,
597 reserved: 0,
598 },
599 field: RadixAbiSliceV1 {
600 ptr: field.as_ptr(),
601 len: field.len() as u32,
602 reserved: 0,
603 },
604 };
605 let _ = unsafe { write(sink.handle, &diagnostic) };
607}
608
609#[doc(hidden)]
610pub unsafe fn run_scalar<F>(
611 context: *const RadixAbiCallContextV1,
612 arguments: *const RadixAbiValueV1,
613 argument_count: u32,
614 output: *const RadixAbiResultBuilderV1,
615 expected_arguments: u32,
616 strict: bool,
617 callback: F,
618) -> RadixAbiStatusV1
619where
620 F: FnOnce(&CallContext<'_>, &[AbiValue<'_>], &mut ResultBuilder<'_>) -> PluginResult<()>,
621{
622 let context_ref = unsafe { context.as_ref() };
623 let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| -> PluginResult<()> {
624 let context = CallContext {
625 raw: context_ref.ok_or_else(|| PluginError::invalid_input("null call context"))?,
626 };
627 abi_contract(radixdb_plugin_abi::validate_call_context(context.raw))?;
628 let raw_output = unsafe { output.as_ref() }
629 .ok_or_else(|| PluginError::invalid_input("null result builder"))?;
630 abi_contract(radixdb_plugin_abi::validate_result_builder(raw_output))?;
631 if raw_output.max_bytes > context.max_output_bytes() {
632 return Err(PluginError::invalid_input(
633 "result builder exceeds call output budget",
634 ));
635 }
636 if argument_count != expected_arguments || (argument_count != 0 && arguments.is_null()) {
637 return Err(PluginError::invalid_input("scalar argument count mismatch"));
638 }
639 let raw_arguments = if argument_count == 0 {
640 &[]
641 } else {
642 unsafe { std::slice::from_raw_parts(arguments, argument_count as usize) }
644 };
645 let values = raw_arguments
646 .iter()
647 .map(|raw| {
648 abi_contract(unsafe { radixdb_plugin_abi::validate_value_contents(raw) })?;
649 unsafe { AbiValue::new(raw) }
650 })
651 .collect::<PluginResult<Vec<_>>>()?;
652 let mut builder = ResultBuilder {
653 raw: raw_output,
654 items: 0,
655 bytes: 0,
656 finished: false,
657 };
658 if strict && values.iter().any(AbiValue::is_null) {
659 builder.push_null()?;
660 } else {
661 callback(&context, &values, &mut builder)?;
662 }
663 if builder.items != 1 {
664 return Err(PluginError::internal(
665 "scalar callback must emit exactly one item",
666 ));
667 }
668 builder.finish()
669 }));
670 finish_outcome(context_ref, outcome)
671}
672
673#[doc(hidden)]
674pub unsafe fn run_batch<F>(
675 context: *const RadixAbiCallContextV1,
676 input: *const RadixAbiBatchViewV1,
677 output: *const RadixAbiResultBuilderV1,
678 expected_columns: u32,
679 callback: F,
680) -> RadixAbiStatusV1
681where
682 F: FnOnce(&CallContext<'_>, &BatchInput<'_>, &mut ResultBuilder<'_>) -> PluginResult<()>,
683{
684 let context_ref = unsafe { context.as_ref() };
685 let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| -> PluginResult<()> {
686 let context = CallContext {
687 raw: context_ref.ok_or_else(|| PluginError::invalid_input("null call context"))?,
688 };
689 abi_contract(radixdb_plugin_abi::validate_call_context(context.raw))?;
690 let batch = unsafe { input.as_ref() }
691 .ok_or_else(|| PluginError::invalid_input("null batch input"))?;
692 abi_contract(unsafe { radixdb_plugin_abi::validate_batch_columns(batch) })?;
693 if batch.column_count != expected_columns
694 || (batch.column_count != 0 && batch.columns.is_null())
695 {
696 return Err(PluginError::invalid_input("batch column count mismatch"));
697 }
698 let columns = if batch.column_count == 0 {
699 &[]
700 } else {
701 unsafe { std::slice::from_raw_parts(batch.columns, batch.column_count as usize) }
703 };
704 if columns
705 .iter()
706 .any(|column| column.row_count != batch.row_count)
707 {
708 return Err(PluginError::invalid_input("batch row count mismatch"));
709 }
710 let raw_output = unsafe { output.as_ref() }
711 .ok_or_else(|| PluginError::invalid_input("null result builder"))?;
712 abi_contract(radixdb_plugin_abi::validate_result_builder(raw_output))?;
713 if raw_output.max_bytes > context.max_output_bytes() {
714 return Err(PluginError::invalid_input(
715 "result builder exceeds call output budget",
716 ));
717 }
718 let input = BatchInput {
719 rows: batch.row_count,
720 columns,
721 };
722 let mut builder = ResultBuilder {
723 raw: raw_output,
724 items: 0,
725 bytes: 0,
726 finished: false,
727 };
728 callback(&context, &input, &mut builder)?;
729 if builder.items != batch.row_count {
730 return Err(PluginError::internal(
731 "batch callback output count differs from input rows",
732 ));
733 }
734 builder.finish()
735 }));
736 finish_outcome(context_ref, outcome)
737}
738
739pub struct BatchInput<'a> {
740 rows: u32,
741 columns: &'a [RadixAbiColumnViewV1],
742}
743
744impl<'a> BatchInput<'a> {
745 pub fn row_count(&self) -> u32 {
746 self.rows
747 }
748
749 pub fn column<T: ValueType>(&self, index: usize) -> PluginResult<ColumnView<'a, T>> {
750 let raw = self
751 .columns
752 .get(index)
753 .ok_or_else(|| PluginError::invalid_input("batch column is missing"))?;
754 Ok(ColumnView {
755 raw,
756 row: 0,
757 marker: PhantomData,
758 })
759 }
760}
761
762#[doc(hidden)]
763pub unsafe fn run_codec_encode<T: RadixType>(
764 context: *const RadixAbiCallContextV1,
765 input: *const RadixAbiValueV1,
766 output: *const RadixAbiResultBuilderV1,
767) -> RadixAbiStatusV1 {
768 unsafe {
769 run_scalar(
770 context,
771 input,
772 1,
773 output,
774 1,
775 true,
776 |_context, arguments, output| {
777 let value = T::decode_abi(&arguments[0])?;
778 output.push(value)
779 },
780 )
781 }
782}
783
784#[doc(hidden)]
785pub unsafe fn run_codec_decode<T: RadixType>(
786 context: *const RadixAbiCallContextV1,
787 input: RadixAbiSliceV1,
788 output: *const RadixAbiResultBuilderV1,
789) -> RadixAbiStatusV1 {
790 let context_ref = unsafe { context.as_ref() };
791 let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| -> PluginResult<()> {
792 let context = CallContext {
793 raw: context_ref.ok_or_else(|| PluginError::invalid_input("null call context"))?,
794 };
795 abi_contract(radixdb_plugin_abi::validate_call_context(context.raw))?;
796 if input.reserved != 0 || (input.len != 0 && input.ptr.is_null()) {
797 return Err(PluginError::invalid_input("invalid codec input slice"));
798 }
799 let bytes = if input.len == 0 {
800 &[]
801 } else {
802 unsafe { std::slice::from_raw_parts(input.ptr, input.len as usize) }
804 };
805 let mut reader = crate::CodecReader::new(bytes);
806 let value = T::decode(&mut reader)?;
807 reader.finish()?;
808 let raw_output = unsafe { output.as_ref() }
809 .ok_or_else(|| PluginError::invalid_input("null result builder"))?;
810 abi_contract(radixdb_plugin_abi::validate_result_builder(raw_output))?;
811 if raw_output.max_bytes > context.max_output_bytes() {
812 return Err(PluginError::invalid_input(
813 "result builder exceeds call output budget",
814 ));
815 }
816 let mut builder = ResultBuilder {
817 raw: raw_output,
818 items: 0,
819 bytes: 0,
820 finished: false,
821 };
822 builder.push(value)?;
823 builder.finish()
824 }));
825 finish_outcome(context_ref, outcome)
826}
827
828#[doc(hidden)]
829pub unsafe fn run_equal<T: RadixType>(
830 context: *const RadixAbiCallContextV1,
831 left: *const RadixAbiValueV1,
832 right: *const RadixAbiValueV1,
833 output: *mut u8,
834) -> RadixAbiStatusV1 {
835 let callback = |left: T, right: T| {
836 let result = T::semantic_equal(&left, &right)
837 .ok_or_else(|| PluginError::internal("equality capability is not implemented"))?;
838 if output.is_null() {
839 return Err(PluginError::invalid_input("null equality output"));
840 }
841 unsafe { output.write(u8::from(result)) };
843 Ok(())
844 };
845 unsafe { run_binary_value(context, left, right, callback) }
846}
847
848#[doc(hidden)]
849pub unsafe fn run_compare<T: RadixType>(
850 context: *const RadixAbiCallContextV1,
851 left: *const RadixAbiValueV1,
852 right: *const RadixAbiValueV1,
853 output: *mut i8,
854) -> RadixAbiStatusV1 {
855 let callback = |left: T, right: T| {
856 let result = T::semantic_compare(&left, &right)
857 .ok_or_else(|| PluginError::internal("ordering capability is not implemented"))?;
858 if output.is_null() {
859 return Err(PluginError::invalid_input("null ordering output"));
860 }
861 let value = match result {
862 std::cmp::Ordering::Less => -1,
863 std::cmp::Ordering::Equal => 0,
864 std::cmp::Ordering::Greater => 1,
865 };
866 unsafe { output.write(value) };
868 Ok(())
869 };
870 unsafe { run_binary_value(context, left, right, callback) }
871}
872
873unsafe fn run_binary_value<T: RadixType, F>(
874 context: *const RadixAbiCallContextV1,
875 left: *const RadixAbiValueV1,
876 right: *const RadixAbiValueV1,
877 callback: F,
878) -> RadixAbiStatusV1
879where
880 F: FnOnce(T, T) -> PluginResult<()>,
881{
882 let context_ref = unsafe { context.as_ref() };
883 let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| -> PluginResult<()> {
884 let context = CallContext {
885 raw: context_ref.ok_or_else(|| PluginError::invalid_input("null call context"))?,
886 };
887 abi_contract(radixdb_plugin_abi::validate_call_context(context.raw))?;
888 let left = unsafe { left.as_ref() }
889 .ok_or_else(|| PluginError::invalid_input("null left value"))?;
890 let right = unsafe { right.as_ref() }
891 .ok_or_else(|| PluginError::invalid_input("null right value"))?;
892 abi_contract(unsafe { radixdb_plugin_abi::validate_value_contents(left) })?;
893 abi_contract(unsafe { radixdb_plugin_abi::validate_value_contents(right) })?;
894 let left = T::decode_abi(&unsafe { AbiValue::new(left)? })?;
895 let right = T::decode_abi(&unsafe { AbiValue::new(right)? })?;
896 callback(left, right)
897 }));
898 finish_outcome(context_ref, outcome)
899}
900
901#[doc(hidden)]
902pub unsafe fn run_hash<T: RadixType>(
903 context: *const RadixAbiCallContextV1,
904 value: *const RadixAbiValueV1,
905 sink: *const RadixAbiHashSinkV1,
906) -> RadixAbiStatusV1 {
907 let context_ref = unsafe { context.as_ref() };
908 let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| -> PluginResult<()> {
909 let context = context_ref.ok_or_else(|| PluginError::invalid_input("null call context"))?;
910 abi_contract(radixdb_plugin_abi::validate_call_context(context))?;
911 let value = unsafe { value.as_ref() }
912 .ok_or_else(|| PluginError::invalid_input("null hash value"))?;
913 abi_contract(unsafe { radixdb_plugin_abi::validate_value_contents(value) })?;
914 let value = T::decode_abi(&unsafe { AbiValue::new(value)? })?;
915 let raw_sink =
916 unsafe { sink.as_ref() }.ok_or_else(|| PluginError::invalid_input("null hash sink"))?;
917 abi_contract(radixdb_plugin_abi::validate_hash_sink(raw_sink))?;
918 let mut sink = HashSink {
919 backend: HashSinkBackend::Abi(raw_sink),
920 };
921 T::semantic_hash(&value, &mut sink)
922 .ok_or_else(|| PluginError::internal("hash capability is not implemented"))?
923 }));
924 finish_outcome(context_ref, outcome)
925}
926
927#[doc(hidden)]
928pub unsafe fn run_key_encoder<T: ValueType, K: ValueType>(
929 context: *const RadixAbiCallContextV1,
930 value: *const RadixAbiValueV1,
931 output: *const RadixAbiResultBuilderV1,
932 callback: fn(T) -> PluginResult<K>,
933) -> RadixAbiStatusV1 {
934 unsafe {
935 run_scalar(
936 context,
937 value,
938 1,
939 output,
940 1,
941 true,
942 |_context, arguments, output| output.push(callback(T::decode_abi(&arguments[0])?)?),
943 )
944 }
945}
946
947#[doc(hidden)]
948pub unsafe fn run_planner<F>(
949 context: *const RadixAbiCallContextV1,
950 predicate: RadixAbiSliceV1,
951 output: *const RadixAbiResultBuilderV1,
952 callback: F,
953) -> RadixAbiStatusV1
954where
955 F: FnOnce(PredicateView<'_>, &mut CandidatePlanBuilder<'_, '_>) -> PluginResult<()>,
956{
957 let context_ref = unsafe { context.as_ref() };
958 let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| -> PluginResult<()> {
959 let context = CallContext {
960 raw: context_ref.ok_or_else(|| PluginError::invalid_input("null call context"))?,
961 };
962 abi_contract(radixdb_plugin_abi::validate_call_context(context.raw))?;
963 if predicate.reserved != 0 || (predicate.len != 0 && predicate.ptr.is_null()) {
964 return Err(PluginError::invalid_input("invalid predicate input"));
965 }
966 let bytes = if predicate.len == 0 {
967 &[]
968 } else {
969 unsafe { std::slice::from_raw_parts(predicate.ptr, predicate.len as usize) }
971 };
972 let raw_output = unsafe { output.as_ref() }
973 .ok_or_else(|| PluginError::invalid_input("null planner output"))?;
974 abi_contract(radixdb_plugin_abi::validate_result_builder(raw_output))?;
975 if raw_output.max_bytes > context.max_output_bytes() {
976 return Err(PluginError::invalid_input(
977 "result builder exceeds call output budget",
978 ));
979 }
980 let mut result = ResultBuilder {
981 raw: raw_output,
982 items: 0,
983 bytes: 0,
984 finished: false,
985 };
986 {
987 let mut builder = CandidatePlanBuilder {
988 output: &mut result,
989 context: &context,
990 };
991 callback(PredicateView { bytes }, &mut builder)?;
992 }
993 result.finish()
994 }));
995 finish_outcome(context_ref, outcome)
996}
997
998fn finish_outcome(
999 context: Option<&RadixAbiCallContextV1>,
1000 outcome: Result<PluginResult<()>, Box<dyn std::any::Any + Send>>,
1001) -> RadixAbiStatusV1 {
1002 match outcome {
1003 Ok(Ok(())) => RADIX_STATUS_OK,
1004 Ok(Err(error)) => {
1005 let status = error_status(&error);
1006 report_error(context, &error, false);
1007 status
1008 }
1009 Err(_) => {
1010 let error = PluginError::internal("plugin callback panicked");
1011 report_error(context, &error, true);
1012 RADIX_STATUS_PANIC
1013 }
1014 }
1015}
1016
1017#[doc(hidden)]
1018pub fn column_builder<'borrow, 'host, T>(
1019 output: &'borrow mut ResultBuilder<'host>,
1020) -> ColumnBuilder<'borrow, 'host, T> {
1021 ColumnBuilder {
1022 output,
1023 marker: PhantomData,
1024 }
1025}
1026
1027#[cfg(test)]
1028mod tests {
1029 use super::*;
1030
1031 fn push_argument(
1032 frame: &mut Vec<u8>,
1033 kind: u8,
1034 flags: u8,
1035 type_ref: radixdb_plugin_abi::RadixAbiTypeRefV1,
1036 value: &[u8],
1037 ) {
1038 frame.extend_from_slice(&[kind, flags, 0, 0]);
1039 frame.extend_from_slice(&type_ref.kind.to_le_bytes());
1040 frame.extend_from_slice(&type_ref.builtin_tag.to_le_bytes());
1041 frame.extend_from_slice(&type_ref.object_id);
1042 frame.extend_from_slice(&type_ref.codec_version.to_le_bytes());
1043 frame.extend_from_slice(&(value.len() as u32).to_le_bytes());
1044 frame.extend_from_slice(value);
1045 }
1046
1047 #[test]
1048 fn normalized_predicate_view_decodes_typed_constants() {
1049 let integer = radixdb_plugin_abi::RadixAbiTypeRefV1::builtin(
1050 radixdb_plugin_abi::RADIX_BUILTIN_INTEGER,
1051 );
1052 let mut frame = Vec::new();
1053 frame.extend_from_slice(b"RPN1");
1054 frame.extend_from_slice(&[1; 16]);
1055 frame.extend_from_slice(&[2; 16]);
1056 frame.extend_from_slice(&0_u16.to_le_bytes());
1057 frame.extend_from_slice(&3_u16.to_le_bytes());
1058 push_argument(&mut frame, 1, 0, integer, &[]);
1059 push_argument(&mut frame, 2, 0, integer, &42_i64.to_le_bytes());
1060 push_argument(&mut frame, 2, 1, integer, &[]);
1061
1062 let predicate = PredicateView { bytes: &frame };
1063 assert_eq!(predicate.target_function_id().unwrap(), [1; 16]);
1064 assert_eq!(predicate.operator_class_id().unwrap(), [2; 16]);
1065 assert_eq!(predicate.indexed_argument().unwrap(), 0);
1066 assert_eq!(predicate.argument_count().unwrap(), 3);
1067 assert!(predicate.constant::<i64>(0).is_err());
1068 assert_eq!(predicate.constant::<i64>(1).unwrap(), Some(42));
1069 assert_eq!(predicate.constant::<i64>(2).unwrap(), None);
1070 assert!(predicate.constant::<i64>(3).is_err());
1071 }
1072
1073 #[test]
1074 fn normalized_predicate_view_rejects_malformed_frames() {
1075 let predicate = PredicateView { bytes: b"bad" };
1076 assert!(predicate.argument_count().is_err());
1077
1078 let mut truncated = Vec::new();
1079 truncated.extend_from_slice(b"RPN1");
1080 truncated.extend_from_slice(&[1; 32]);
1081 truncated.extend_from_slice(&0_u16.to_le_bytes());
1082 truncated.extend_from_slice(&1_u16.to_le_bytes());
1083 let predicate = PredicateView { bytes: &truncated };
1084 assert!(predicate.constant::<i64>(0).is_err());
1085 }
1086}