1use std::collections::HashMap;
9use std::panic::{catch_unwind, AssertUnwindSafe};
10
11use crate::abi::{
12 FolditPluginAsset, FolditPluginBuffer, FolditPluginDispatchContext, FolditPluginError,
13 FolditPluginParamEntry, FolditPluginParamTag, FolditPluginResidueRef, FolditPluginStatus,
14};
15use crate::error::{PluginError, Result};
16use crate::proto::plugin as proto;
17use crate::protocol::{DispatchContext, ParamValue, PollOutcome, ResidueRef};
18
19pub type BoxedPlugin = Box<dyn crate::Plugin>;
23
24#[must_use]
32pub fn buffer_from_vec(mut v: Vec<u8>) -> FolditPluginBuffer {
33 if v.is_empty() {
34 return FolditPluginBuffer::empty();
35 }
36 let (data, len, capacity) = (v.as_mut_ptr(), v.len(), v.capacity());
37 std::mem::forget(v);
38 FolditPluginBuffer {
39 data,
40 len,
41 capacity,
42 }
43}
44
45pub unsafe fn drop_buffer(buf: *mut FolditPluginBuffer) {
50 if buf.is_null() {
51 return;
52 }
53 let b = unsafe { &mut *buf };
54 if !b.data.is_null() {
55 drop(unsafe { Vec::from_raw_parts(b.data, b.len, b.capacity) });
56 }
57 *b = FolditPluginBuffer::empty();
58}
59
60pub unsafe fn drop_error(err: *mut FolditPluginError) {
65 if err.is_null() {
66 return;
67 }
68 let e = unsafe { &mut *err };
69 unsafe {
70 drop_buffer(&raw mut e.code);
71 drop_buffer(&raw mut e.message);
72 }
73}
74
75pub unsafe fn report_error(out_err: *mut FolditPluginError, e: &PluginError) -> FolditPluginStatus {
82 match e {
83 PluginError::Unsupported => FolditPluginStatus::Unsupported,
84 PluginError::Op { code, message } => {
85 unsafe { write_error(out_err, code, message) };
86 FolditPluginStatus::Err
87 }
88 other => {
89 unsafe { write_error(out_err, "PLUGIN_ERROR", &other.to_string()) };
90 FolditPluginStatus::Err
91 }
92 }
93}
94
95pub unsafe fn write_error(out_err: *mut FolditPluginError, code: &str, message: &str) {
98 if out_err.is_null() {
99 return;
100 }
101 unsafe {
102 *out_err = FolditPluginError {
103 code: buffer_from_vec(code.as_bytes().to_vec()),
104 message: buffer_from_vec(message.as_bytes().to_vec()),
105 };
106 }
107}
108
109pub fn guard<T>(f: impl FnOnce() -> Result<T>) -> Result<T> {
115 catch_unwind(AssertUnwindSafe(f))
116 .unwrap_or_else(|_| Err(PluginError::Other("plugin panicked".into())))
117}
118
119#[must_use]
126pub unsafe fn slice_from_raw<'a>(p: *const u8, len: usize) -> &'a [u8] {
127 if p.is_null() || len == 0 {
128 &[]
129 } else {
130 unsafe { std::slice::from_raw_parts(p, len) }
131 }
132}
133
134unsafe fn str_from_raw<'a>(p: *const u8, len: usize) -> Option<&'a str> {
137 std::str::from_utf8(unsafe { slice_from_raw(p, len) }).ok()
138}
139
140unsafe fn residues_from_raw(p: *const FolditPluginResidueRef, n: usize) -> Vec<ResidueRef> {
143 if p.is_null() || n == 0 {
144 return Vec::new();
145 }
146 unsafe { std::slice::from_raw_parts(p, n) }
147 .iter()
148 .map(|r| ResidueRef {
149 entity_id: molex::EntityId::from_raw(u32::try_from(r.entity_id).unwrap_or_default()),
151 residue_index: r.residue_index,
152 })
153 .collect()
154}
155
156#[must_use]
159pub unsafe fn ctx_from_c(ctx: *const FolditPluginDispatchContext) -> DispatchContext {
160 if ctx.is_null() {
161 return DispatchContext::default();
162 }
163 let c = unsafe { &*ctx };
164 DispatchContext {
165 focused_entity_id: (c.has_focused_entity == 1).then(|| {
166 molex::EntityId::from_raw(u32::try_from(c.focused_entity_id).unwrap_or_default())
167 }),
168 selection: unsafe { residues_from_raw(c.selection, c.selection_len) },
169 designable: unsafe { residues_from_raw(c.designable, c.designable_len) },
170 }
171}
172
173#[must_use]
178pub unsafe fn params_from_c(
179 p: *const FolditPluginParamEntry,
180 n: usize,
181) -> HashMap<String, ParamValue> {
182 if p.is_null() || n == 0 {
183 return HashMap::new();
184 }
185 unsafe { std::slice::from_raw_parts(p, n) }
186 .iter()
187 .filter_map(|e| {
188 let key = unsafe { str_from_raw(e.key_data, e.key_len) }?.to_owned();
189 let v = &e.value;
190 let value = match v.tag {
191 FolditPluginParamTag::Int => ParamValue::Int(v.int_value),
192 FolditPluginParamTag::Float => ParamValue::Float(v.float_value),
193 FolditPluginParamTag::Bool => ParamValue::Bool(v.bool_value != 0),
194 FolditPluginParamTag::String => ParamValue::String(
195 unsafe { str_from_raw(v.string_data, v.string_len) }?.to_owned(),
196 ),
197 FolditPluginParamTag::Vec3 => {
198 ParamValue::Vec3([v.vec3_value.x, v.vec3_value.y, v.vec3_value.z])
199 }
200 FolditPluginParamTag::Unspecified => return None,
201 };
202 Some((key, value))
203 })
204 .collect()
205}
206
207#[must_use]
210pub unsafe fn assets_from_c(p: *const FolditPluginAsset, n: usize) -> Vec<proto::PuzzleAsset> {
211 if p.is_null() || n == 0 {
212 return Vec::new();
213 }
214 unsafe { std::slice::from_raw_parts(p, n) }
215 .iter()
216 .map(|a| proto::PuzzleAsset {
217 name: unsafe { str_from_raw(a.name_data, a.name_len) }
218 .unwrap_or_default()
219 .to_owned(),
220 data: unsafe { slice_from_raw(a.data, a.data_len) }.to_vec(),
221 })
222 .collect()
223}
224
225#[must_use]
231pub fn poll_outcome_to_proto(outcome: PollOutcome) -> proto::PollStreamResponse {
232 use proto::poll_stream_response::Result as R;
233 let result = match outcome {
234 PollOutcome::Pending {
235 latest_assembly,
236 progress,
237 stage,
238 score,
239 } => R::Pending(proto::StreamPending {
240 latest_assembly: latest_assembly.unwrap_or_default(),
241 progress,
242 stage,
243 score,
244 }),
245 PollOutcome::Checkpoint {
246 latest_assembly,
247 progress,
248 stage,
249 score,
250 } => R::Checkpoint(proto::StreamCheckpoint {
251 latest_assembly: latest_assembly.unwrap_or_default(),
252 progress,
253 stage,
254 score,
255 }),
256 PollOutcome::Cancelled { assembly, score } => {
257 R::Cancelled(proto::StreamCancelled { assembly, score })
258 }
259 PollOutcome::Final { assembly, score } => R::Final(proto::StreamFinal { assembly, score }),
260 PollOutcome::Error {
261 code,
262 message,
263 details,
264 } => R::Error(proto::Error {
265 code,
266 message,
267 details,
268 }),
269 };
270 proto::PollStreamResponse {
271 result: Some(result),
272 }
273}
274
275#[must_use]
277pub fn encode_to_buffer<M: prost::Message>(msg: &M) -> FolditPluginBuffer {
278 buffer_from_vec(msg.encode_to_vec())
279}
280
281#[macro_export]
291macro_rules! export_plugin {
292 ($ctor:expr) => {
293 const _: () = {
294 use ::std::os::raw::{c_char, c_void};
295 use $crate::abi::*;
296 use $crate::export::*;
297
298 unsafe fn plugin<'a>(handle: FolditPluginHandle) -> &'a dyn $crate::Plugin {
301 unsafe { &**handle.cast::<BoxedPlugin>() }
302 }
303
304 unsafe fn call(
306 out_err: *mut FolditPluginError,
307 f: impl FnOnce() -> $crate::Result<()>,
308 ) -> FolditPluginStatus {
309 match guard(f) {
310 Ok(()) => FolditPluginStatus::Ok,
311 Err(e) => unsafe { report_error(out_err, &e) },
312 }
313 }
314
315 unsafe extern "C" fn create(
316 config_json: *const c_char,
317 config_len: usize,
318 ) -> FolditPluginHandle {
319 let ctor: fn(&str) -> $crate::Result<BoxedPlugin> = $ctor;
320 let built = guard(|| {
321 let bytes = unsafe { slice_from_raw(config_json.cast::<u8>(), config_len) };
322 ctor(::std::str::from_utf8(bytes).unwrap_or("{}"))
323 });
324 match built {
325 Ok(p) => {
326 ::std::boxed::Box::into_raw(::std::boxed::Box::new(p)).cast::<c_void>()
327 }
328 Err(_) => ::std::ptr::null_mut(),
329 }
330 }
331
332 unsafe extern "C" fn destroy(handle: FolditPluginHandle) {
333 if handle.is_null() {
334 return;
335 }
336 drop(unsafe { ::std::boxed::Box::from_raw(handle.cast::<BoxedPlugin>()) });
337 }
338
339 unsafe extern "C" fn register(
340 handle: FolditPluginHandle,
341 out_buf: *mut FolditPluginBuffer,
342 out_err: *mut FolditPluginError,
343 ) -> FolditPluginStatus {
344 unsafe {
345 call(out_err, || {
346 let reg = plugin(handle).register()?;
347 *out_buf = encode_to_buffer(®);
348 Ok(())
349 })
350 }
351 }
352
353 #[allow(clippy::too_many_arguments)]
354 unsafe extern "C" fn init(
355 handle: FolditPluginHandle,
356 assembly: *const u8,
357 assembly_len: usize,
358 assets: *const FolditPluginAsset,
359 assets_len: usize,
360 params: *const FolditPluginParamEntry,
361 params_len: usize,
362 out_session: *mut u64,
363 out_initial_buf: *mut FolditPluginBuffer,
364 out_err: *mut FolditPluginError,
365 ) -> FolditPluginStatus {
366 unsafe {
367 call(out_err, || {
368 let (session, initial) = plugin(handle).init(
369 slice_from_raw(assembly, assembly_len),
370 &assets_from_c(assets, assets_len),
371 ¶ms_from_c(params, params_len),
372 )?;
373 *out_session = session;
374 *out_initial_buf = buffer_from_vec(initial);
375 Ok(())
376 })
377 }
378 }
379
380 unsafe extern "C" fn update_assembly(
381 handle: FolditPluginHandle,
382 session: u64,
383 payload_kind: FolditPluginAssemblyPayloadKind,
384 bytes: *const u8,
385 bytes_len: usize,
386 from_gen: u64,
387 to_gen: u64,
388 out_err: *mut FolditPluginError,
389 ) -> FolditPluginStatus {
390 unsafe {
391 call(out_err, || {
392 let b = slice_from_raw(bytes, bytes_len);
393 let payload = match payload_kind {
394 FolditPluginAssemblyPayloadKind::Full => {
395 $crate::AssemblyPayload::Full(b)
396 }
397 FolditPluginAssemblyPayloadKind::Delta => {
398 $crate::AssemblyPayload::Delta(b)
399 }
400 };
401 plugin(handle).update_assembly(session, payload, from_gen, to_gen)
402 })
403 }
404 }
405
406 unsafe extern "C" fn drop_session(
407 handle: FolditPluginHandle,
408 session: u64,
409 out_err: *mut FolditPluginError,
410 ) -> FolditPluginStatus {
411 unsafe { call(out_err, || plugin(handle).drop_session(session)) }
412 }
413
414 #[allow(clippy::too_many_arguments)]
415 unsafe extern "C" fn invoke(
416 handle: FolditPluginHandle,
417 session: u64,
418 op_id: *const u8,
419 op_id_len: usize,
420 ctx: *const FolditPluginDispatchContext,
421 params: *const FolditPluginParamEntry,
422 params_len: usize,
423 out_assembly: *mut FolditPluginBuffer,
424 out_err: *mut FolditPluginError,
425 ) -> FolditPluginStatus {
426 unsafe {
427 call(out_err, || {
428 let op = ::std::str::from_utf8(slice_from_raw(op_id, op_id_len))
429 .map_err(|e| $crate::PluginError::Other(e.to_string()))?;
430 let bytes = plugin(handle).invoke(
431 session,
432 op,
433 &ctx_from_c(ctx),
434 ¶ms_from_c(params, params_len),
435 )?;
436 *out_assembly = buffer_from_vec(bytes);
437 Ok(())
438 })
439 }
440 }
441
442 #[allow(clippy::too_many_arguments)]
443 unsafe extern "C" fn start_stream(
444 handle: FolditPluginHandle,
445 session: u64,
446 op_id: *const u8,
447 op_id_len: usize,
448 ctx: *const FolditPluginDispatchContext,
449 params: *const FolditPluginParamEntry,
450 params_len: usize,
451 request_id: u64,
452 out_err: *mut FolditPluginError,
453 ) -> FolditPluginStatus {
454 unsafe {
455 call(out_err, || {
456 let op = ::std::str::from_utf8(slice_from_raw(op_id, op_id_len))
457 .map_err(|e| $crate::PluginError::Other(e.to_string()))?;
458 plugin(handle).start_stream(
459 session,
460 op,
461 &ctx_from_c(ctx),
462 ¶ms_from_c(params, params_len),
463 request_id,
464 )
465 })
466 }
467 }
468
469 unsafe extern "C" fn poll_stream(
470 handle: FolditPluginHandle,
471 request_id: u64,
472 out_buf: *mut FolditPluginBuffer,
473 out_err: *mut FolditPluginError,
474 ) -> FolditPluginStatus {
475 unsafe {
476 call(out_err, || {
477 let outcome = plugin(handle).poll_stream(request_id)?;
478 *out_buf = encode_to_buffer(&poll_outcome_to_proto(outcome));
479 Ok(())
480 })
481 }
482 }
483
484 unsafe extern "C" fn update_stream(
485 handle: FolditPluginHandle,
486 request_id: u64,
487 params: *const FolditPluginParamEntry,
488 params_len: usize,
489 out_err: *mut FolditPluginError,
490 ) -> FolditPluginStatus {
491 unsafe {
492 call(out_err, || {
493 plugin(handle).update_stream(request_id, ¶ms_from_c(params, params_len))
494 })
495 }
496 }
497
498 unsafe extern "C" fn cancel_stream(
499 handle: FolditPluginHandle,
500 request_id: u64,
501 out_err: *mut FolditPluginError,
502 ) -> FolditPluginStatus {
503 unsafe { call(out_err, || plugin(handle).cancel_stream(request_id)) }
504 }
505
506 #[allow(clippy::too_many_arguments)]
507 unsafe extern "C" fn query(
508 handle: FolditPluginHandle,
509 session: u64,
510 query_id: *const u8,
511 query_id_len: usize,
512 ctx: *const FolditPluginDispatchContext,
513 params: *const FolditPluginParamEntry,
514 params_len: usize,
515 assembly: *const u8,
516 assembly_len: usize,
517 out_data: *mut FolditPluginBuffer,
518 out_err: *mut FolditPluginError,
519 ) -> FolditPluginStatus {
520 unsafe {
521 call(out_err, || {
522 let q = ::std::str::from_utf8(slice_from_raw(query_id, query_id_len))
523 .map_err(|e| $crate::PluginError::Other(e.to_string()))?;
524 let bytes = plugin(handle).query(
525 session,
526 q,
527 &ctx_from_c(ctx),
528 ¶ms_from_c(params, params_len),
529 slice_from_raw(assembly, assembly_len),
530 )?;
531 *out_data = buffer_from_vec(bytes);
532 Ok(())
533 })
534 }
535 }
536
537 unsafe extern "C" fn free_buffer(buf: *mut FolditPluginBuffer) {
538 unsafe { drop_buffer(buf) };
539 }
540
541 unsafe extern "C" fn free_error(err: *mut FolditPluginError) {
542 unsafe { drop_error(err) };
543 }
544
545 static VTABLE: FolditPluginVtable = FolditPluginVtable {
546 abi_version: FOLDIT_PLUGIN_ABI_VERSION,
547 padding: 0,
548 create,
549 destroy,
550 register,
551 init,
552 update_assembly,
553 drop_session,
554 invoke,
555 start_stream,
556 poll_stream,
557 update_stream,
558 cancel_stream,
559 query,
560 free_buffer,
561 free_error,
562 };
563
564 #[no_mangle]
565 extern "C" fn foldit_plugin_vtable() -> *const FolditPluginVtable {
566 &raw const VTABLE
567 }
568 };
569 };
570}
571
572#[cfg(test)]
575mod tests {
576 use std::collections::HashMap;
577
578 use prost::Message as _;
579
580 use crate::abi::{
581 FolditPluginBuffer, FolditPluginError, FolditPluginStatus, FolditPluginVtable,
582 FOLDIT_PLUGIN_ABI_VERSION,
583 };
584 use crate::plugin::AssemblyPayload;
585 use crate::proto::plugin as proto;
586 use crate::protocol::{ParamValue, PollOutcome};
587 use crate::{Plugin, Result};
588
589 struct Dummy;
590
591 impl Plugin for Dummy {
592 fn init(
593 &self,
594 assembly_bytes: &[u8],
595 _assets: &[proto::PuzzleAsset],
596 _params: &HashMap<String, ParamValue>,
597 ) -> Result<(u64, Vec<u8>)> {
598 Ok((42, assembly_bytes.to_vec()))
599 }
600
601 fn register(&self) -> Result<proto::PluginRegistration> {
602 Ok(proto::PluginRegistration {
603 id: "dummy".into(),
604 version: "0.1.0".into(),
605 operations: vec![],
606 queries: vec![],
607 })
608 }
609
610 fn update_assembly(
611 &self,
612 _s: u64,
613 _p: AssemblyPayload<'_>,
614 _f: u64,
615 _t: u64,
616 ) -> Result<()> {
617 Ok(())
618 }
619
620 fn drop_session(&self, _s: u64) -> Result<()> {
621 Ok(())
622 }
623
624 fn poll_stream(&self, _request_id: u64) -> Result<PollOutcome> {
625 Ok(PollOutcome::Final {
626 assembly: vec![1, 2, 3],
627 score: None,
628 })
629 }
630 }
631
632 #[allow(
635 clippy::unnecessary_wraps,
636 reason = "signature is fixed by export_plugin!"
637 )]
638 fn ctor(_config_json: &str) -> Result<Box<dyn Plugin>> {
639 Ok(Box::new(Dummy))
640 }
641
642 crate::export_plugin!(ctor);
643
644 unsafe extern "C" {
645 fn foldit_plugin_vtable() -> *const FolditPluginVtable;
646 }
647
648 unsafe fn take(buf: &mut FolditPluginBuffer, vt: &FolditPluginVtable) -> Vec<u8> {
649 let bytes = unsafe { super::slice_from_raw(buf.data, buf.len) }.to_vec();
650 unsafe { (vt.free_buffer)(&raw mut *buf) };
651 bytes
652 }
653
654 #[test]
655 fn vtable_roundtrips_through_the_c_abi() {
656 let vt = unsafe { &*foldit_plugin_vtable() };
657 assert_eq!(vt.abi_version, FOLDIT_PLUGIN_ABI_VERSION);
658
659 let cfg = br#"{"plugin_dir":"/tmp"}"#;
660 let handle = unsafe { (vt.create)(cfg.as_ptr().cast(), cfg.len()) };
661 assert!(!handle.is_null(), "create returned null");
662
663 let mut err = FolditPluginError::empty();
664
665 let mut buf = FolditPluginBuffer::empty();
667 let status = unsafe { (vt.register)(handle, &raw mut buf, &raw mut err) };
668 assert!(matches!(status, FolditPluginStatus::Ok));
669 let reg = proto::PluginRegistration::decode(&unsafe { take(&mut buf, vt) }[..])
673 .unwrap_or_default();
674 assert_eq!(reg.id, "dummy");
675
676 let mut session = 0u64;
678 let mut initial = FolditPluginBuffer::empty();
679 let assembly = [7u8, 8, 9];
680 let status = unsafe {
681 (vt.init)(
682 handle,
683 assembly.as_ptr(),
684 assembly.len(),
685 std::ptr::null(),
686 0,
687 std::ptr::null(),
688 0,
689 &raw mut session,
690 &raw mut initial,
691 &raw mut err,
692 )
693 };
694 assert!(matches!(status, FolditPluginStatus::Ok));
695 assert_eq!(session, 42);
696 assert_eq!(unsafe { take(&mut initial, vt) }, assembly);
697
698 let mut poll = FolditPluginBuffer::empty();
700 let status = unsafe { (vt.poll_stream)(handle, 1, &raw mut poll, &raw mut err) };
701 assert!(matches!(status, FolditPluginStatus::Ok));
702 let resp = proto::PollStreamResponse::decode(&unsafe { take(&mut poll, vt) }[..])
703 .unwrap_or_default();
704 assert!(
705 matches!(
706 resp.result,
707 Some(proto::poll_stream_response::Result::Final(ref f)) if f.assembly == [1, 2, 3]
708 ),
709 "poll_stream must return Final with the echoed assembly, got {:?}",
710 resp.result
711 );
712
713 let mut out = FolditPluginBuffer::empty();
715 let op = b"nope";
716 let status = unsafe {
717 (vt.invoke)(
718 handle,
719 session,
720 op.as_ptr(),
721 op.len(),
722 std::ptr::null(),
723 std::ptr::null(),
724 0,
725 &raw mut out,
726 &raw mut err,
727 )
728 };
729 assert!(matches!(status, FolditPluginStatus::Unsupported));
730
731 unsafe { (vt.destroy)(handle) };
732 }
733}